From 16bc60644d6b5668b3fb4f4387e647dd2c9e2b79 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 17:11:22 +0200 Subject: [PATCH 001/412] Bump version to v1.15.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 523b9b019d..5d05c91373 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -2,7 +2,7 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 14 +MINOR_VERSION = 15 PATCH_VERSION = '0-dev' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 62d4b296626e7b8cc4c102c832d8ccc4ded35093 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 17:38:48 +0200 Subject: [PATCH 002/412] Format --- esphome/components/scd30/scd30.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 0b0e08387d..55ab07879e 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -113,8 +113,7 @@ void SCD30Component::update() { uint32_t temp_hum_u32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5]))); uint32_float_t humidity{.uint32 = temp_hum_u32}; - ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", - co2.value, temperature.value, humidity.value); + ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", co2.value, temperature.value, humidity.value); if (this->co2_sensor_ != nullptr) this->co2_sensor_->publish_state(co2.value); if (this->temperature_sensor_ != nullptr) From e553c0768e0dc117ff5721a6c1ddb0269a2af791 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 20:23:14 +0200 Subject: [PATCH 003/412] Link pip&python in lint Dockerfile --- docker/Dockerfile.lint | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 98c22d4c88..073ad90cb6 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -14,5 +14,7 @@ RUN \ COPY requirements_test.txt /requirements_test.txt RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt +RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python + VOLUME ["/esphome"] WORKDIR /esphome From 58b63118211cb2e9502d1949fde2ebbf8ee2a3dd Mon Sep 17 00:00:00 2001 From: Evan Coleman Date: Sat, 19 Oct 2019 14:44:43 -0400 Subject: [PATCH 004/412] Add SSD1325 Display Component (#736) * add ssd1325 component * fix i2c * remove ssd1325 i2c * add test * set max contrast * No macros - see styleguide * Remove invalid function * Formatting Co-authored-by: Otto Winter --- esphome/components/ssd1325_base/__init__.py | 42 +++++ .../components/ssd1325_base/ssd1325_base.cpp | 177 ++++++++++++++++++ .../components/ssd1325_base/ssd1325_base.h | 50 +++++ esphome/components/ssd1325_spi/__init__.py | 0 esphome/components/ssd1325_spi/display.py | 26 +++ .../components/ssd1325_spi/ssd1325_spi.cpp | 64 +++++++ esphome/components/ssd1325_spi/ssd1325_spi.h | 29 +++ tests/test1.yaml | 7 + 8 files changed, 395 insertions(+) create mode 100644 esphome/components/ssd1325_base/__init__.py create mode 100644 esphome/components/ssd1325_base/ssd1325_base.cpp create mode 100644 esphome/components/ssd1325_base/ssd1325_base.h create mode 100644 esphome/components/ssd1325_spi/__init__.py create mode 100644 esphome/components/ssd1325_spi/display.py create mode 100644 esphome/components/ssd1325_spi/ssd1325_spi.cpp create mode 100644 esphome/components/ssd1325_spi/ssd1325_spi.h diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py new file mode 100644 index 0000000000..69e11ec0d1 --- /dev/null +++ b/esphome/components/ssd1325_base/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') +SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer) +SSD1325Model = ssd1325_base_ns.enum('SSD1325Model') + +MODELS = { + 'SSD1325_128X32': SSD1325Model.SSD1325_MODEL_128_32, + 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, + 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, + 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, +} + +SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1325_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1036(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_EXTERNAL_VCC in config: + cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp new file mode 100644 index 0000000000..3079e19cc8 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -0,0 +1,177 @@ +#include "ssd1325_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1325_base { + +static const char *TAG = "ssd1325"; + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t SSD1325_SETCOLADDR = 0x15; +static const uint8_t SSD1325_SETROWADDR = 0x75; +static const uint8_t SSD1325_SETCONTRAST = 0x81; +static const uint8_t SSD1325_SETCURRENT = 0x84; + +static const uint8_t SSD1325_SETREMAP = 0xA0; +static const uint8_t SSD1325_SETSTARTLINE = 0xA1; +static const uint8_t SSD1325_SETOFFSET = 0xA2; +static const uint8_t SSD1325_NORMALDISPLAY = 0xA4; +static const uint8_t SSD1325_DISPLAYALLON = 0xA5; +static const uint8_t SSD1325_DISPLAYALLOFF = 0xA6; +static const uint8_t SSD1325_INVERTDISPLAY = 0xA7; +static const uint8_t SSD1325_SETMULTIPLEX = 0xA8; +static const uint8_t SSD1325_MASTERCONFIG = 0xAD; +static const uint8_t SSD1325_DISPLAYOFF = 0xAE; +static const uint8_t SSD1325_DISPLAYON = 0xAF; + +static const uint8_t SSD1325_SETPRECHARGECOMPENABLE = 0xB0; +static const uint8_t SSD1325_SETPHASELEN = 0xB1; +static const uint8_t SSD1325_SETROWPERIOD = 0xB2; +static const uint8_t SSD1325_SETCLOCK = 0xB3; +static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; +static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; +static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; +static const uint8_t SSD1325_SETVSL = 0xBF; + +static const uint8_t SSD1325_GFXACCEL = 0x23; +static const uint8_t SSD1325_DRAWRECT = 0x24; +static const uint8_t SSD1325_COPY = 0x25; + +void SSD1325::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1325_DISPLAYOFF); /* display off */ + this->command(SSD1325_SETCLOCK); /* set osc division */ + this->command(0xF1); /* 145 */ + this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ + this->command(0x3f); /* duty = 1/64 */ + this->command(SSD1325_SETOFFSET); /* set display offset --- */ + this->command(0x4C); /* 76 */ + this->command(SSD1325_SETSTARTLINE); /*set start line */ + this->command(0x00); /* ------ */ + this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(0x02); + this->command(SSD1325_SETREMAP); /* set segment remap------ */ + this->command(0x56); + this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETGRAYTABLE); + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + this->command(SSD1325_SETCONTRAST); /* set contrast current */ + this->command(0x7F); // max! + this->command(SSD1325_SETROWPERIOD); + this->command(0x51); + this->command(SSD1325_SETPHASELEN); + this->command(0x55); + this->command(SSD1325_SETPRECHARGECOMP); + this->command(0x02); + this->command(SSD1325_SETPRECHARGECOMPENABLE); + this->command(0x28); + this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin + this->command(0x1C); //? + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x0D | 0x02); + this->command(SSD1325_NORMALDISPLAY); /* set display mode */ + this->command(SSD1325_DISPLAYON); /* display ON */ +} +void SSD1325::display() { + this->command(SSD1325_SETCOLADDR); /* set column address */ + this->command(0x00); /* set column start address */ + this->command(0x3F); /* set column end address */ + this->command(SSD1325_SETROWADDR); /* set row address */ + this->command(0x00); /* set row start address */ + this->command(0x3F); /* set row end address */ + + this->write_display_data(); +} +void SSD1325::update() { + this->do_update_(); + this->display(); +} +int SSD1325::get_height_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return 32; + case SSD1325_MODEL_128_64: + return 64; + case SSD1325_MODEL_96_16: + return 16; + case SSD1325_MODEL_64_48: + return 48; + default: + return 0; + } +} +int SSD1325::get_width_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + case SSD1325_MODEL_128_64: + return 128; + case SSD1325_MODEL_96_16: + return 96; + case SSD1325_MODEL_64_48: + return 64; + default: + return 0; + } +} +size_t SSD1325::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y % 8; + if (color) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} +void SSD1325::fill(int color) { + uint8_t fill = color ? 0xFF : 0x00; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1325::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1325::model_str_() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return "SSD1325 128x32"; + case SSD1325_MODEL_128_64: + return "SSD1325 128x64"; + case SSD1325_MODEL_96_16: + return "SSD1325 96x16"; + case SSD1325_MODEL_64_48: + return "SSD1325 64x48"; + default: + return "Unknown"; + } +} + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h new file mode 100644 index 0000000000..e227f68f86 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1325_base { + +enum SSD1325Model { + SSD1325_MODEL_128_32 = 0, + SSD1325_MODEL_128_64, + SSD1325_MODEL_96_16, + SSD1325_MODEL_64_48, +}; + +class SSD1325 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1325Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(int color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, int color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1325Model model_{SSD1325_MODEL_128_64}; + GPIOPin *reset_pin_{nullptr}; + bool external_vcc_{false}; +}; + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/__init__.py b/esphome/components/ssd1325_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py new file mode 100644 index 0000000000..4615d45393 --- /dev/null +++ b/esphome/components/ssd1325_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1325_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1325_base'] +DEPENDENCIES = ['spi'] + +ssd1325_spi = cg.esphome_ns.namespace('ssd1325_spi') +SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1325), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1325_base.setup_ssd1036(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp new file mode 100644 index 0000000000..1f547f8fd3 --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -0,0 +1,64 @@ +#include "ssd1325_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1325_spi { + +static const char *TAG = "ssd1325_spi"; + +void SPISSD1325::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); + SSD1325::setup(); +} +void SPISSD1325::dump_config() { + LOG_DISPLAY("", "SPI SSD1325", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1325::command(uint8_t value) { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + this->cs_->digital_write(false); + this->write_byte(value); + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1325::write_display_data() { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + this->cs_->digital_write(false); + delay(1); + this->enable(); + for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { + for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once + uint8_t left8 = this->buffer_[y * 16 + x]; + uint8_t right8 = this->buffer_[y * 16 + x + 1]; + for (uint8_t p = 0; p < 8; p++) { + uint8_t d = 0; + if (left8 & (1 << p)) + d |= 0xF0; + if (right8 & (1 << p)) + d |= 0x0F; + this->write_byte(d); + } + } + } + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h new file mode 100644 index 0000000000..e4e7d55769 --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1325_base/ssd1325_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1325_spi { + +class SPISSD1325 : public ssd1325_base::SSD1325, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index ac199a0a4a..686ff027e0 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1333,6 +1333,13 @@ display: reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +- platform: ssd1325_spi + model: "SSD1325 128x64" + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper cs_pin: GPIO23 dc_pin: GPIO23 From 286ca07cc8e5c4b0b89ebdc02309390428b7122b Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:21:07 +0200 Subject: [PATCH 005/412] Add support for SGP30 eCO2 and TVOC sensors (#679) * Add support for SGP30 eCO2 and TVOC sensors * Added test for SGP30 * Lint issues fixed * Lint fixes * Fixed light lengths * Cleanup * Add support for Sensirion SCD30 CO2 sensors * Fixed few lint issues * Lint fixes * Fixed line ending for lint * Cleanup * Refactored float conversion * Refactor unnecessary return * Refactoring and cleanup * Updated uptime_sensor_ referencing and simplified checking on availability of copensation * Temperature and Humidity source moved to a separate compensation block; Dependency for Uptime sensor removed. * Both humidity_source and temperature_source are now mandatory if the compensation block is defined; * Clean up * Cleanup * Cleanup in search of perfection * Use correct comment style Co-authored-by: Otto Winter --- esphome/components/sgp30/__init__.py | 0 esphome/components/sgp30/sensor.py | 54 +++++ esphome/components/sgp30/sgp30.cpp | 295 +++++++++++++++++++++++++++ esphome/components/sgp30/sgp30.h | 54 +++++ tests/test1.yaml | 9 + 5 files changed, 412 insertions(+) create mode 100644 esphome/components/sgp30/__init__.py create mode 100644 esphome/components/sgp30/sensor.py create mode 100644 esphome/components/sgp30/sgp30.cpp create mode 100644 esphome/components/sgp30/sgp30.h diff --git a/esphome/components/sgp30/__init__.py b/esphome/components/sgp30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py new file mode 100644 index 0000000000..0063c29bd3 --- /dev/null +++ b/esphome/components/sgp30/sensor.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ + UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 + +DEPENDENCIES = ['i2c'] + +sgp30_ns = cg.esphome_ns.namespace('sgp30') +SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice) + +CONF_ECO2 = 'eco2' +CONF_TVOC = 'tvoc' +CONF_BASELINE = 'baseline' +CONF_UPTIME = 'uptime' +CONF_COMPENSATION = 'compensation' +CONF_COMPENSATION_HUMIDITY = 'humidity_source' +CONF_COMPENSATION_TEMPERATURE = 'temperature_source' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SGP30Component), + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), + cv.Optional(CONF_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema({ + cv.Required(CONF_COMPENSATION_HUMIDITY): cv.use_id(sensor.Sensor), + cv.Required(CONF_COMPENSATION_TEMPERATURE): cv.use_id(sensor.Sensor) + }), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_ECO2 in config: + sens = yield sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_eco2_sensor(sens)) + + if CONF_TVOC in config: + sens = yield sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc_sensor(sens)) + + if CONF_BASELINE in config: + cg.add(var.set_baseline(config[CONF_BASELINE])) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = yield cg.get_variable(compensation_config[CONF_COMPENSATION_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) + sens = yield cg.get_variable(compensation_config[CONF_COMPENSATION_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp new file mode 100644 index 0000000000..9a73295447 --- /dev/null +++ b/esphome/components/sgp30/sgp30.cpp @@ -0,0 +1,295 @@ +#include "sgp30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sgp30 { + +static const char *TAG = "sgp30"; + +static const uint16_t SGP30_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP30_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP30_CMD_IAQ_INIT = 0x2003; +static const uint16_t SGP30_CMD_MEASURE_IAQ = 0x2008; +static const uint16_t SGP30_CMD_SET_ABSOLUTE_HUMIDITY = 0x2061; +static const uint16_t SGP30_CMD_GET_IAQ_BASELINE = 0x2015; +static const uint16_t SGP30_CMD_SET_IAQ_BASELINE = 0x201E; + +// Sensor baseline should first be relied on after 1H of operation, +// if the sensor starts with a baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; + +// Sensor baseline could first be relied on after 12H of operation, +// if the sensor starts without any prior baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; + +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)) { + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + + // Featureset identification for future use + if (!this->write_command_(SGP30_CMD_GET_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]; + if (uint16_t(this->featureset_ >> 12) != 0x0) { + if (uint16_t(this->featureset_ >> 12) == 0x1) { + // ID matching a different sensor: SGPC3 + this->error_code_ = UNSUPPORTED_ID; + } else { + // Unknown ID + this->error_code_ = INVALID_ID; + } + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + // Sensor initialization + 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(); + return; + } + + // Sensor baseline reliability timer + if (this->baseline_ > 0) { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; + this->write_iaq_baseline_(this->baseline_); + } else { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE; + } +} + +bool SGP30Component::is_sensor_baseline_reliable_() { + if ((this->required_warm_up_time_ == 0) || (std::floor(millis() / 1000) >= this->required_warm_up_time_)) { + // requirement for warm up is removed once the millis uptime surpasses the required warm_up_time + // this avoids the repetitive warm up when the millis uptime is rolled over every ~40 days + this->required_warm_up_time_ = 0; + return true; + } + return false; +} + +void SGP30Component::read_iaq_baseline_() { + if (this->is_sensor_baseline_reliable_()) { + 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)) { + this->status_set_warning(); + return; + } + + uint8_t eco2baseline = (raw_data[0]); + uint8_t tvocbaseline = (raw_data[1]); + + ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF))); + this->status_clear_warning(); + }); + } else { + ESP_LOGD(TAG, "Baseline reading not available for: %.0fs", + (this->required_warm_up_time_ - std::floor(millis() / 1000))); + } +} + +void SGP30Component::send_env_data_() { + if (this->humidity_sensor_ == nullptr && this->temperature_sensor_ == nullptr) + return; + float humidity = NAN; + if (this->humidity_sensor_ != nullptr) + humidity = this->humidity_sensor_->state; + if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); + } + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); + } + + float absolute_humidity = + 216.7f * (((humidity / 100) * 6.112f * std::exp((17.62f * temperature) / (243.12f + temperature))) / + (273.15f + temperature)); + uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); + uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); + ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); + uint8_t crc = sht_crc_(humidity_full, humidity_dec); + uint8_t data[4]; + data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; + data[1] = humidity_full; + data[2] = humidity_dec; + data[3] = crc; + if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { + ESP_LOGE(TAG, "Error sending compensation data."); + } +} + +void SGP30Component::write_iaq_baseline_(uint16_t baseline) { + uint8_t e_c_o2_baseline = baseline >> 8; + uint8_t tvoc_baseline = baseline & 0xFF; + uint8_t data[4]; + data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; + data[1] = e_c_o2_baseline; + data[2] = tvoc_baseline; + data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline); + if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) { + ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline); + } else + ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline); +} + +void SGP30Component::dump_config() { + ESP_LOGCONFIG(TAG, "SGP30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case INVALID_ID: + ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + break; + case UNSUPPORTED_ID: + ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)."); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Baseline: 0x%04X%s", this->baseline_, + ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)")); + ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "eCO2", this->eco2_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } +} + +void SGP30Component::update() { + if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + float eco2 = (raw_data[0]); + float tvoc = (raw_data[1]); + + ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + if (this->eco2_sensor_ != nullptr) + this->eco2_sensor_->publish_state(eco2); + if (this->tvoc_sensor_ != nullptr) + this->tvoc_sensor_->publish_state(tvoc); + this->status_clear_warning(); + this->send_env_data_(); + this->read_iaq_baseline_(); + }); +} + +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; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + 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); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sgp30 +} // namespace esphome diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h new file mode 100644 index 0000000000..2362d1bca6 --- /dev/null +++ b/esphome/components/sgp30/sgp30.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include + +namespace esphome { +namespace sgp30 { + +/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. +class SGP30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } + void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_baseline(uint16_t baseline) { baseline_ = baseline; } + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + 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 baseline); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + uint64_t serial_number_; + uint16_t featureset_; + long required_warm_up_time_; + + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *eco2_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + uint16_t baseline_{0x0000}; + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace sgp30 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 686ff027e0..913fb6d549 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -531,6 +531,15 @@ sensor: name: "Living Room Humidity 9" address: 0x61 update_interval: 15s + - platform: sgp30 + eco2: + name: "Workshop eCO2" + accuracy_decimals: 1 + tvoc: + name: "Workshop TVOC" + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s - platform: template name: "Template Sensor" id: template_sensor From 7063aa60093c68ae0f62a22e45dea9ee174cb44c Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:31:27 +0200 Subject: [PATCH 006/412] Add support for SHTCx Temperature sensors (#676) * Add support for Sensirion STS3x Temperature sensors * Removed humidty reading from STS3x sensor * Fixed line error and operand error * Fixed syntax * Add test snippet for STS3x sensor * Clean up * Add support for Sensirion SHTC1 and SHTC3 Temperature sensors * Fixed the test * Fix lint issues * Update esphome/components/shtcx/shtcx.cpp Good point. Co-Authored-By: Otto Winter * Refactored device type identification and logging * Refactoring and cleanup * Remove sts3x Co-authored-by: Otto Winter --- esphome/components/shtcx/__init__.py | 0 esphome/components/shtcx/sensor.py | 32 ++++++ esphome/components/shtcx/shtcx.cpp | 166 +++++++++++++++++++++++++++ esphome/components/shtcx/shtcx.h | 35 ++++++ tests/test1.yaml | 7 ++ 5 files changed, 240 insertions(+) create mode 100644 esphome/components/shtcx/__init__.py create mode 100644 esphome/components/shtcx/sensor.py create mode 100644 esphome/components/shtcx/shtcx.cpp create mode 100644 esphome/components/shtcx/shtcx.h diff --git a/esphome/components/shtcx/__init__.py b/esphome/components/shtcx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py new file mode 100644 index 0000000000..eb215078e7 --- /dev/null +++ b/esphome/components/shtcx/sensor.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \ + ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +shtcx_ns = cg.esphome_ns.namespace('shtcx') +SHTCXComponent = shtcx_ns.class_('SHTCXComponent', cg.PollingComponent, i2c.I2CDevice) + +SHTCXType = shtcx_ns.enum('SHTCXType') + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SHTCXComponent), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x70)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp new file mode 100644 index 0000000000..d52bdb1257 --- /dev/null +++ b/esphome/components/shtcx/shtcx.cpp @@ -0,0 +1,166 @@ +#include "shtcx.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace shtcx { + +static const char *TAG = "shtcx"; + +static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; +static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; +static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; +static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; +static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; + +inline const char* to_string(SHTCXType type) { + switch (type) { + case SHTCX_TYPE_SHTC3: + return "SHTC3"; + case SHTCX_TYPE_SHTC1: + return "SHTC1"; + default: + return "[Unknown model]"; + } +} + +void SHTCXComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up SHTCx..."); + this->soft_reset(); + + 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)) { + 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->type_ = SHTCX_TYPE_SHTC3; + } else { + this->type_ = SHTCX_TYPE_SHTC1; + } + } else { + this->type_ = SHTCX_TYPE_UNKNOWN; + } + ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_)); +} +void SHTCXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SHTCx:"); + ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_)); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SHTCx failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} +float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } +void SHTCXComponent::update() { + if (this->status_has_warning()) { + ESP_LOGW(TAG, "Retrying to reconnect the sensor."); + this->soft_reset(); + } + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->wake_up(); + } + if (!this->write_command_(SHTCX_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)) { + this->status_set_warning(); + return; + } + + 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) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->sleep(); + } + }); +} + +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; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + 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); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +void SHTCXComponent::soft_reset() { + this->write_command_(SHTCX_COMMAND_SOFT_RESET); + delayMicroseconds(200); +} +void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); } + +void SHTCXComponent::wake_up() { + this->write_command_(SHTCX_COMMAND_WAKEUP); + delayMicroseconds(200); +} + +} // namespace shtcx +} // namespace esphome diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h new file mode 100644 index 0000000000..ccc6533bfa --- /dev/null +++ b/esphome/components/shtcx/shtcx.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +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 { + 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; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void soft_reset(); + void sleep(); + void wake_up(); + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + SHTCXType type_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace shtcx +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 913fb6d549..19dc00aa88 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -540,6 +540,13 @@ sensor: accuracy_decimals: 1 address: 0x58 update_interval: 5s + - platform: shtcx + temperature: + name: "Living Room Temperature 10" + humidity: + name: "Living Room Humidity 10" + address: 0x70 + update_interval: 15s - platform: template name: "Template Sensor" id: template_sensor From 18426b71e46e6267046210a8e08ddc9c22d96c51 Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:31:37 +0200 Subject: [PATCH 007/412] Add support for STS3x Temperature sensors (#669) * Add support for Sensirion STS3x Temperature sensors * Removed humidty reading from STS3x sensor * Fixed line error and operand error * Fixed syntax * Add test snippet for STS3x sensor * Clean up * #550 Proactive fix for STS3x component reporting WARNING status and reinitialzing similar to SHT3xd * Flattened config. * Fixed missing temperature unit * Code formatting * Added marking for future commands * Cleanup * Removed whitespace * Cleanup * Cleanup --- esphome/components/sts3x/__init__.py | 0 esphome/components/sts3x/sensor.py | 22 +++++ esphome/components/sts3x/sts3x.cpp | 123 +++++++++++++++++++++++++++ esphome/components/sts3x/sts3x.h | 24 ++++++ tests/test1.yaml | 3 + 5 files changed, 172 insertions(+) create mode 100644 esphome/components/sts3x/__init__.py create mode 100644 esphome/components/sts3x/sensor.py create mode 100644 esphome/components/sts3x/sts3x.cpp create mode 100644 esphome/components/sts3x/sts3x.h diff --git a/esphome/components/sts3x/__init__.py b/esphome/components/sts3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py new file mode 100644 index 0000000000..f48deeeae5 --- /dev/null +++ b/esphome/components/sts3x/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS + +DEPENDENCIES = ['i2c'] + +sts3x_ns = cg.esphome_ns.namespace('sts3x') + +STS3XComponent = sts3x_ns.class_('STS3XComponent', sensor.Sensor, + cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(STS3XComponent), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x4A)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp new file mode 100644 index 0000000000..1a24a17caf --- /dev/null +++ b/esphome/components/sts3x/sts3x.cpp @@ -0,0 +1,123 @@ +#include "sts3x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sts3x { + +static const char *TAG = "sts3x"; + +static const uint16_t STS3X_COMMAND_READ_SERIAL_NUMBER = 0x3780; +static const uint16_t STS3X_COMMAND_READ_STATUS = 0xF32D; +static const uint16_t STS3X_COMMAND_SOFT_RESET = 0x30A2; +static const uint16_t STS3X_COMMAND_POLLING_H = 0x2400; + +/// Commands for future use +static const uint16_t STS3X_COMMAND_CLEAR_STATUS = 0x3041; +static const uint16_t STS3X_COMMAND_HEATER_ENABLE = 0x306D; +static const uint16_t STS3X_COMMAND_HEATER_DISABLE = 0x3066; +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)) { + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[2]; + if (!this->read_data_(raw_serial_number, 1)) { + this->mark_failed(); + return; + } + uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16); + ESP_LOGV(TAG, " Serial Number: 0x%08X", serial_number); +} +void STS3XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "STS3x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ST3x failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "STS3x", this); +} +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); + } + 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)) { + this->status_set_warning(); + return; + } + + float temperature = 175.0f * float(raw_data[0]) / 65535.0f - 45.0f; + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + }); +} + +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; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + 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); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sts3x +} // namespace esphome diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h new file mode 100644 index 0000000000..436cf938d8 --- /dev/null +++ b/esphome/components/sts3x/sts3x.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.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 { + 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 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 19dc00aa88..2cdbbe8cff 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -522,6 +522,9 @@ sensor: name: "Living Room Humidity 8" address: 0x44 update_interval: 15s + - platform: sts3x + name: "Living Room Temperature 9" + address: 0x4A - platform: scd30 co2: name: "Living Room CO2 9" From af15a4e71053f9bbe38a5686b632f5a8b1e964e6 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 19 Oct 2019 16:37:05 -0300 Subject: [PATCH 008/412] Add dfplayer mini component (#655) * Add dfplayer mini component * receiving some data * implemented many actions * lint * undo homeassistant_time.h * Update esphome/components/dfplayer/__init__.py Co-Authored-By: Otto Winter * Update esphome/components/dfplayer/dfplayer.cpp Co-Authored-By: Otto Winter * add set device. fixes * lint * Fixes and sync with docs * add test * lint * lint * lint --- esphome/components/dfplayer/__init__.py | 221 +++++++++++++++++++++++ esphome/components/dfplayer/dfplayer.cpp | 120 ++++++++++++ esphome/components/dfplayer/dfplayer.h | 165 +++++++++++++++++ tests/test3.yaml | 87 +++++++++ 4 files changed, 593 insertions(+) create mode 100644 esphome/components/dfplayer/__init__.py create mode 100644 esphome/components/dfplayer/dfplayer.cpp create mode 100644 esphome/components/dfplayer/dfplayer.h diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py new file mode 100644 index 0000000000..78bcfa80f4 --- /dev/null +++ b/esphome/components/dfplayer/__init__.py @@ -0,0 +1,221 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +dfplayer_ns = cg.esphome_ns.namespace('dfplayer') +DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) +DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger', + automation.Trigger.template()) +DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition) + +MULTI_CONF = True +CONF_FOLDER = 'folder' +CONF_FILE = 'file' +CONF_LOOP = 'loop' +CONF_VOLUME = 'volume' +CONF_DEVICE = 'device' +CONF_EQ_PRESET = 'eq_preset' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +EqPreset = dfplayer_ns.enum("EqPreset") +EQ_PRESET = { + 'NORMAL': EqPreset.NORMAL, + 'POP': EqPreset.POP, + 'ROCK': EqPreset.ROCK, + 'JAZZ': EqPreset.JAZZ, + 'CLASSIC': EqPreset.CLASSIC, + 'BASS': EqPreset.BASS, +} +Device = dfplayer_ns.enum("Device") +DEVICE = { + 'USB': Device.USB, + 'TF_CARD': Device.TF_CARD, +} + +NextAction = dfplayer_ns.class_('NextAction', automation.Action) +PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action) +PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action) +PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action) +SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action) +SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action) +SleepAction = dfplayer_ns.class_('SleepAction', automation.Action) +ResetAction = dfplayer_ns.class_('ResetAction', automation.Action) +StartAction = dfplayer_ns.class_('StartAction', automation.Action) +PauseAction = dfplayer_ns.class_('PauseAction', automation.Action) +StopAction = dfplayer_ns.class_('StopAction', automation.Action) +RandomAction = dfplayer_ns.class_('RandomAction', automation.Action) +SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(DFPlayer), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger), + }), +}).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_next_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_previous_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +}, key=CONF_FILE)) +def dfplayer_play_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FOLDER): cv.templatable(cv.int_), + cv.Optional(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +})) +def dfplayer_play_folder_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FOLDER], args, float) + cg.add(var.set_folder(template_)) + if CONF_FILE in config: + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), +}, key=CONF_DEVICE)) +def dfplayer_set_device_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_DEVICE], args, Device) + cg.add(var.set_device(template_)) + yield var + + +@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_VOLUME): cv.templatable(cv.int_), +}, key=CONF_VOLUME)) +def dfplayer_set_volume_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_VOLUME], args, float) + cg.add(var.set_volume(template_)) + yield var + + +@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), +}, key=CONF_EQ_PRESET)) +def dfplayer_set_eq_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) + cg.add(var.set_eq(template_)) + yield var + + +@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_sleep_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.start', StartAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_start_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_pause_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.random', RandomAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_random_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp new file mode 100644 index 0000000000..9589a907c4 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -0,0 +1,120 @@ +#include "dfplayer.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfplayer { + +static const char* TAG = "dfplayer"; + +void DFPlayer::play_folder(uint16_t folder, uint16_t file) { + if (folder < 100 && file < 256) { + this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); + } else if (folder <= 10 && file <= 1000) { + this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); + } else { + ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file); + } +} + +void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { + uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; + uint16_t checksum = 0; + for (uint8_t i = 1; i < 7; i++) + checksum += buffer[i]; + checksum = -checksum; + buffer[7] = checksum >> 8; + buffer[8] = (uint8_t) checksum; + + this->sent_cmd_ = cmd; + + ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument); + this->write_array(buffer, 10); +} + +void DFPlayer::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card loaded"); + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card unloaded"); + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3D: // Playback finished + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; + this->read_pos_ = 0; + continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + } +} + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h new file mode 100644 index 0000000000..2cb9eea4eb --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.h @@ -0,0 +1,165 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +const size_t DFPLAYER_READ_BUFFER_LENGTH = 25; // two messages + some extra + +namespace esphome { +namespace dfplayer { + +enum EqPreset { + NORMAL = 0, + POP = 1, + ROCK = 2, + JAZZ = 3, + CLASSIC = 4, + BASS = 5, +}; + +enum Device { + USB = 1, + TF_CARD = 2, +}; + +class DFPlayer : public uart::UARTDevice, public Component { + public: + void loop() override; + + void next() { this->send_cmd_(0x01); } + void previous() { this->send_cmd_(0x02); } + void play_file(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x03, file); + } + void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); } + void play_folder(uint16_t folder, uint16_t file); + void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); } + void volume_up() { this->send_cmd_(0x04); } + void volume_down() { this->send_cmd_(0x05); } + void set_device(Device device) { this->send_cmd_(0x09, device); } + void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } + void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } + void sleep() { this->send_cmd_(0x0A); } + void reset() { this->send_cmd_(0x0C); } + void start() { this->send_cmd_(0x0D); } + void pause() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0E); + } + void stop() { this->send_cmd_(0x16); } + void random() { this->send_cmd_(0x18); } + + bool is_playing() { return is_playing_; } + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + void send_cmd_(uint8_t cmd, uint16_t argument = 0); + void send_cmd_(uint8_t cmd, uint16_t high, uint16_t low) { + this->send_cmd_(cmd, ((high & 0xFF) << 8) | (low & 0xFF)); + } + uint8_t sent_cmd_{0}; + + char read_buffer_[DFPLAYER_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + bool is_playing_{false}; + bool ack_set_is_playing_{false}; + bool ack_reset_is_playing_{false}; + + CallbackManager on_finished_playback_callback_; +}; + +#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ + template class ACTION_CLASS : public Action, public Parented { \ + public: \ + void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + }; + +DFPLAYER_SIMPLE_ACTION(NextAction, next) +DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) + +template class PlayFileAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_file_loop(file); + } else { + this->parent_->play_file(file); + } + } +}; + +template class PlayFolderAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, folder) + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto folder = this->folder_.value(x...); + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_folder_loop(folder); + } else { + this->parent_->play_folder(folder, file); + } + } +}; + +template class SetDeviceAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { + auto device = this->device_.value(x...); + this->parent_->set_device(device); + } +}; + +template class SetVolumeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { + auto volume = this->volume_.value(x...); + this->parent_->set_volume(volume); + } +}; + +template class SetEqAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { + auto eq = this->eq_.value(x...); + this->parent_->set_eq(eq); + } +}; + +DFPLAYER_SIMPLE_ACTION(SleepAction, sleep) +DFPLAYER_SIMPLE_ACTION(ResetAction, reset) +DFPLAYER_SIMPLE_ACTION(StartAction, start) +DFPLAYER_SIMPLE_ACTION(PauseAction, pause) +DFPLAYER_SIMPLE_ACTION(StopAction, stop) +DFPLAYER_SIMPLE_ACTION(RandomAction, random) + +template class DFPlayerIsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class DFPlayerFinishedPlaybackTrigger : public Trigger<> { + public: + explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace dfplayer +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index a7e8d26168..df4afe5ad9 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -60,6 +60,83 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + - service: dfplayer_next + then: + - dfplayer.play_next: + - service: dfplayer_previous + then: + - dfplayer.play_previous: + - service: dfplayer_play + variables: + file: int + then: + - dfplayer.play: !lambda 'return file;' + - service: dfplayer_play_loop + variables: + file: int + loop_: bool + then: + - dfplayer.play: + file: !lambda 'return file;' + loop: !lambda 'return loop_;' + - service: dfplayer_play_folder + variables: + folder: int + file: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + file: !lambda 'return file;' + + - service: dfplayer_play_loo_folder + variables: + folder: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + loop: True + + - service: dfplayer_set_device + variables: + device: int + then: + - dfplayer.set_device: + device: TF_CARD + + - service: dfplayer_set_volume + variables: + volume: int + then: + - dfplayer.set_volume: !lambda 'return volume;' + - service: dfplayer_set_eq + variables: + preset: int + then: + - dfplayer.set_eq: !lambda 'return static_cast(preset);' + + - service: dfplayer_sleep + then: + - dfplayer.sleep + + - service: dfplayer_reset + then: + - dfplayer.reset + + - service: dfplayer_start + then: + - dfplayer.start + + - service: dfplayer_pause + then: + - dfplayer.pause + + - service: dfplayer_stop + then: + - dfplayer.stop + + - service: dfplayer_random + then: + - dfplayer.random wifi: ssid: 'MySSID' @@ -532,3 +609,13 @@ sim800l: - sim800l.send_sms: message: 'hello you' recipient: '+1234' + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: + dfplayer.is_playing + then: + logger.log: 'Playback finished event' From 96ff9a162c23f37a92a3fd60e17edfb3e49f293d Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 19 Oct 2019 12:47:24 -0700 Subject: [PATCH 009/412] Add new component for Tuya dimmers (#743) * Add new component for Tuya dimmers * Update code * Class naming * Log output * Fixes * Lint * Format * Fix test * log setting datapoint values * remove in_setup_ and fix datapoint handling Co-authored-by: Samuel Sieb Co-authored-by: Otto Winter --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 23 +- esphome/components/tuya/__init__.py | 20 ++ esphome/components/tuya/light/__init__.py | 38 +++ esphome/components/tuya/light/tuya_light.cpp | 85 +++++ esphome/components/tuya/light/tuya_light.h | 36 +++ esphome/components/tuya/tuya.cpp | 294 ++++++++++++++++++ esphome/components/tuya/tuya.h | 73 +++++ esphome/core/helpers.cpp | 16 + esphome/core/helpers.h | 3 + 9 files changed, 571 insertions(+), 17 deletions(-) create mode 100644 esphome/components/tuya/__init__.py create mode 100644 esphome/components/tuya/light/__init__.py create mode 100644 esphome/components/tuya/light/tuya_light.cpp create mode 100644 esphome/components/tuya/light/tuya_light.h create mode 100644 esphome/components/tuya/tuya.cpp create mode 100644 esphome/components/tuya/tuya.h diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index ff1fe59668..c578acdaea 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,6 +1,7 @@ #include "esp32_ble_tracker.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 @@ -202,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode(const std::string &raw_data) { - char buf[20]; - std::string res; - for (size_t i = 0; i < raw_data.size(); i++) { - if (i + 1 != raw_data.size()) { - sprintf(buf, "0x%02X.", static_cast(raw_data[i])); - } else { - sprintf(buf, "0x%02X ", static_cast(raw_data[i])); - } - res += buf; - } - sprintf(buf, "(%zu)", raw_data.size()); - res += buf; - return res; +std::string hexencode_string(const std::string &raw_data) { + return hexencode(reinterpret_cast(raw_data.c_str()), raw_data.size()); } ESPBTUUID::ESPBTUUID() : uuid_() {} @@ -327,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e for (auto uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode(this->service_data_).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); + ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); if (this->service_data_uuid_.has_value()) { ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); } ESP_LOGVV(TAG, "Adv data: %s", - hexencode(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); + hexencode_string(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py new file mode 100644 index 0000000000..541f10f862 --- /dev/null +++ b/esphome/components/tuya/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ['uart'] + +tuya_ns = cg.esphome_ns.namespace('tuya') +Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) + +CONF_TUYA_ID = 'tuya_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tuya), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py new file mode 100644 index 0000000000..ec588d6cc9 --- /dev/null +++ b/esphome/components/tuya/light/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import light +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_DIMMER_DATAPOINT = "dimmer_datapoint" +CONF_SWITCH_DATAPOINT = "switch_datapoint" + +TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) + +CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_MIN_VALUE): cv.int_, + cv.Optional(CONF_MAX_VALUE): cv.int_, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + yield light.register_light(var, config) + + if CONF_DIMMER_DATAPOINT in config: + cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_MIN_VALUE in config: + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + if CONF_MAX_VALUE in config: + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp new file mode 100644 index 0000000000..9696252049 --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -0,0 +1,85 @@ +#include "esphome/core/log.h" +#include "tuya_light.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.light"; + +void TuyaLight::setup() { + if (this->dimmer_id_.has_value()) { + this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_brightness(float(datapoint.value_uint) / this->max_value_); + call.perform(); + }); + } + if (switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_state(datapoint.value_bool); + call.perform(); + }); + } +} + +void TuyaLight::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Dimmer:"); + if (this->dimmer_id_.has_value()) + ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); +} + +light::LightTraits TuyaLight::get_traits() { + auto traits = light::LightTraits(); + traits.set_supports_brightness(this->dimmer_id_.has_value()); + return traits; +} + +void TuyaLight::setup_state(light::LightState *state) { state_ = state; } + +void TuyaLight::write_state(light::LightState *state) { + float brightness; + state->current_values_as_brightness(&brightness); + + if (brightness == 0.0f) { + // turning off, first try via switch (if exists), then dimmer + if (switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = false; + + parent_->set_datapoint_value(datapoint); + } else if (dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = 0; + parent_->set_datapoint_value(datapoint); + } + return; + } + + auto brightness_int = static_cast(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + if (this->dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = brightness_int; + parent_->set_datapoint_value(datapoint); + } + if (this->switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = true; + parent_->set_datapoint_value(datapoint); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h new file mode 100644 index 0000000000..581512c29c --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace tuya { + +class TuyaLight : public Component, public light::LightOutput { + public: + void setup() override; + void dump_config() override; + void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_min_value(uint32_t min_value) { min_value_ = min_value; } + void set_max_value(uint32_t max_value) { max_value_ = max_value; } + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override; + void write_state(light::LightState *state) override; + + protected: + void update_dimmer_(uint32_t value); + void update_switch_(uint32_t value); + + Tuya *parent_; + optional dimmer_id_{}; + optional switch_id_{}; + uint32_t min_value_ = 0; + uint32_t max_value_ = 255; + light::LightState *state_{nullptr}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp new file mode 100644 index 0000000000..ae3e7eed43 --- /dev/null +++ b/esphome/components/tuya/tuya.cpp @@ -0,0 +1,294 @@ +#include "tuya.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya"; + +void Tuya::setup() { + this->send_empty_command_(TuyaCommandType::MCU_CONF); + this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); +} + +void Tuya::loop() { + while (this->available()) { + uint8_t c; + this->read_byte(&c); + this->handle_char_(c); + } +} + +void Tuya::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya:"); + if ((gpio_status_ != -1) || (gpio_reset_ != -1)) + ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + for (auto &info : this->datapoints_) { + if (info.type == TuyaDatapointType::BOOLEAN) + ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); + else if (info.type == TuyaDatapointType::INTEGER) + ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::ENUM) + ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); + else if (info.type == TuyaDatapointType::BITMASK) + ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask); + else + ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); + } +} + +bool Tuya::validate_message_() { + uint32_t at = this->rx_message_.size() - 1; + auto *data = &this->rx_message_[0]; + uint8_t new_byte = data[at]; + + // Byte 0: HEADER1 (always 0x55) + if (at == 0) + return new_byte == 0x55; + // Byte 1: HEADER2 (always 0xAA) + if (at == 1) + return new_byte == 0xAA; + + // Byte 2: VERSION + // no validation for the following fields: + uint8_t version = data[2]; + if (at == 2) + return true; + // Byte 3: COMMAND + uint8_t command = data[3]; + if (at == 3) + return true; + + // Byte 4: LENGTH1 + // Byte 5: LENGTH2 + if (at <= 5) + // no validation for these fields + return true; + + uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5])); + + // wait until all data is read + if (at - 6 < length) + return true; + + // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256 + uint8_t rx_checksum = new_byte; + uint8_t calc_checksum = 0; + for (uint32_t i = 0; i < 6 + length; i++) + calc_checksum += data[i]; + + if (rx_checksum != calc_checksum) { + ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum); + return false; + } + + // valid message + const uint8_t *message_data = data + 6; + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, + hexencode(message_data, length).c_str()); + this->handle_command_(command, version, message_data, length); + + // return false to reset rx buffer + return false; +} + +void Tuya::handle_char_(uint8_t c) { + this->rx_message_.push_back(c); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } +} + +void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { + uint8_t c; + switch ((TuyaCommandType) command) { + case TuyaCommandType::HEARTBEAT: + ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); + if (buffer[0] == 0) { + ESP_LOGI(TAG, "MCU restarted"); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + } + break; + case TuyaCommandType::QUERY_PRODUCT: { + // check it is a valid string + bool valid = false; + for (int i = 0; i < len; i++) { + if (buffer[i] == 0x00) { + valid = true; + break; + } + } + if (valid) { + ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast(buffer)); + } + break; + } + case TuyaCommandType::MCU_CONF: + if (len >= 2) { + gpio_status_ = buffer[0]; + gpio_reset_ = buffer[1]; + } + // set wifi state LED to off or on depending on the MCU firmware + // but it shouldn't be blinking + c = 0x3; + this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + break; + case TuyaCommandType::WIFI_STATE: + break; + case TuyaCommandType::WIFI_RESET: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); + break; + case TuyaCommandType::WIFI_SELECT: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); + break; + case TuyaCommandType::SET_DATAPOINT: + break; + case TuyaCommandType::STATE: { + this->handle_datapoint_(buffer, len); + break; + } + case TuyaCommandType::QUERY_STATE: + break; + default: + ESP_LOGE(TAG, "invalid command (%02x) received", command); + } +} + +void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { + if (len < 2) + return; + + TuyaDatapoint datapoint{}; + datapoint.id = buffer[0]; + datapoint.type = (TuyaDatapointType) buffer[1]; + datapoint.value_uint = 0; + + size_t data_size = (buffer[2] << 8) + buffer[3]; + const uint8_t *data = buffer + 4; + size_t data_len = len - 4; + if (data_size != data_len) { + ESP_LOGW(TAG, "invalid datapoint update"); + return; + } + + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + if (data_len != 1) + return; + datapoint.value_bool = data[0]; + break; + case TuyaDatapointType::INTEGER: + if (data_len != 4) + return; + datapoint.value_uint = + (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + break; + case TuyaDatapointType::ENUM: + if (data_len != 1) + return; + datapoint.value_enum = data[0]; + break; + case TuyaDatapointType::BITMASK: + if (data_len != 2) + return; + datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0); + break; + default: + return; + } + ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint); + + // Update internal datapoints + bool found = false; + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + other = datapoint; + found = true; + } + } + if (!found) { + this->datapoints_.push_back(datapoint); + // New datapoint found, reprint dump_config after a delay. + this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); + } + + // Run through listeners + for (auto &listener : this->listeners_) + if (listener.datapoint_id == datapoint.id) + listener.on_datapoint(datapoint); +} + +void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { + uint8_t len_hi = len >> 8; + uint8_t len_lo = len >> 0; + this->write_array({0x55, 0xAA, + 0x00, // version + (uint8_t) command, len_hi, len_lo}); + if (len != 0) + this->write_array(buffer, len); + + uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo; + for (int i = 0; i < len; i++) + checksum += buffer[i]; + this->write_byte(checksum); +} + +void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { + std::vector buffer; + ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + if (other.value_uint == datapoint.value_uint) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + } + } + buffer.push_back(datapoint.id); + buffer.push_back(static_cast(datapoint.type)); + + std::vector data; + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + data.push_back(datapoint.value_bool); + break; + case TuyaDatapointType::INTEGER: + data.push_back(datapoint.value_uint >> 24); + data.push_back(datapoint.value_uint >> 16); + data.push_back(datapoint.value_uint >> 8); + data.push_back(datapoint.value_uint >> 0); + break; + case TuyaDatapointType::ENUM: + data.push_back(datapoint.value_enum); + break; + case TuyaDatapointType::BITMASK: + data.push_back(datapoint.value_bitmask >> 8); + data.push_back(datapoint.value_bitmask >> 0); + break; + default: + return; + } + + buffer.push_back(data.size() >> 8); + buffer.push_back(data.size() >> 0); + buffer.insert(buffer.end(), data.begin(), data.end()); + this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); +} + +void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { + auto listener = TuyaDatapointListener{ + .datapoint_id = datapoint_id, + .on_datapoint = func, + }; + this->listeners_.push_back(listener); + + // Run through existing datapoints + for (auto &datapoint : this->datapoints_) + if (datapoint.id == datapoint_id) + func(datapoint); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h new file mode 100644 index 0000000000..6bc6d92da0 --- /dev/null +++ b/esphome/components/tuya/tuya.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace tuya { + +enum class TuyaDatapointType : uint8_t { + RAW = 0x00, // variable length + BOOLEAN = 0x01, // 1 byte (0/1) + INTEGER = 0x02, // 4 byte + STRING = 0x03, // variable length + ENUM = 0x04, // 1 byte + BITMASK = 0x05, // 2 bytes +}; + +struct TuyaDatapoint { + uint8_t id; + TuyaDatapointType type; + union { + bool value_bool; + int value_int; + uint32_t value_uint; + uint8_t value_enum; + uint16_t value_bitmask; + }; +}; + +struct TuyaDatapointListener { + uint8_t datapoint_id; + std::function on_datapoint; +}; + +enum class TuyaCommandType : uint8_t { + HEARTBEAT = 0x00, + QUERY_PRODUCT = 0x01, + MCU_CONF = 0x02, + WIFI_STATE = 0x03, + WIFI_RESET = 0x04, + WIFI_SELECT = 0x05, + SET_DATAPOINT = 0x06, + STATE = 0x07, + QUERY_STATE = 0x08, +}; + +class Tuya : public Component, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + void register_listener(uint8_t datapoint_id, const std::function &func); + void set_datapoint_value(TuyaDatapoint datapoint); + + protected: + void handle_char_(uint8_t c); + void handle_datapoint_(const uint8_t *buffer, size_t len); + bool validate_message_(); + + void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); + void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); + void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + + int gpio_status_ = -1; + int gpio_reset_ = -1; + std::vector listeners_; + std::vector datapoints_; + std::vector rx_message_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c65ca919ba..6d6aa80b66 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -314,4 +314,20 @@ std::array decode_uint16(uint16_t value) { return {msb, lsb}; } +std::string hexencode(const uint8_t *data, uint32_t len) { + char buf[20]; + std::string res; + for (size_t i = 0; i < len; i++) { + if (i + 1 != len) { + sprintf(buf, "%02X.", data[i]); + } else { + sprintf(buf, "%02X ", data[i]); + } + res += buf; + } + sprintf(buf, "(%u)", len); + res += buf; + return res; +} + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5670d136a3..88f0d587e5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -156,6 +156,9 @@ enum ParseOnOffState { ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); +// Encode raw data to a human-readable string (for debugging) +std::string hexencode(const uint8_t *data, uint32_t len); + // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template struct seq {}; // NOLINT template struct gens : gens {}; // NOLINT From 352bdd9fb5b2cb2489ce85728118a65b417c6dc8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 21:50:39 +0200 Subject: [PATCH 010/412] Tuya Set gamma correction and transition length defaults See also https://github.com/esphome/esphome-docs/pull/353/files#r336751499 --- esphome/components/tuya/light/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index ec588d6cc9..605bdae32e 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -1,7 +1,8 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE +from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ + CONF_DEFAULT_TRANSITION_LENGTH from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] @@ -18,6 +19,11 @@ CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, + + # Change the default gamma_correct and default transition length settings. + # The Tuya MCU handles transitions and gamma correction on its own. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds, }).extend(cv.COMPONENT_SCHEMA) From 4fa11dfa68018fa7f72db71475a012f858cad760 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 21:59:55 +0200 Subject: [PATCH 011/412] Lint --- esphome/components/shtcx/shtcx.cpp | 2 +- tests/test1.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index d52bdb1257..b8daceb1af 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -12,7 +12,7 @@ static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; -inline const char* to_string(SHTCXType type) { +inline const char *to_string(SHTCXType type) { switch (type) { case SHTCX_TYPE_SHTC3: return "SHTC3"; diff --git a/tests/test1.yaml b/tests/test1.yaml index 2cdbbe8cff..5caa03cf03 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -544,12 +544,12 @@ sensor: address: 0x58 update_interval: 5s - platform: shtcx - temperature: - name: "Living Room Temperature 10" - humidity: - name: "Living Room Humidity 10" - address: 0x70 - update_interval: 15s + temperature: + name: "Living Room Temperature 10" + humidity: + name: "Living Room Humidity 10" + address: 0x70 + update_interval: 15s - platform: template name: "Template Sensor" id: template_sensor From b59cf6572b915f4b8da6240ff8554e49a18b9972 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Oct 2019 22:31:32 +0200 Subject: [PATCH 012/412] Add lint check for integer constants (#775) --- esphome/components/sim800l/sim800l.h | 4 ++-- esphome/core/preferences.cpp | 10 +++++----- script/ci-custom.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 17cd0111fe..0a3f4b463b 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -4,11 +4,11 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" -#define SIM800L_READ_BUFFER_LENGTH 255 - namespace esphome { namespace sim800l { +const uint8_t SIM800L_READ_BUFFER_LENGTH = 255; + enum State { STATE_IDLE = 0, STATE_INIT, diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp index f62a764b7e..2329ed34f5 100644 --- a/esphome/core/preferences.cpp +++ b/esphome/core/preferences.cpp @@ -54,15 +54,15 @@ bool ESPPreferenceObject::save_() { #ifdef ARDUINO_ARCH_ESP8266 -#define ESP_RTC_USER_MEM_START 0x60001200 +static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -#define ESP_RTC_USER_MEM_SIZE_WORDS 128 -#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4 +static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; +static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; #ifdef USE_ESP8266_PREFERENCES_FLASH -#define ESP8266_FLASH_STORAGE_SIZE 128 +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else -#define ESP8266_FLASH_STORAGE_SIZE 64 +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { diff --git a/script/ci-custom.py b/script/ci-custom.py index fff24df0dc..07641a17e9 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -5,6 +5,7 @@ import codecs import collections import fnmatch import os.path +import re import subprocess import sys @@ -143,6 +144,19 @@ def lint_end_newline(fname, content): return None +@lint_content_check(include=['*.cpp', '*.h', '*.tcc'], + exclude=['esphome/core/log.h']) +def lint_no_defines(fname, content): + errors = [] + for match in re.finditer(r'#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)', content, re.MULTILINE): + errors.append( + "#define macros for integer constants are not allowed, please use " + "`static const uint8_t {} = {};` style instead (replace uint8_t with the appropriate " + "datatype). See also Google styleguide.".format(match.group(1), match.group(2)) + ) + return errors + + def relative_cpp_search_text(fname, content): parts = fname.split('/') integration = parts[2] From 6c8d0f18525d0f440134ac6a59367b3db7ed434c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 15:57:59 +0200 Subject: [PATCH 013/412] Change message --- esphome/core/application.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index a2f94235f5..fbd3c56e54 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -73,9 +73,8 @@ void Application::loop() { const uint32_t end = millis(); if (end - start > 200) { - ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.1f s).", (end - start) / 1e3f); + ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f); ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); - ESP_LOGV(TAG, "This will become a warning soon."); } const uint32_t now = millis(); From 16f42a3d03bfb14559f5cbd58611a7c70cc1b3db Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 16:15:30 +0200 Subject: [PATCH 014/412] Add script.wait action (#778) Fixes https://github.com/esphome/feature-requests/issues/416, fixes https://github.com/esphome/issues/issues/572 --- esphome/components/script/__init__.py | 9 ++++++ esphome/components/script/script.h | 42 +++++++++++++++++++++++++++ tests/test3.yaml | 2 ++ 3 files changed, 53 insertions(+) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index e1983689a6..9590679f83 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -8,6 +8,7 @@ script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) CONFIG_SCHEMA = automation.validate_automation({ @@ -42,6 +43,14 @@ def script_stop_action_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action('script.wait', ScriptWaitAction, maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(Script) +})) +def script_wait_action_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(Script) })) diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index f937b9d637..3b97327da8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -49,5 +49,47 @@ template class IsRunningCondition : public Condition { Script *parent_; }; +template class ScriptWaitAction : public Action, public Component { + public: + ScriptWaitAction(Script *script) : script_(script) {} + + void play(Ts... x) { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + // Check if we can continue immediately. + if (!this->script_->is_running()) { + this->triggered_ = false; + this->play_next(x...); + return; + } + this->var_ = std::make_tuple(x...); + this->triggered_ = true; + this->loop(); + } + + void stop() override { this->triggered_ = false; } + + void loop() override { + if (!this->triggered_) + return; + + if (this->script_->is_running()) + return; + + this->triggered_ = false; + this->play_next_tuple(this->var_); + } + + float get_setup_priority() const override { return setup_priority::DATA; } + + bool is_running() override { return this->triggered_ || this->is_running_next(); } + + protected: + Script *script_; + bool triggered_{false}; + std::tuple var_{}; +}; + } // namespace script } // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index df4afe5ad9..f3e530f07a 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -383,6 +383,8 @@ text_sensor: - lambda: !lambda |- ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - script.execute: my_script + - script.wait: my_script + - script.stop: my_script - homeassistant.service: service: notify.html5 data: From 9a152e588e3e9a57627bee0bdb687795c06bc6f2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 17:56:57 +0200 Subject: [PATCH 015/412] Vl53l0x (#644) * VL530LX * VL53L0X * Updates * License * Lint --- MANIFEST.in | 1 + esphome/components/i2c/i2c.cpp | 25 ++ esphome/components/i2c/i2c.h | 20 ++ esphome/components/vl53l0x/LICENSE.txt | 80 ++++++ esphome/components/vl53l0x/__init__.py | 0 esphome/components/vl53l0x/sensor.py | 24 ++ esphome/components/vl53l0x/vl53l0x_sensor.cpp | 249 +++++++++++++++++ esphome/components/vl53l0x/vl53l0x_sensor.h | 257 ++++++++++++++++++ 8 files changed, 656 insertions(+) create mode 100644 esphome/components/vl53l0x/LICENSE.txt create mode 100644 esphome/components/vl53l0x/__init__.py create mode 100644 esphome/components/vl53l0x/sensor.py create mode 100644 esphome/components/vl53l0x/vl53l0x_sensor.cpp create mode 100644 esphome/components/vl53l0x/vl53l0x_sensor.h diff --git a/MANIFEST.in b/MANIFEST.in index e5c7b8f748..cdea2df2a6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include README.md include esphome/dashboard/templates/*.html recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc +recursive-include esphome LICENSE.txt diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 840944748c..562bd26771 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -208,5 +208,30 @@ void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } uint8_t next_i2c_bus_num_ = 0; #endif +I2CRegister &I2CRegister::operator=(uint8_t value) { + this->parent_->write_byte(this->register_, value); + return *this; +} + +I2CRegister &I2CRegister::operator&=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() & value); + return *this; +} + +I2CRegister &I2CRegister::operator|=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() | value); + return *this; +} + +uint8_t I2CRegister::get() { + uint8_t value = 0x00; + this->parent_->read_byte(this->register_, &value); + return value; +} +I2CRegister &I2CRegister::operator=(const std::vector &value) { + this->parent_->write_bytes(this->register_, value); + return *this; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 67cd0373d3..c4ed40e268 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -134,6 +134,24 @@ class I2CComponent : public Component { extern uint8_t next_i2c_bus_num_; #endif +class I2CDevice; + +class I2CRegister { + public: + I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} + + I2CRegister &operator=(uint8_t value); + I2CRegister &operator=(const std::vector &value); + I2CRegister &operator&=(uint8_t value); + I2CRegister &operator|=(uint8_t value); + + uint8_t get(); + + protected: + I2CDevice *parent_; + uint8_t register_; +}; + /** All components doing communication on the I2C bus should subclass I2CDevice. * * This class stores 1. the address of the i2c device and has a helper function to allow @@ -153,6 +171,8 @@ class I2CDevice { /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); + I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + /** Read len amount of bytes from a register into data. Optionally with a conversion time after * writing the register value to the bus. * diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt new file mode 100644 index 0000000000..fe33583414 --- /dev/null +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -0,0 +1,80 @@ +Most of the code in this integration is based on the VL53L0x library +by Pololu (Pololu Corporation), which in turn is based on the VL53L0X +API from ST. The code has been adapted to work with ESPHome's i2c APIs. +Please see the top-level LICENSE.txt for information about ESPHome's license. +The licenses for Pololu's and ST's software are included below. +Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). + +================================================================= + +Copyright (c) 2017 Pololu Corporation. For more information, see + +https://www.pololu.com/ +https://forum.pololu.com/ + +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. + +================================================================= + +Most of the functionality of this library is based on the VL53L0X +API provided by ST (STSW-IMG005), and some of the explanatory +comments are quoted or paraphrased from the API source code, API +user manual (UM2039), and the VL53L0X datasheet. + +The following applies to source code reproduced or derived from +the API: + +----------------------------------------------------------------- + +Copyright © 2016, STMicroelectronics International N.V. All +rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. +* Neither the name of STMicroelectronics nor the +names of its contributors may be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND +NON-INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS ARE DISCLAIMED. +IN NO EVENT SHALL STMICROELECTRONICS INTERNATIONAL N.V. BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------- diff --git a/esphome/components/vl53l0x/__init__.py b/esphome/components/vl53l0x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py new file mode 100644 index 0000000000..6740d53e13 --- /dev/null +++ b/esphome/components/vl53l0x/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL + +DEPENDENCIES = ['i2c'] + +vl53l0x_ns = cg.esphome_ns.namespace('vl53l0x') +VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComponent, + i2c.I2CDevice) + +CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ + cv.GenerateID(): cv.declare_id(VL53L0XSensor), + cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( + min=0.0, max=512.0, min_included=False, max_included=False) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp new file mode 100644 index 0000000000..231bed99ac --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -0,0 +1,249 @@ +#include "vl53l0x_sensor.h" +#include "esphome/core/log.h" + +/* + * Most of the code in this integration is based on the VL53L0x library + * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X + * API from ST. + * + * For more information about licensing, please view the included LICENSE.txt file + * in the vl53l0x integration directory. + */ + +namespace esphome { +namespace vl53l0x { + +static const char *TAG = "vl53l0x"; + +void VL53L0XSensor::dump_config() { + LOG_SENSOR("", "VL53L0X", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} +void VL53L0XSensor::setup() { + reg(0x89) |= 0x01; + reg(0x88) = 0x00; + + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + stop_variable_ = reg(0x91).get(); + + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + reg(0x60) |= 0x12; + + auto rate_value = static_cast(signal_rate_limit_ * 128); + write_byte_16(0x44, rate_value); + + reg(0x01) = 0xFF; + + // getSpadInfo() + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) |= 0x04; + reg(0xFF) = 0x07; + reg(0x81) = 0x01; + reg(0x80) = 0x01; + reg(0x94) = 0x6B; + reg(0x83) = 0x00; + + while (reg(0x83).get() == 0x00) + yield(); + + reg(0x83) = 0x01; + uint8_t tmp = reg(0x92).get(); + uint8_t spad_count = tmp & 0x7F; + bool spad_type_is_aperture = tmp & 0x80; + + reg(0x81) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) &= ~0x04; + reg(0xFF) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + uint8_t ref_spad_map[6]; + this->read_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x4F) = 0x00; + reg(0x4E) = 0x2C; + reg(0xFF) = 0x00; + reg(0xB6) = 0xB4; + + uint8_t first_spad_to_enable = spad_type_is_aperture ? 12 : 0; + uint8_t spads_enabled = 0; + for (int i = 0; i < 48; i++) { + uint8_t &val = ref_spad_map[i / 8]; + uint8_t mask = 1 << (i % 8); + + if (i < first_spad_to_enable || spads_enabled == spad_count) + val &= ~mask; + else if (val & mask) + spads_enabled += 1; + } + + this->write_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x00; + reg(0x09) = 0x00; + reg(0x10) = 0x00; + reg(0x11) = 0x00; + reg(0x24) = 0x01; + reg(0x25) = 0xFF; + reg(0x75) = 0x00; + reg(0xFF) = 0x01; + reg(0x4E) = 0x2C; + reg(0x48) = 0x00; + reg(0x30) = 0x20; + reg(0xFF) = 0x00; + reg(0x30) = 0x09; + reg(0x54) = 0x00; + reg(0x31) = 0x04; + reg(0x32) = 0x03; + reg(0x40) = 0x83; + reg(0x46) = 0x25; + reg(0x60) = 0x00; + reg(0x27) = 0x00; + reg(0x50) = 0x06; + reg(0x51) = 0x00; + reg(0x52) = 0x96; + reg(0x56) = 0x08; + reg(0x57) = 0x30; + reg(0x61) = 0x00; + reg(0x62) = 0x00; + reg(0x64) = 0x00; + reg(0x65) = 0x00; + reg(0x66) = 0xA0; + reg(0xFF) = 0x01; + reg(0x22) = 0x32; + reg(0x47) = 0x14; + reg(0x49) = 0xFF; + reg(0x4A) = 0x00; + reg(0xFF) = 0x00; + reg(0x7A) = 0x0A; + reg(0x7B) = 0x00; + reg(0x78) = 0x21; + reg(0xFF) = 0x01; + reg(0x23) = 0x34; + reg(0x42) = 0x00; + reg(0x44) = 0xFF; + reg(0x45) = 0x26; + reg(0x46) = 0x05; + reg(0x40) = 0x40; + reg(0x0E) = 0x06; + reg(0x20) = 0x1A; + reg(0x43) = 0x40; + reg(0xFF) = 0x00; + reg(0x34) = 0x03; + reg(0x35) = 0x44; + reg(0xFF) = 0x01; + reg(0x31) = 0x04; + reg(0x4B) = 0x09; + reg(0x4C) = 0x05; + reg(0x4D) = 0x04; + reg(0xFF) = 0x00; + reg(0x44) = 0x00; + reg(0x45) = 0x20; + reg(0x47) = 0x08; + reg(0x48) = 0x28; + reg(0x67) = 0x00; + reg(0x70) = 0x04; + reg(0x71) = 0x01; + reg(0x72) = 0xFE; + reg(0x76) = 0x00; + reg(0x77) = 0x00; + reg(0xFF) = 0x01; + reg(0x0D) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x01; + reg(0x01) = 0xF8; + reg(0xFF) = 0x01; + reg(0x8E) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x0A) = 0x04; + reg(0x84) &= ~0x10; + reg(0x0B) = 0x01; + + measurement_timing_budget_us_ = get_measurement_timing_budget_(); + reg(0x01) = 0xE8; + set_measurement_timing_budget_(measurement_timing_budget_us_); + reg(0x01) = 0x01; + + if (!perform_single_ref_calibration_(0x40)) { + ESP_LOGW(TAG, "1st reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0x02; + if (!perform_single_ref_calibration_(0x00)) { + ESP_LOGW(TAG, "2nd reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0xE8; +} +void VL53L0XSensor::update() { + if (this->initiated_read_ || this->waiting_for_interrupt_) { + this->publish_state(NAN); + this->status_set_warning(); + } + + // initiate single shot measurement + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + + reg(0x00) = 0x00; + reg(0x91) = stop_variable_; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x00) = 0x01; + this->waiting_for_interrupt_ = false; + this->initiated_read_ = true; + // wait for timeout +} +void VL53L0XSensor::loop() { + if (this->initiated_read_) { + if (reg(0x00).get() & 0x01) { + // waiting + } else { + // done + // wait until reg(0x13) & 0x07 is set + this->initiated_read_ = false; + this->waiting_for_interrupt_ = true; + } + } + if (this->waiting_for_interrupt_) { + if (reg(0x13).get() & 0x07) { + uint16_t range_mm; + this->read_byte_16(0x14 + 10, &range_mm); + reg(0x0B) = 0x01; + this->waiting_for_interrupt_ = false; + + if (range_mm >= 8190) { + ESP_LOGW(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str()); + this->publish_state(NAN); + return; + } + + float range_m = range_mm / 1e3f; + ESP_LOGD(TAG, "'%s' - Got distance %.3f m", this->name_.c_str(), range_m); + this->publish_state(range_m); + } + } +} + +} // namespace vl53l0x +} // namespace esphome diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h new file mode 100644 index 0000000000..1825383cee --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -0,0 +1,257 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace vl53l0x { + +struct SequenceStepEnables { + bool tcc, msrc, dss, pre_range, final_range; +}; + +struct SequenceStepTimeouts { + uint16_t pre_range_vcsel_period_pclks, final_range_vcsel_period_pclks; + + uint16_t msrc_dss_tcc_mclks, pre_range_mclks, final_range_mclks; + uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us; +}; + +class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + + void loop() override; + + void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + + protected: + uint32_t get_measurement_timing_budget_() { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1910; + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + // "Start and end overhead times always present" + uint32_t budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) + budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + + if (enables.dss) + budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + else if (enables.msrc) + budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + + if (enables.pre_range) + budget_us += (timeouts.pre_range_us + pre_range_overhead); + + if (enables.final_range) + budget_us += (timeouts.final_range_us + final_range_overhead); + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + return budget_us; + } + + bool set_measurement_timing_budget_(uint32_t budget_us) { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1320; // note that this is different than the value in get_ + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + uint32_t min_timing_budget = 20000; + + if (budget_us < min_timing_budget) { + return false; + } + + uint32_t used_budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + } + + if (enables.dss) { + used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + } else if (enables.msrc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + } + + if (enables.pre_range) { + used_budget_us += (timeouts.pre_range_us + pre_range_overhead); + } + + if (enables.final_range) { + used_budget_us += final_range_overhead; + + // "Note that the final range timeout is determined by the timing + // budget and the sum of all other timeouts within the sequence. + // If there is no room for the final range timeout, then an error + // will be set. Otherwise the remaining time will be applied to + // the final range." + + if (used_budget_us > budget_us) { + // "Requested timeout too big." + return false; + } + + uint32_t final_range_timeout_us = budget_us - used_budget_us; + + // set_sequence_step_timeout() begin + // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE) + + // "For the final range timeout, the pre-range timeout + // must be added. To do this both final and pre-range + // timeouts must be expressed in macro periods MClks + // because they have different vcsel periods." + + uint16_t final_range_timeout_mclks = + timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks); + + if (enables.pre_range) { + final_range_timeout_mclks += timeouts.pre_range_mclks; + } + + write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks)); + + // set_sequence_step_timeout() end + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + } + return true; + } + + void get_sequence_step_enables_(SequenceStepEnables *enables) { + uint8_t sequence_config = reg(0x01).get(); + enables->tcc = (sequence_config >> 4) & 0x1; + enables->dss = (sequence_config >> 3) & 0x1; + enables->msrc = (sequence_config >> 2) & 0x1; + enables->pre_range = (sequence_config >> 6) & 0x1; + enables->final_range = (sequence_config >> 7) & 0x1; + } + + enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; + + void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) { + timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE); + + timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1; + timeouts->msrc_dss_tcc_us = + timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks); + + uint16_t value; + read_byte_16(0x51, &value); + timeouts->pre_range_mclks = decode_timeout_(value); + timeouts->pre_range_us = + timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks); + + timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE); + + read_byte_16(0x71, &value); + timeouts->final_range_mclks = decode_timeout_(value); + + if (enables->pre_range) { + timeouts->final_range_mclks -= timeouts->pre_range_mclks; + } + + timeouts->final_range_us = + timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks); + } + + uint8_t get_vcsel_pulse_period_(VcselPeriodType type) { + uint8_t vcsel; + if (type == VCSEL_PERIOD_PRE_RANGE) + vcsel = reg(0x50).get(); + else if (type == VCSEL_PERIOD_FINAL_RANGE) + vcsel = reg(0x70).get(); + else + return 255; + + return (vcsel + 1) << 1; + } + + uint32_t get_macro_period_(uint8_t vcsel_period_pclks) { + return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL; + } + + uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000; + } + uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns); + } + + uint16_t decode_timeout_(uint16_t reg_val) { + // format: "(LSByte * 2^MSByte) + 1" + uint8_t msb = (reg_val >> 8) & 0xFF; + uint8_t lsb = (reg_val >> 0) & 0xFF; + return (uint16_t(lsb) << msb) + 1; + } + uint16_t encode_timeout_(uint16_t timeout_mclks) { + // format: "(LSByte * 2^MSByte) + 1" + uint32_t ls_byte = 0; + uint16_t ms_byte = 0; + + if (timeout_mclks <= 0) + return 0; + + ls_byte = timeout_mclks - 1; + + while ((ls_byte & 0xFFFFFF00) > 0) { + ls_byte >>= 1; + ms_byte++; + } + + return (ms_byte << 8) | (ls_byte & 0xFF); + } + + bool perform_single_ref_calibration_(uint8_t vhv_init_byte) { + reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP + + uint32_t start = millis(); + while ((reg(0x13).get() & 0x07) == 0) { + if (millis() - start > 1000) + return false; + yield(); + } + + reg(0x0B) = 0x01; + reg(0x00) = 0x00; + + return true; + } + + float signal_rate_limit_; + uint32_t measurement_timing_budget_us_; + bool initiated_read_{false}; + bool waiting_for_interrupt_{false}; + uint8_t stop_variable_; +}; + +} // namespace vl53l0x +} // namespace esphome From f1e00f8c8e8ec86b99e82aa5d041d3753045f3ed Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 18:10:14 +0200 Subject: [PATCH 016/412] Add GPIO Switch interlock wait time (#777) * Add interlock wait time to gpio switch Fixes https://github.com/esphome/issues/issues/753 * Format * Fix --- esphome/components/gpio/switch/__init__.py | 3 +++ esphome/components/gpio/switch/gpio_switch.cpp | 18 +++++++++++++++++- esphome/components/gpio/switch/gpio_switch.h | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 7b383cb8a9..f75bc71009 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -15,12 +15,14 @@ RESTORE_MODES = { 'ALWAYS_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, } +CONF_INTERLOCK_WAIT_TIME = 'interlock_wait_time' CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(GPIOSwitch), cv.Required(CONF_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESTORE_MODE, default='RESTORE_DEFAULT_OFF'): cv.enum(RESTORE_MODES, upper=True, space='_'), cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional(CONF_INTERLOCK_WAIT_TIME, default='0ms'): cv.positive_time_period_milliseconds, }).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +42,4 @@ def to_code(config): lock = yield cg.get_variable(it) interlock.append(lock) cg.add(var.set_interlock(interlock)) + cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME])) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d22a74847e..d87e5a61e6 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -69,13 +69,29 @@ void GPIOSwitch::dump_config() { void GPIOSwitch::write_state(bool state) { if (state != this->inverted_) { // Turning ON, check interlocking + + bool found = false; for (auto *lock : this->interlock_) { if (lock == this) continue; - if (lock->state) + if (lock->state) { lock->turn_off(); + found = true; + } } + if (found && this->interlock_wait_time_ != 0) { + this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + // Don't write directly, call the function again + // (some other switch may have changed state while we were waiting) + this->write_state(state); + }); + return; + } + } else if (this->interlock_wait_time_ != 0) { + // If we are switched off during the interlock wait time, cancel any pending + // re-activations + this->cancel_timeout("interlock"); } this->pin_->digital_write(state); diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index ceace477b2..dc0dd9bc95 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -26,6 +26,7 @@ class GPIOSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; void set_interlock(const std::vector &interlock); + void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } protected: void write_state(bool state) override; @@ -33,6 +34,7 @@ class GPIOSwitch : public switch_::Switch, public Component { GPIOPin *pin_; GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF}; std::vector interlock_; + uint32_t interlock_wait_time_{0}; }; } // namespace gpio From e077ad56bd47b1b388f0d2fc5652a24a73e90510 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 19:24:20 +0200 Subject: [PATCH 017/412] Add PZEM004T/PZEMAC/PZEMDC Support (#587) * Add PZEM004T Support * Don't flush as much * Update pzem004t.cpp * Add generalized modbus * Add PZEMAC * Add PZEMDC * Fix file modes * Lint * Fix * Fix * Add check_uart_settings --- esphome/components/cse7766/cse7766.cpp | 1 + esphome/components/mhz19/mhz19.cpp | 1 + esphome/components/modbus/__init__.py | 43 ++++++++ esphome/components/modbus/modbus.cpp | 119 +++++++++++++++++++++++ esphome/components/modbus/modbus.h | 51 ++++++++++ esphome/components/pmsx003/pmsx003.cpp | 1 + esphome/components/pzem004t/__init__.py | 0 esphome/components/pzem004t/pzem004t.cpp | 103 ++++++++++++++++++++ esphome/components/pzem004t/pzem004t.h | 39 ++++++++ esphome/components/pzem004t/sensor.py | 37 +++++++ esphome/components/pzemac/__init__.py | 0 esphome/components/pzemac/pzemac.cpp | 62 ++++++++++++ esphome/components/pzemac/pzemac.h | 31 ++++++ esphome/components/pzemac/sensor.py | 47 +++++++++ esphome/components/pzemdc/__init__.py | 0 esphome/components/pzemdc/pzemdc.cpp | 52 ++++++++++ esphome/components/pzemdc/pzemdc.h | 31 ++++++ esphome/components/pzemdc/sensor.py | 36 +++++++ esphome/components/sds011/sds011.cpp | 1 + esphome/components/senseair/senseair.cpp | 1 + esphome/components/tuya/tuya.cpp | 1 + esphome/components/uart/__init__.py | 3 + esphome/components/uart/uart.cpp | 39 ++++++-- esphome/components/uart/uart.h | 19 +++- esphome/const.py | 2 + tests/test3.yaml | 25 +++++ 26 files changed, 738 insertions(+), 7 deletions(-) create mode 100644 esphome/components/modbus/__init__.py create mode 100644 esphome/components/modbus/modbus.cpp create mode 100644 esphome/components/modbus/modbus.h create mode 100644 esphome/components/pzem004t/__init__.py create mode 100644 esphome/components/pzem004t/pzem004t.cpp create mode 100644 esphome/components/pzem004t/pzem004t.h create mode 100644 esphome/components/pzem004t/sensor.py create mode 100644 esphome/components/pzemac/__init__.py create mode 100644 esphome/components/pzemac/pzemac.cpp create mode 100644 esphome/components/pzemac/pzemac.h create mode 100644 esphome/components/pzemac/sensor.py create mode 100644 esphome/components/pzemdc/__init__.py create mode 100644 esphome/components/pzemdc/pzemdc.cpp create mode 100644 esphome/components/pzemdc/pzemdc.h create mode 100644 esphome/components/pzemdc/sensor.py diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 358453a63a..6c014138fd 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -172,6 +172,7 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); + this->check_uart_settings(4800); } } // namespace cse7766 diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 36ccf70d84..8e28d04dea 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -94,6 +94,7 @@ void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + this->check_uart_settings(9600); if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py new file mode 100644 index 0000000000..88105b7baf --- /dev/null +++ b/esphome/components/modbus/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.core import coroutine + +DEPENDENCIES = ['uart'] + +modbus_ns = cg.esphome_ns.namespace('modbus') +Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice) +ModbusDevice = modbus_ns.class_('ModbusDevice') +MULTI_CONF = True + +CONF_MODBUS_ID = 'modbus_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Modbus), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + cg.add_global(modbus_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + yield uart.register_uart_device(var, config) + + +def modbus_device_schema(default_address): + schema = { + cv.GenerateID(CONF_MODBUS_ID): cv.use_id(Modbus), + } + if default_address is None: + schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t + else: + schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t + return cv.Schema(schema) + + +@coroutine +def register_modbus_device(var, config): + parent = yield cg.get_variable(config[CONF_MODBUS_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_address(config[CONF_ADDRESS])) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp new file mode 100644 index 0000000000..92505c6429 --- /dev/null +++ b/esphome/components/modbus/modbus.cpp @@ -0,0 +1,119 @@ +#include "modbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus { + +static const char *TAG = "modbus"; + +void Modbus::loop() { + const uint32_t now = millis(); + if (now - this->last_modbus_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_modbus_byte_ = now; + } + + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + if (this->parse_modbus_byte_(byte)) { + this->last_modbus_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } +} + +uint16_t crc16(uint8_t *data, uint8_t len) { + uint16_t crc = 0xFFFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool Modbus::parse_modbus_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: modbus address (match all) + if (at == 0) + return true; + uint8_t address = raw[0]; + + // Byte 1: Function (msb indicates error) + if (at == 1) + return (byte & 0x80) != 0x80; + + // Byte 2: Size (with modbus rtu function code 4/3) + // See also https://en.wikipedia.org/wiki/Modbus + if (at == 2) + return true; + + uint8_t data_len = raw[2]; + // Byte 3..3+data_len-1: Data + if (at < 3 + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == 3 + data_len) + return true; + // Byte 3+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, 3 + data_len); + uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } + + std::vector data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == address) { + device->on_modbus_data(data); + found = true; + } + } + if (!found) { + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + } + + // return false to reset buffer + return false; +} + +void Modbus::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus:"); + this->check_uart_settings(9600, 2); +} +float Modbus::get_setup_priority() const { + // After UART bus + return setup_priority::BUS - 1.0f; +} +void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { + uint8_t frame[8]; + frame[0] = address; + frame[1] = function; + frame[2] = start_address >> 8; + frame[3] = start_address >> 0; + frame[4] = register_count >> 8; + frame[5] = register_count >> 0; + auto crc = crc16(frame, 6); + frame[6] = crc >> 0; + frame[7] = crc >> 8; + + this->write_array(frame, 8); +} + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h new file mode 100644 index 0000000000..b75de147b1 --- /dev/null +++ b/esphome/components/modbus/modbus.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace modbus { + +class ModbusDevice; + +class Modbus : public uart::UARTDevice, public Component { + public: + Modbus() = default; + + void loop() override; + + void dump_config() override; + + void register_device(ModbusDevice *device) { this->devices_.push_back(device); } + + float get_setup_priority() const override; + + void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); + + protected: + bool parse_modbus_byte_(uint8_t byte); + + std::vector rx_buffer_; + uint32_t last_modbus_byte_{0}; + std::vector devices_; +}; + +class ModbusDevice { + public: + void set_parent(Modbus *parent) { parent_ = parent; } + void set_address(uint8_t address) { address_ = address; } + virtual void on_modbus_data(const std::vector &data) = 0; + + void send(uint8_t function, uint16_t start_address, uint16_t register_count) { + this->parent_->send(this->address_, function, start_address, register_count); + } + + protected: + friend Modbus; + + Modbus *parent_; + uint8_t address_; +}; + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 548099a495..489442c637 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -169,6 +169,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + this->check_uart_settings(9600); } } // namespace pmsx003 diff --git a/esphome/components/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp new file mode 100644 index 0000000000..3f07e74f9b --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -0,0 +1,103 @@ +#include "pzem004t.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzem004t { + +static const char *TAG = "pzem004t"; + +void PZEM004T::loop() { + const uint32_t now = millis(); + if (now - this->last_read_ > 500 && this->available()) { + while (this->available()) + this->read(); + this->last_read_ = now; + } + + // PZEM004T packet size is 7 byte + while (this->available() >= 7) { + auto resp = *this->read_array<7>(); + // packet format: + // 0: packet type + // 1-5: data + // 6: checksum (sum of other bytes) + // see https://github.com/olehs/PZEM004T + uint8_t sum = 0; + for (int i = 0; i < 6; i++) + sum += resp[i]; + + if (sum != resp[6]) { + ESP_LOGV(TAG, "PZEM004T invalid checksum! 0x%02X != 0x%02X", sum, resp[6]); + continue; + } + + switch (resp[0]) { + case 0xA4: { // Set Module Address Response + this->write_state_(READ_VOLTAGE); + break; + } + case 0xA0: { // Voltage Response + uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float voltage = int_voltage + (resp[3] / 10.0f); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + ESP_LOGD(TAG, "Got Voltage %.1f V", voltage); + this->write_state_(READ_CURRENT); + break; + } + case 0xA1: { // Current Response + uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float current = int_current + (resp[3] / 100.0f); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + ESP_LOGD(TAG, "Got Current %.2f A", current); + this->write_state_(READ_POWER); + break; + } + case 0xA2: { // Active Power Response + uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); + ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(DONE); + break; + } + + case 0xA3: // Energy Response + case 0xA5: // Set Power Alarm Response + case 0xB0: // Voltage Request + case 0xB1: // Current Request + case 0xB2: // Active Power Response + case 0xB3: // Energy Request + case 0xB4: // Set Module Address Request + case 0xB5: // Set Power Alarm Request + default: + break; + } + + this->last_read_ = now; + } +} +void PZEM004T::update() { this->write_state_(SET_ADDRESS); } +void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { + if (state == DONE) { + this->read_state_ = state; + return; + } + std::array data{}; + data[0] = state; + data[1] = 192; + data[2] = 168; + data[3] = 1; + data[4] = 1; + data[5] = 0; + data[6] = 0; + for (int i = 0; i < 6; i++) + data[6] += data[i]; + + this->write_array(data); + this->read_state_ = state; +} + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h new file mode 100644 index 0000000000..7969efd78c --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pzem004t { + +class PZEM004T : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void loop() override; + + void update() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + + enum PZEM004TReadState { + SET_ADDRESS = 0xB4, + READ_VOLTAGE = 0xB0, + READ_CURRENT = 0xB1, + READ_POWER = 0xB2, + DONE = 0x00, + } read_state_{DONE}; + + void write_state_(PZEM004TReadState state); + + uint32_t last_read_{0}; +}; + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py new file mode 100644 index 0000000000..6e3628c5ec --- /dev/null +++ b/esphome/components/pzem004t/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['uart'] + +pzem004t_ns = cg.esphome_ns.namespace('pzem004t') +PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEM004T), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp new file mode 100644 index 0000000000..1c3957dc09 --- /dev/null +++ b/esphome/components/pzemac/pzemac.cpp @@ -0,0 +1,62 @@ +#include "pzemac.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemac { + +static const char *TAG = "pzemac"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMAC::on_modbus_data(const std::vector &data) { + if (data.size() < 20) { + ESP_LOGW(TAG, "Invalid size for PZEM AC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 + // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 10.0f; // max 6553.5 V + + uint32_t raw_current = pzem_get_32bit(2); + float current = raw_current / 1000.0f; // max 4294967.295 A + + uint32_t raw_active_power = pzem_get_32bit(6); + float active_power = raw_active_power / 10.0f; // max 429496729.5 W + + uint16_t raw_frequency = pzem_get_16bit(14); + float frequency = raw_frequency / 10.0f; + + uint16_t raw_power_factor = pzem_get_16bit(16); + float power_factor = raw_power_factor / 100.0f; + + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, + power_factor); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(active_power); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); +} + +void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h new file mode 100644 index 0000000000..0ba742fafe --- /dev/null +++ b/esphome/components/pzemac/pzemac.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemac { + +class PZEMAC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py new file mode 100644 index 0000000000..35d8069767 --- /dev/null +++ b/esphome/components/pzemac/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemac_ns = cg.esphome_ns.namespace('pzemac') +PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMAC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_FREQUENCY in config: + conf = config[CONF_FREQUENCY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_frequency_sensor(sens)) + if CONF_POWER_FACTOR in config: + conf = config[CONF_POWER_FACTOR] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp new file mode 100644 index 0000000000..c85d35106d --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -0,0 +1,52 @@ +#include "pzemdc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemdc { + +static const char *TAG = "pzemdc"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMDC::on_modbus_data(const std::vector &data) { + if (data.size() < 16) { + ESP_LOGW(TAG, "Invalid size for PZEM DC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 = ModBus register + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index + // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29 + // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 100.0f; // max 655.35 V + + uint16_t raw_current = pzem_get_16bit(2); + float current = raw_current / 100.0f; // max 655.35 A + + uint32_t raw_power = pzem_get_32bit(4); + float power = raw_power / 10.0f; // max 429496729.5 W + + ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); +} + +void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h new file mode 100644 index 0000000000..e19203dc21 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemdc { + +class PZEMDC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py new file mode 100644 index 0000000000..8c6fd08868 --- /dev/null +++ b/esphome/components/pzemdc/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemdc_ns = cg.esphome_ns.namespace('pzemdc') +PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMDC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 6ca414c55d..1a5be0adc3 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -56,6 +56,7 @@ void SDS011Component::dump_config() { ESP_LOGCONFIG(TAG, " RX-only mode: %s", ONOFF(this->rx_mode_only_)); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + this->check_uart_settings(9600); } void SDS011Component::loop() { diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 96f456282f..8b41a441ad 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -73,6 +73,7 @@ bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t void SenseAirComponent::dump_config() { ESP_LOGCONFIG(TAG, "SenseAir:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); } } // namespace senseair diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index ae3e7eed43..4efcf08fe6 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -36,6 +36,7 @@ void Tuya::dump_config() { else ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); } + this->check_uart_settings(9600); } bool Tuya::validate_message_() { diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 110bd64c81..2511cf28b1 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -29,11 +29,13 @@ def validate_rx_pin(value): return value +CONF_STOP_BITS = 'stop_bits' CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) @@ -48,6 +50,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) # A schema to use for all UART devices, all UART integrations must extend this! diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 83ae81490e..fd27a8f897 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -25,7 +25,10 @@ void UARTComponent::setup() { } int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->hw_serial_->begin(this->baud_rate_, SERIAL_8N1, rx, tx); + uint32_t config = SERIAL_8N1; + if (this->stop_bits_ == 2) + config = SERIAL_8N2; + this->hw_serial_->begin(this->baud_rate_, config, rx, tx); } void UARTComponent::dump_config() { @@ -37,6 +40,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); } void UARTComponent::write_byte(uint8_t data) { @@ -102,21 +106,27 @@ void UARTComponent::setup() { // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. + uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; + if (this->stop_bits_ == 1) + mode |= UART_NB_STOP_BIT_1; + else + mode |= UART_NB_STOP_BIT_2; + SerialConfig config = static_cast(mode); if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_); } } @@ -129,6 +139,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { @@ -231,7 +242,7 @@ void UARTComponent::flush() { } } -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { this->bit_time_ = F_CPU / baud_rate; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -246,6 +257,7 @@ void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_ra this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); } + this->stop_bits_ = stop_bits; } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; @@ -262,6 +274,8 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg rec |= arg->read_bit_(&wait, start) << 7; // Stop bit arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; @@ -289,6 +303,8 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->write_bit_(data & (1 << 7), &wait, start); // Stop bit this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); enable_interrupts(); } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { @@ -344,5 +360,16 @@ int UARTComponent::peek() { return data; } +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { + if (this->parent_->baud_rate_ != baud_rate) { + ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, + this->parent_->baud_rate_); + } + if (this->parent_->stop_bits_ != stop_bits) { + ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, + this->parent_->stop_bits_); + } +} + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 3b347c1ff7..0e92fed0dc 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -10,7 +10,7 @@ namespace uart { #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits); uint8_t read_byte(); uint8_t peek_byte(); @@ -33,6 +33,7 @@ class ESP8266SoftwareSerial { size_t rx_buffer_size_{512}; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; + uint8_t stop_bits_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; }; @@ -72,9 +73,11 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } protected: bool check_read_timeout_(size_t len = 1); + friend class UARTDevice; HardwareSerial *hw_serial_{nullptr}; #ifdef ARDUINO_ARCH_ESP8266 @@ -83,6 +86,7 @@ class UARTComponent : public Component, public Stream { optional tx_pin_; optional rx_pin_; uint32_t baud_rate_; + uint8_t stop_bits_; }; #ifdef ARDUINO_ARCH_ESP32 @@ -100,6 +104,9 @@ class UARTDevice : public Stream { void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); } void write_array(const std::vector &data) { this->parent_->write_array(data); } + template void write_array(const std::array &data) { + this->parent_->write_array(data.data(), data.size()); + } void write_str(const char *str) { this->parent_->write_str(str); } @@ -107,6 +114,13 @@ class UARTDevice : public Stream { bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); } bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); } + template optional> read_array() { // NOLINT + std::array res; + if (!this->read_array(res.data(), N)) { + return {}; + } + return res; + } int available() override { return this->parent_->available(); } @@ -116,6 +130,9 @@ class UARTDevice : public Stream { int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } + /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1); + protected: UARTComponent *parent_{nullptr}; }; diff --git a/esphome/const.py b/esphome/const.py index 5d05c91373..524031da8c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -323,6 +323,7 @@ CONF_PM_2_5 = 'pm_2_5' CONF_PORT = 'port' CONF_POSITION = 'position' CONF_POWER = 'power' +CONF_POWER_FACTOR = 'power_factor' CONF_POWER_ON_VALUE = 'power_on_value' CONF_POWER_SAVE_MODE = 'power_save_mode' CONF_POWER_SUPPLY = 'power_supply' @@ -479,6 +480,7 @@ ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' +ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' ICON_FLOWER = 'mdi:flower' diff --git a/tests/test3.yaml b/tests/test3.yaml index f3e530f07a..d1596c8f41 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -291,6 +291,31 @@ sensor: name: ADE7953 Active Power A active_power_b: name: ADE7953 Active Power B + - platform: pzem004t + voltage: + name: "PZEM00T Voltage" + current: + name: "PZEM004T Current" + power: + name: "PZEM004T Power" + - platform: pzemac + voltage: + name: "PZEMAC Voltage" + current: + name: "PZEMAC Current" + power: + name: "PZEMAC Power" + frequency: + name: "PZEMAC Frequency" + power_factor: + name: "PZEMAC Power Factor" + - platform: pzemdc + voltage: + name: "PZEMDC Voltage" + current: + name: "PZEMDC Current" + power: + name: "PZEMDC Power" time: - platform: homeassistant From 1e22b1e95964e1992e0f19a2e25862dc8a154873 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 19:24:34 +0200 Subject: [PATCH 018/412] Update AsyncMQTTClient/ESPAsyncWebServer (#779) --- esphome/components/mqtt/__init__.py | 4 ++-- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 5a9bbee377..073bb3cede 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -154,8 +154,8 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) - # https://github.com/marvinroger/async-mqtt-client/blob/master/library.json - cg.add_library('AsyncMqttClient-esphome', '0.8.2') + # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json + cg.add_library('AsyncMqttClient-esphome', '0.8.3') cg.add_define('USE_MQTT') cg.add_global(mqtt_ns.using) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 53463a9927..54102b931f 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -22,4 +22,4 @@ def to_code(config): if CORE.is_esp32: cg.add_library('FS', None) # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library('ESPAsyncWebServer-esphome', '1.2.4') + cg.add_library('ESPAsyncWebServer-esphome', '1.2.5') diff --git a/platformio.ini b/platformio.ini index a3bb27a897..19b850c546 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,10 +10,10 @@ include_dir = include [common] lib_deps = - AsyncTCP@1.0.3 - AsyncMqttClient-esphome@0.8.2 + AsyncTCP@1.1.1 + AsyncMqttClient-esphome@0.8.3 ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.3 + ESPAsyncWebServer-esphome@1.2.5 FastLED@3.2.9 NeoPixelBus-esphome@2.5.2 ESPAsyncTCP-esphome@1.2.2 From d64a4ef2b4b4df47889034003100c68313795c19 Mon Sep 17 00:00:00 2001 From: amishv <41266094+amishv@users.noreply.github.com> Date: Mon, 21 Oct 2019 16:13:28 +0530 Subject: [PATCH 019/412] Implementation of LCD Clear (#781) * Implementation of LCD Clear * Implementation of LCD Clear * Implementation of LCD Clear * Implementation of LCD Clear --- esphome/components/lcd_base/lcd_display.cpp | 5 +++++ esphome/components/lcd_base/lcd_display.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 51541049b1..25ac143817 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -148,6 +148,11 @@ void LCDDisplay::printf(const char *format, ...) { if (ret > 0) this->print(0, 0, buffer); } +void LCDDisplay::clear() { + // clear display, also sets DDRAM address to 0 (home) + this->command_(LCD_DISPLAY_COMMAND_CLEAR_DISPLAY); + delay(2); +} #ifdef USE_TIME void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { char buffer[64]; diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 791f31ace3..ee150059c6 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -23,6 +23,8 @@ class LCDDisplay : public PollingComponent { float get_setup_priority() const override; void update() override; void display(); + //// Clear LCD display + void clear(); /// Print the given text at the specified column and row. void print(uint8_t column, uint8_t row, const char *str); From ae8700447ebd9d87b870af8de7cd2ebf2c475c93 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 15:46:39 +0200 Subject: [PATCH 020/412] Add sensor force_update option (#783) * Add sensor force_update option * Add test --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 9 +++++++++ esphome/components/api/api_pb2.h | 1 + esphome/components/mqtt/mqtt_sensor.cpp | 3 +++ esphome/components/sensor/__init__.py | 9 +++++---- esphome/components/sensor/sensor.h | 13 +++++++++++++ esphome/const.py | 1 + tests/test1.yaml | 1 + 8 files changed, 34 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e454bf1d31..175bd3858f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -406,6 +406,7 @@ message ListEntitiesSensorResponse { string icon = 5; string unit_of_measurement = 6; int32 accuracy_decimals = 7; + bool force_update = 8; } message SensorStateResponse { option (id) = 25; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c4fa89ef97..3f635d1cdb 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1359,6 +1359,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->accuracy_decimals = value.as_int32(); return true; } + case 8: { + this->force_update = value.as_bool(); + return true; + } default: return false; } @@ -1407,6 +1411,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_string(6, this->unit_of_measurement); buffer.encode_int32(7, this->accuracy_decimals); + buffer.encode_bool(8, this->force_update); } void ListEntitiesSensorResponse::dump_to(std::string &out) const { char buffer[64]; @@ -1440,6 +1445,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { sprintf(buffer, "%d", this->accuracy_decimals); out.append(buffer); out.append("\n"); + + out.append(" force_update: "); + out.append(YESNO(this->force_update)); + out.append("\n"); out.append("}"); } bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9997a68477..2e685959bf 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -364,6 +364,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { std::string icon{}; // NOLINT std::string unit_of_measurement{}; // NOLINT int32_t accuracy_decimals{0}; // NOLINT + bool force_update{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a241cf6ed6..f87e7651b9 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -55,6 +55,9 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (!this->sensor_->get_icon().empty()) root["icon"] = this->sensor_->get_icon(); + if (this->sensor_->get_force_update()) + root["force_update"] = true; + config.command_topic = false; } bool MQTTSensorComponent::send_initial_state() { diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index a9b00b7c08..11a6e5e173 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -6,10 +6,9 @@ from esphome import automation from esphome.components import mqtt from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \ CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, CONF_ICON, CONF_ID, CONF_INTERNAL, \ - CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, \ - CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_TO, CONF_TRIGGER_ID, \ - CONF_UNIT_OF_MEASUREMENT, \ - CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID + CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, \ + CONF_TO, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID, \ + CONF_FORCE_UPDATE from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry @@ -87,6 +86,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement, cv.Optional(CONF_ICON): icon, cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All(cv.requires_component('mqtt'), cv.Any(None, cv.positive_time_period_milliseconds)), cv.Optional(CONF_FILTERS): validate_filters, @@ -258,6 +258,7 @@ def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) + cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) if CONF_FILTERS in config: filters = yield build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 1c7c854394..14ace91b35 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -18,6 +18,9 @@ namespace sensor { if (!obj->unique_id().empty()) { \ ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ } \ + if (obj->get_force_update()) { \ + ESP_LOGV(TAG, prefix " Force Update: YES"); \ + } \ } /** Base-class for all sensors. @@ -142,6 +145,15 @@ class Sensor : public Nameable { void internal_send_state_to_frontend(float state); + bool get_force_update() const { return force_update_; } + /** Set this sensor's force_update mode. + * + * If the sensor is in force_update mode, the frontend is required to save all + * state changes to the database when they are published, even if the state is the + * same as before. + */ + void set_force_update(bool force_update) { force_update_ = force_update; } + protected: /** Override this to set the Home Assistant unit of measurement for this sensor. * @@ -174,6 +186,7 @@ class Sensor : public Nameable { optional accuracy_decimals_; Filter *filter_list_{nullptr}; ///< Store all active filters. bool has_state_{false}; + bool force_update_{false}; }; class PollingSensorComponent : public PollingComponent, public Sensor { diff --git a/esphome/const.py b/esphome/const.py index 524031da8c..ac020d00e6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -156,6 +156,7 @@ CONF_FILTERS = 'filters' CONF_FILTER_OUT = 'filter_out' CONF_FLASH_LENGTH = 'flash_length' CONF_FOR = 'for' +CONF_FORCE_UPDATE = 'force_update' CONF_FORMALDEHYDE = 'formaldehyde' CONF_FORMAT = 'format' CONF_FREQUENCY = 'frequency' diff --git a/tests/test1.yaml b/tests/test1.yaml index 5caa03cf03..2fbef9d4f7 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -185,6 +185,7 @@ sensor: accuracy_decimals: 5 expire_after: 120s setup_priority: -100 + force_update: true filters: - offset: 2.0 - multiply: 1.2 From c0adaa8de848040d169d53b5e58a04e39c1ff026 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 22:55:16 +0200 Subject: [PATCH 021/412] Update docker base image to 2.0.1 (#785) --- .gitlab-ci.yml | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile.lint | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3808b7381..3db0b982ae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.0.0' + BASE_VERSION: '2.0.1' TZ: UTC stages: diff --git a/docker/Dockerfile b/docker/Dockerfile index 66d88655f4..11bbeeda2b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.0 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1 FROM ${BUILD_FROM} COPY . . diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 073ad90cb6..5d8893bdbe 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.0.0 +FROM esphome/esphome-base-amd64:2.0.1 RUN \ apt-get update \ From 1177b856a0fba427002b25c63a934af65ce4a954 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 22:55:27 +0200 Subject: [PATCH 022/412] Fix ledc can't find bit_depth (#786) Fixes https://github.com/esphome/issues/issues/759 --- esphome/components/ledc/ledc_output.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index b3652c84d6..2b1c181a62 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -42,8 +42,8 @@ float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) { } optional ledc_bit_depth_for_frequency(float frequency) { for (int i = 20; i >= 1; i--) { - const float min_frequency = ledc_min_frequency_for_bit_depth(frequency); - const float max_frequency = ledc_max_frequency_for_bit_depth(frequency); + const float min_frequency = ledc_min_frequency_for_bit_depth(i); + const float max_frequency = ledc_max_frequency_for_bit_depth(i); if (min_frequency <= frequency && frequency <= max_frequency) return i; } @@ -56,7 +56,7 @@ void LEDCOutput::apply_frequency(float frequency) { ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); this->status_set_warning(); } - this->bit_depth_ = *bit_depth_opt; + this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; ledcSetup(this->channel_, frequency, this->bit_depth_); // re-apply duty From 9fb60b8015a92a8cef79d0150a5a25cc9ea2a459 Mon Sep 17 00:00:00 2001 From: Nils Schulte <47043622+Schnilz@users.noreply.github.com> Date: Mon, 21 Oct 2019 23:05:50 +0200 Subject: [PATCH 023/412] web_server_base AUTO_LOAD includes ASYNC_TCP (#788) * web_server_base AUTO_LOAD includes ASYNC_TCP fix AUTO_LOAD of web_server_base to include ASYNC_TCP * Remove from dependencies Co-authored-by: Otto Winter --- esphome/components/web_server_base/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 54102b931f..923a594eb8 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -3,7 +3,8 @@ import esphome.codegen as cg from esphome.const import CONF_ID from esphome.core import coroutine_with_priority, CORE -DEPENDENCIES = ['network', 'async_tcp'] +DEPENDENCIES = ['network'] +AUTO_LOAD = ['async_tcp'] web_server_base_ns = cg.esphome_ns.namespace('web_server_base') WebServerBase = web_server_base_ns.class_('WebServerBase', cg.Component) From 6542be5588025ce11154b1a3169b527bfc7a3f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 22 Oct 2019 00:06:11 +0300 Subject: [PATCH 024/412] Wizard board name fixes (#787) * Sort board names in wizard * Fix board name in invalid board error message --- esphome/wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 34ff0ec6c7..f9dce7143a 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -220,7 +220,7 @@ def wizard(path): else: safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2'))) boards = list(ESP8266_BOARD_PINS.keys()) - safe_print("Options: {}".format(', '.join(boards))) + safe_print("Options: {}".format(', '.join(sorted(boards)))) while True: board = safe_input(color("bold_white", "(board): ")) @@ -228,7 +228,7 @@ def wizard(path): board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: - safe_print(color('red', "Sorry, I don't think the board \"{}\" exists.")) + safe_print(color('red', "Sorry, I don't think the board \"{}\" exists.".format(board))) safe_print() sleep(0.25) safe_print() From c18050bda08506b12399dd584140ad5940b7279c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 23:32:12 +0200 Subject: [PATCH 025/412] Add Python 2 deprecation notice (#784) * Add Python 2 deprecation notice * Update __main__.py --- esphome/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 403bf89401..bf2d68d834 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -509,6 +509,11 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 + if IS_PY2: + _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " + "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " + "or higher.") + if args.command in PRE_CONFIG_ACTIONS: try: return PRE_CONFIG_ACTIONS[args.command](args) From 3f0503c2962cfc6244744f6befdc2aee4462715d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 22 Oct 2019 22:56:34 +0200 Subject: [PATCH 026/412] Fix modbus CRC calculation (#789) * Fix modbus CRC calculation Fixes https://github.com/esphome/feature-requests/issues/49#issuecomment-545045776 * Fix --- esphome/components/modbus/modbus.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 92505c6429..74d0c40986 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -24,7 +24,7 @@ void Modbus::loop() { } } -uint16_t crc16(uint8_t *data, uint8_t len) { +uint16_t crc16(const uint8_t *data, uint8_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *data++; @@ -43,7 +43,7 @@ uint16_t crc16(uint8_t *data, uint8_t len) { bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); - uint8_t *raw = &this->rx_buffer_[0]; + const uint8_t *raw = &this->rx_buffer_[0]; // Byte 0: modbus address (match all) if (at == 0) @@ -69,7 +69,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { return true; // Byte 3+len+1: CRC_HI (over all bytes) uint16_t computed_crc = crc16(raw, 3 + data_len); - uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len]) << 8); + uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8); if (computed_crc != remote_crc) { ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); return false; From d63cd8b4cd4d77f51dcdff3b8fdec62f00ad73f8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 23 Oct 2019 14:43:27 +0200 Subject: [PATCH 027/412] Add additional custom lint checks (#790) --- .../components/binary_sensor_map/sensor.py | 3 +- esphome/components/coolix/climate.py | 8 +- esphome/components/custom/output/__init__.py | 3 +- esphome/components/dfplayer/__init__.py | 4 +- .../esp32_ble_beacon/esp32_ble_beacon.cpp | 2 +- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- esphome/components/esp32_camera/__init__.py | 6 +- .../components/esp32_touch/esp32_touch.cpp | 8 +- esphome/components/esp32_touch/esp32_touch.h | 1 + esphome/components/mpr121/mpr121.cpp | 2 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- esphome/components/ms5611/ms5611.cpp | 2 +- esphome/components/ota/ota_component.cpp | 2 +- esphome/components/pmsx003/sensor.py | 24 ++-- esphome/components/restart/restart_switch.cpp | 4 +- esphome/components/rotary_encoder/sensor.py | 4 +- esphome/components/scd30/sensor.py | 4 +- esphome/components/sgp30/sensor.py | 12 +- .../components/shutdown/shutdown_switch.cpp | 2 +- .../components/ssd1325_spi/ssd1325_spi.cpp | 2 +- .../sx1509/binary_sensor/__init__.py | 6 +- esphome/components/tcl112/climate.py | 8 +- .../components/template/output/__init__.py | 3 +- .../waveshare_epaper/waveshare_epaper.h | 8 +- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/yashima/climate.py | 7 +- esphome/const.py | 66 ++++----- script/ci-custom.py | 130 ++++++++++++++++-- 28 files changed, 209 insertions(+), 118 deletions(-) diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 8b8cd8fc4e..27f4654ded 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, binary_sensor from esphome.const import CONF_ID, CONF_CHANNELS, CONF_VALUE, CONF_TYPE, UNIT_EMPTY, \ - ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR + ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP DEPENDENCIES = ['binary_sensor'] @@ -11,7 +11,6 @@ binary_sensor_map_ns = cg.esphome_ns.namespace('binary_sensor_map') BinarySensorMap = binary_sensor_map_ns.class_('BinarySensorMap', cg.Component, sensor.Sensor) SensorMapType = binary_sensor_map_ns.enum('SensorMapType') -CONF_GROUP = 'group' SENSOR_MAP_TYPES = { CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, } diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index fe74798689..14868a7be0 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,18 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, remote_transmitter, remote_receiver, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components.remote_base import CONF_TRANSMITTER_ID, CONF_RECEIVER_ID +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT AUTO_LOAD = ['sensor', 'climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_RECEIVER_ID = 'receiver_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py index 6042863872..efe6f19dab 100644 --- a/esphome/components/custom/output/__init__.py +++ b/esphome/components/custom/output/__init__.py @@ -1,13 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY from .. import custom_ns CustomBinaryOutputConstructor = custom_ns.class_('CustomBinaryOutputConstructor') CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor') -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONFIG_SCHEMA = cv.typed_schema({ diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 78bcfa80f4..890c2bede4 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE from esphome.components import uart DEPENDENCIES = ['uart'] @@ -14,10 +14,8 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', au MULTI_CONF = True CONF_FOLDER = 'folder' -CONF_FILE = 'file' CONF_LOOP = 'loop' CONF_VOLUME = 'volume' -CONF_DEVICE = 'device' CONF_EQ_PRESET = 'eq_preset' CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 1f3bf01a86..a61975d02f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -50,7 +50,7 @@ void ESP32BLEBeacon::ble_core_task(void *params) { ble_setup(); while (true) { - delay(1000); + delay(1000); // NOLINT } } void ESP32BLEBeacon::ble_setup() { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c578acdaea..7a5bd733a2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -134,7 +134,7 @@ bool ESP32BLETracker::ble_setup() { } // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); + delay(200); // NOLINT return true; } diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index c69e0a5710..81980d9d38 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \ - ESP_PLATFORM_ESP32 + ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS ESP_PLATFORMS = [ESP_PLATFORM_ESP32] DEPENDENCIES = ['api'] @@ -35,23 +35,19 @@ FRAME_SIZES = { 'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, } -CONF_DATA_PINS = 'data_pins' CONF_VSYNC_PIN = 'vsync_pin' CONF_HREF_PIN = 'href_pin' CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin' CONF_EXTERNAL_CLOCK = 'external_clock' CONF_I2C_PINS = 'i2c_pins' -CONF_RESET_PIN = 'reset_pin' CONF_POWER_DOWN_PIN = 'power_down_pin' CONF_MAX_FRAMERATE = 'max_framerate' CONF_IDLE_FRAMERATE = 'idle_framerate' -CONF_RESOLUTION = 'resolution' CONF_JPEG_QUALITY = 'jpeg_quality' CONF_VERTICAL_FLIP = 'vertical_flip' CONF_HORIZONTAL_MIRROR = 'horizontal_mirror' CONF_CONTRAST = 'contrast' -CONF_BRIGHTNESS = 'brightness' CONF_SATURATION = 'saturation' CONF_TEST_PATTERN = 'test_pattern' diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 56bc407e84..ce0028159e 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -108,6 +108,8 @@ void ESP32TouchComponent::dump_config() { } void ESP32TouchComponent::loop() { + const uint32_t now = millis(); + bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { uint16_t value; if (this->iir_filter_enabled_()) { @@ -119,14 +121,14 @@ void ESP32TouchComponent::loop() { child->value_ = value; child->publish_state(value < child->get_threshold()); - if (this->setup_mode_) { + if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } } - if (this->setup_mode_) { + if (should_print) { // Avoid spamming logs - delay(250); + this->setup_mode_last_log_print_ = now; } } diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 7adee23971..45d459a2ff 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -50,6 +50,7 @@ class ESP32TouchComponent : public Component { touch_volt_atten_t voltage_attenuation_{}; std::vector children_; bool setup_mode_{false}; + uint32_t setup_mode_last_log_print_{}; uint32_t iir_filter_{0}; }; diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index a24a703306..2025bc5b3f 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -10,7 +10,7 @@ void MPR121Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MPR121..."); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); + delay(100); // NOLINT if (!this->write_byte(MPR121_ECR, 0x0)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index e07204d559..2eb1c52153 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -201,7 +201,7 @@ void MQTTClientComponent::check_connected() { this->status_clear_warning(); ESP_LOGI(TAG, "MQTT Connected!"); // MQTT Client needs some time to be fully set up. - delay(100); + delay(100); // NOLINT this->resubscribe_subscriptions_(); diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 33ed6b1899..39bce9f32c 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,7 +19,7 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); + delay(100); // NOLINT for (uint8_t offset = 0; offset < 6; offset++) { if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { this->mark_failed(); diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 6d2a0dd7c3..2041c688eb 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -266,7 +266,7 @@ void OTAComponent::handle_() { delay(10); ESP_LOGI(TAG, "OTA update finished!"); this->status_clear_warning(); - delay(100); + delay(100); // NOLINT App.safe_reboot(); error: diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index fa9a92d430..0cbaf1bf29 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -12,24 +12,24 @@ pmsx003_ns = cg.esphome_ns.namespace('pmsx003') PMSX003Component = pmsx003_ns.class_('PMSX003Component', uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_('PMSX003Sensor', sensor.Sensor) -CONF_PMSX003 = 'PMSX003' -CONF_PMS5003T = 'PMS5003T' -CONF_PMS5003ST = 'PMS5003ST' +TYPE_PMSX003 = 'PMSX003' +TYPE_PMS5003T = 'PMS5003T' +TYPE_PMS5003ST = 'PMS5003ST' PMSX003Type = pmsx003_ns.enum('PMSX003Type') PMSX003_TYPES = { - CONF_PMSX003: PMSX003Type.PMSX003_TYPE_X003, - CONF_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, - CONF_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, + TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, + TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, + TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_PM_2_5: [CONF_PMSX003, CONF_PMS5003T, CONF_PMS5003ST], - CONF_PM_10_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_TEMPERATURE: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_HUMIDITY: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_FORMALDEHYDE: [CONF_PMS5003ST], + CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_FORMALDEHYDE: [TYPE_PMS5003ST], } diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/restart_switch.cpp index cd0979b9d3..f66ebc616e 100644 --- a/esphome/components/restart/restart_switch.cpp +++ b/esphome/components/restart/restart_switch.cpp @@ -13,8 +13,8 @@ void RestartSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Restarting device..."); - // then execute - delay(100); // Let MQTT settle a bit + // Let MQTT settle a bit + delay(100); // NOLINT App.safe_reboot(); } } diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 742096cd5a..214ccbd056 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins, automation from esphome.components import sensor from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT, CONF_VALUE + ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -13,8 +13,6 @@ RESOLUTIONS = { 4: RotaryEncoderResolution.ROTARY_ENCODER_4_PULSES_PER_CYCLE, } -CONF_PIN_A = 'pin_a' -CONF_PIN_B = 'pin_b' CONF_PIN_RESET = 'pin_reset' RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index aa863b904a..7a60725276 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,15 +3,13 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ - UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT + UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2 DEPENDENCIES = ['i2c'] scd30_ns = cg.esphome_ns.namespace('scd30') SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) -CONF_CO2 = 'co2' - CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SCD30Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 0063c29bd3..6329b122fd 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -14,8 +14,8 @@ CONF_TVOC = 'tvoc' CONF_BASELINE = 'baseline' CONF_UPTIME = 'uptime' CONF_COMPENSATION = 'compensation' -CONF_COMPENSATION_HUMIDITY = 'humidity_source' -CONF_COMPENSATION_TEMPERATURE = 'temperature_source' +CONF_HUMIDITY_SOURCE = 'humidity_source' +CONF_TEMPERATURE_SOURCE = 'temperature_source' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SGP30Component), @@ -24,8 +24,8 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema({ - cv.Required(CONF_COMPENSATION_HUMIDITY): cv.use_id(sensor.Sensor), - cv.Required(CONF_COMPENSATION_TEMPERATURE): cv.use_id(sensor.Sensor) + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) }), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58)) @@ -48,7 +48,7 @@ def to_code(config): if CONF_COMPENSATION in config: compensation_config = config[CONF_COMPENSATION] - sens = yield cg.get_variable(compensation_config[CONF_COMPENSATION_HUMIDITY]) + sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) cg.add(var.set_humidity_sensor(sens)) - sens = yield cg.get_variable(compensation_config[CONF_COMPENSATION_TEMPERATURE]) + sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/shutdown_switch.cpp index d27bb8aadc..ce33cd187f 100644 --- a/esphome/components/shutdown/shutdown_switch.cpp +++ b/esphome/components/shutdown/shutdown_switch.cpp @@ -14,7 +14,7 @@ void ShutdownSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Shutting down..."); - delay(100); // Let MQTT settle a bit + delay(100); // NOLINT App.run_safe_shutdown_hooks(); #ifdef ARDUINO_ARCH_ESP8266 diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 1f547f8fd3..399700f1dd 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -14,7 +14,7 @@ void SPISSD1325::setup() { this->cs_->setup(); // OUTPUT this->init_reset_(); - delay(500); + delay(500); // NOLINT SSD1325::setup(); } void SPISSD1325::dump_config() { diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py index e780505edb..9a65524383 100644 --- a/esphome/components/sx1509/binary_sensor/__init__.py +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -5,7 +5,7 @@ from esphome.const import CONF_ID from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID CONF_ROW = 'row' -CONF_COLUMN = 'col' +CONF_COL = 'col' DEPENDENCIES = ['sx1509'] @@ -15,7 +15,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SX1509BinarySensor), cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), cv.Required(CONF_ROW): cv.int_range(min=0, max=4), - cv.Required(CONF_COLUMN): cv.int_range(min=0, max=4), + cv.Required(CONF_COL): cv.int_range(min=0, max=4), }) @@ -23,6 +23,6 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield binary_sensor.register_binary_sensor(var, config) hub = yield cg.get_variable(config[CONF_SX1509_ID]) - cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COLUMN])) + cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) cg.add(hub.register_keypad_binary_sensor(var)) diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index d21300d946..66af291a17 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,18 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, remote_transmitter, remote_receiver, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components.remote_base import CONF_TRANSMITTER_ID, CONF_RECEIVER_ID +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT AUTO_LOAD = ['sensor', 'climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_RECEIVER_ID = 'receiver_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), diff --git a/esphome/components/template/output/__init__.py b/esphome/components/template/output/__init__.py index 5cc9e089bd..cc85a9da68 100644 --- a/esphome/components/template/output/__init__.py +++ b/esphome/components/template/output/__init__.py @@ -2,13 +2,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import output -from esphome.const import CONF_ID, CONF_TYPE +from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY from .. import template_ns TemplateBinaryOutput = template_ns.class_('TemplateBinaryOutput', output.BinaryOutput) TemplateFloatOutput = template_ns.class_('TemplateFloatOutput', output.FloatOutput) -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONF_WRITE_ACTION = 'write_action' diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 13aebd4ec9..eff6b895a9 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -45,9 +45,9 @@ class WaveshareEPaper : public PollingComponent, void reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(200); + delay(200); // NOLINT this->reset_pin_->digital_write(true); - delay(200); + delay(200); // NOLINT } } @@ -144,7 +144,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { // COMMAND PANEL SETTING this->command(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER SETTING this->command(0x01); @@ -153,7 +153,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { this->data(0x00); this->data(0x00); this->data(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER OFF this->command(0x02); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0608f222f7..cb664d3cc3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -481,7 +481,7 @@ void WiFiComponent::retry_connect() { // If retry failed for more than 5 times, let's restart STA ESP_LOGW(TAG, "Restarting WiFi adapter..."); this->wifi_mode_(false, {}); - delay(100); + delay(100); // NOLINT this->num_retried_ = 0; } else { this->num_retried_++; diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py index 5d33670fb5..4c4b98d9e7 100644 --- a/esphome/components/yashima/climate.py +++ b/esphome/components/yashima/climate.py @@ -1,17 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, remote_transmitter, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components.remote_base import CONF_TRANSMITTER_ID +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT AUTO_LOAD = ['sensor'] yashima_ns = cg.esphome_ns.namespace('yashima') YashimaClimate = yashima_ns.class_('YashimaClimate', climate.Climate, cg.Component) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(YashimaClimate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), diff --git a/esphome/const.py b/esphome/const.py index ac020d00e6..ffef106945 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -63,8 +63,8 @@ CONF_BROKER = 'broker' CONF_BSSID = 'bssid' CONF_BUFFER_SIZE = 'buffer_size' CONF_BUILD_PATH = 'build_path' -CONF_BUSY_PIN = 'busy_pin' CONF_BUS_VOLTAGE = 'bus_voltage' +CONF_BUSY_PIN = 'busy_pin' CONF_CALIBRATE_LINEAR = 'calibrate_linear' CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' @@ -84,23 +84,23 @@ CONF_CO2 = 'co2' CONF_CODE = 'code' CONF_COLD_WHITE = 'cold_white' CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' -CONF_COLORS = 'colors' CONF_COLOR_CORRECT = 'color_correct' CONF_COLOR_TEMPERATURE = 'color_temperature' +CONF_COLORS = 'colors' CONF_COMMAND = 'command' CONF_COMMAND_TOPIC = 'command_topic' CONF_COMMENT = 'comment' CONF_COMMIT = 'commit' -CONF_COMPONENTS = 'components' CONF_COMPONENT_ID = 'component_id' +CONF_COMPONENTS = 'components' CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' -CONF_CSS_URL = 'css_url' CONF_CS_PIN = 'cs_pin' +CONF_CSS_URL = 'css_url' CONF_CURRENT = 'current' CONF_CURRENT_OPERATION = 'current_operation' CONF_CURRENT_RESISTOR = 'current_resistor' @@ -122,12 +122,12 @@ CONF_DELTA = 'delta' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DIMENSIONS = 'dimensions' -CONF_DIRECTION = 'direction' CONF_DIR_PIN = 'dir_pin' +CONF_DIRECTION = 'direction' CONF_DISCOVERY = 'discovery' -CONF_DISTANCE = 'distance' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' +CONF_DISTANCE = 'distance' CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' @@ -152,8 +152,8 @@ CONF_FAMILY = 'family' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' -CONF_FILTERS = 'filters' CONF_FILTER_OUT = 'filter_out' +CONF_FILTERS = 'filters' CONF_FLASH_LENGTH = 'flash_length' CONF_FOR = 'for' CONF_FORCE_UPDATE = 'force_update' @@ -172,8 +172,8 @@ CONF_GREEN = 'green' CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' -CONF_HEATER = 'heater' CONF_HEAT_ACTION = 'heat_action' +CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' @@ -207,8 +207,8 @@ CONF_INVERTED = 'inverted' CONF_IP_ADDRESS = 'ip_address' CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' -CONF_KEEPALIVE = 'keepalive' CONF_KEEP_ON_TIME = 'keep_on_time' +CONF_KEEPALIVE = 'keepalive' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' @@ -218,9 +218,9 @@ CONF_LIGHTNING_ENERGY = 'lightning_energy' CONF_LIGHTNING_THRESHOLD = 'lightning_threshold' CONF_LOADED_INTEGRATIONS = 'loaded_integrations' CONF_LOCAL = 'local' +CONF_LOG_TOPIC = 'log_topic' CONF_LOGGER = 'logger' CONF_LOGS = 'logs' -CONF_LOG_TOPIC = 'log_topic' CONF_LOW = 'low' CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' CONF_MAC_ADDRESS = 'mac_address' @@ -240,13 +240,13 @@ CONF_MAX_VOLTAGE = 'max_voltage' CONF_MEASUREMENT_DURATION = 'measurement_duration' CONF_MEDIUM = 'medium' CONF_METHOD = 'method' -CONF_MINUTE = 'minute' -CONF_MINUTES = 'minutes' CONF_MIN_LENGTH = 'min_length' CONF_MIN_LEVEL = 'min_level' CONF_MIN_POWER = 'min_power' CONF_MIN_TEMPERATURE = 'min_temperature' CONF_MIN_VALUE = 'min_value' +CONF_MINUTE = 'minute' +CONF_MINUTES = 'minutes' CONF_MISO_PIN = 'miso_pin' CONF_MODE = 'mode' CONF_MODEL = 'model' @@ -262,14 +262,13 @@ CONF_NBITS = 'nbits' CONF_NEC = 'nec' CONF_NETWORKS = 'networks' CONF_NOISE_LEVEL = 'noise_level' -CONF_NUMBER = 'number' CONF_NUM_ATTEMPTS = 'num_attempts' CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' +CONF_NUMBER = 'number' CONF_OFFSET = 'offset' CONF_ON = 'on' -CONF_ONE = 'one' CONF_ON_BOOT = 'on_boot' CONF_ON_CLICK = 'on_click' CONF_ON_DOUBLE_CLICK = 'on_double_click' @@ -288,6 +287,7 @@ CONF_ON_TURN_OFF = 'on_turn_off' CONF_ON_TURN_ON = 'on_turn_on' CONF_ON_VALUE = 'on_value' CONF_ON_VALUE_RANGE = 'on_value_range' +CONF_ONE = 'one' CONF_OPEN_ACTION = 'open_action' CONF_OPEN_DURATION = 'open_duration' CONF_OPEN_ENDSTOP = 'open_endstop' @@ -299,11 +299,11 @@ CONF_OSCILLATION_OUTPUT = 'oscillation_output' CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' CONF_OTA = 'ota' CONF_OUTPUT = 'output' -CONF_OUTPUTS = 'outputs' CONF_OUTPUT_ID = 'output_id' +CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' -CONF_PAGES = 'pages' CONF_PAGE_ID = 'page_id' +CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' CONF_PASSWORD = 'password' CONF_PAYLOAD = 'payload' @@ -311,15 +311,15 @@ CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_PHASE_BALANCER = 'phase_balancer' CONF_PIN = 'pin' -CONF_PINS = 'pins' CONF_PIN_A = 'pin_a' CONF_PIN_B = 'pin_b' CONF_PIN_C = 'pin_c' CONF_PIN_D = 'pin_d' +CONF_PINS = 'pins' CONF_PLATFORM = 'platform' CONF_PLATFORMIO_OPTIONS = 'platformio_options' -CONF_PM_10_0 = 'pm_10_0' CONF_PM_1_0 = 'pm_1_0' +CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' CONF_PORT = 'port' CONF_POSITION = 'position' @@ -353,8 +353,8 @@ CONF_RESTORE_MODE = 'restore_mode' CONF_RESTORE_STATE = 'restore_state' CONF_RESTORE_VALUE = 'restore_value' CONF_RETAIN = 'retain' -CONF_RGBW = 'rgbw' CONF_RGB_ORDER = 'rgb_order' +CONF_RGBW = 'rgbw' CONF_RISING_EDGE = 'rising_edge' CONF_ROTATION = 'rotation' CONF_RS_PIN = 'rs_pin' @@ -377,14 +377,14 @@ CONF_SEL_PIN = 'sel_pin' CONF_SEND_EVERY = 'send_every' CONF_SEND_FIRST_AT = 'send_first_at' CONF_SENSOR = 'sensor' -CONF_SENSORS = 'sensors' CONF_SENSOR_ID = 'sensor_id' +CONF_SENSORS = 'sensors' +CONF_SEQUENCE = 'sequence' CONF_SERVERS = 'servers' CONF_SERVICE = 'service' CONF_SERVICES = 'services' CONF_SETUP_MODE = 'setup_mode' CONF_SETUP_PRIORITY = 'setup_priority' -CONF_SEQUENCE = 'sequence' CONF_SHUNT_RESISTANCE = 'shunt_resistance' CONF_SHUNT_VOLTAGE = 'shunt_voltage' CONF_SHUTDOWN_MESSAGE = 'shutdown_message' @@ -408,6 +408,8 @@ CONF_STEP_PIN = 'step_pin' CONF_STOP = 'stop' CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' +CONF_SUPPORTS_COOL = 'supports_cool' +CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TAG = 'tag' @@ -425,10 +427,10 @@ CONF_TILT = 'tilt' CONF_TILT_ACTION = 'tilt_action' CONF_TILT_LAMBDA = 'tilt_lambda' CONF_TIME = 'time' +CONF_TIME_ID = 'time_id' CONF_TIMEOUT = 'timeout' CONF_TIMES = 'times' CONF_TIMEZONE = 'timezone' -CONF_TIME_ID = 'time_id' CONF_TIMING = 'timing' CONF_TO = 'to' CONF_TOLERANCE = 'tolerance' @@ -449,8 +451,8 @@ CONF_UNIQUE = 'unique' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UPDATE_INTERVAL = 'update_interval' CONF_UPDATE_ON_BOOT = 'update_on_boot' -CONF_USERNAME = 'username' CONF_USE_ADDRESS = 'use_address' +CONF_USERNAME = 'username' CONF_UUID = 'uuid' CONF_VALUE = 'value' CONF_VARIABLES = 'variables' @@ -470,8 +472,8 @@ CONF_WHITE = 'white' CONF_WIDTH = 'width' CONF_WIFI = 'wifi' CONF_WILL_MESSAGE = 'will_message' -CONF_WIND_SPEED = 'wind_speed' CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees' +CONF_WIND_SPEED = 'wind_speed' CONF_WINDOW_SIZE = 'window_size' CONF_ZERO = 'zero' @@ -479,8 +481,8 @@ ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' -ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' +ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' @@ -499,26 +501,26 @@ ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' +ICON_SIGN_DIRECTION = 'mdi:sign-direction' ICON_SIGNAL = 'mdi: signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' -ICON_SIGN_DIRECTION = 'mdi:sign-direction' -ICON_WEATHER_SUNSET = 'mdi:weather-sunset' -ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' -ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' ICON_THERMOMETER = 'mdi:thermometer' ICON_TIMER = 'mdi:timer' ICON_WATER_PERCENT = 'mdi:water-percent' +ICON_WEATHER_SUNSET = 'mdi:weather-sunset' +ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' +ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' ICON_WEATHER_WINDY = 'mdi:weather-windy' ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' UNIT_CELSIUS = u'°C' UNIT_DECIBEL = 'dB' -UNIT_DEGREES = u'°' UNIT_DEGREE_PER_SECOND = u'°/s' +UNIT_DEGREES = u'°' UNIT_EMPTY = '' -UNIT_HZ = 'hz' UNIT_HECTOPASCAL = 'hPa' +UNIT_HZ = 'hz' UNIT_KELVIN = 'K' UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' @@ -529,8 +531,8 @@ UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' UNIT_MICROTESLA = u'µT' UNIT_OHM = u'Ω' -UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PARTS_PER_BILLION = 'ppb' +UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' UNIT_PULSES_PER_MINUTE = 'pulses/min' UNIT_SECOND = 's' diff --git a/script/ci-custom.py b/script/ci-custom.py index 07641a17e9..3592faefab 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -40,6 +40,7 @@ ignore_types = ('.ico', '.woff', '.woff2', '') LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] +LINT_POST_CHECKS = [] def run_check(lint_obj, fname, *args): @@ -85,6 +86,31 @@ def lint_content_check(**kwargs): return decorator +def lint_post_check(func): + _add_check(LINT_POST_CHECKS, func) + return func + + +def lint_re_check(regex, **kwargs): + prog = re.compile(regex, re.MULTILINE) + decor = lint_content_check(**kwargs) + + def decorator(func): + def new_func(fname, content): + errors = [] + for match in prog.finditer(content): + if 'NOLINT' in match.group(0): + continue + lineno = content.count("\n", 0, match.start()) + 1 + err = func(fname, match) + if err is None: + continue + errors.append("{} See line {}.".format(err, lineno)) + return errors + return decor(new_func) + return decorator + + def lint_content_find_check(find, **kwargs): decor = lint_content_check(**kwargs) @@ -93,9 +119,12 @@ def lint_content_find_check(find, **kwargs): find_ = find if callable(find): find_ = find(fname, content) + errors = [] for line, col in find_all(content, find_): err = func(fname) - return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1) + errors.append("{err} See line {line}:{col}." + "".format(err=err, line=line+1, col=col+1)) + return errors return decor(new_func) return decorator @@ -144,16 +173,95 @@ def lint_end_newline(fname, content): return None -@lint_content_check(include=['*.cpp', '*.h', '*.tcc'], - exclude=['esphome/core/log.h']) -def lint_no_defines(fname, content): +CPP_RE_EOL = r'\s*?(?://.*?)?$' + + +def highlight(s): + return '\033[36m{}\033[0m'.format(s) + + +@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, + include=cpp_include, exclude=['esphome/core/log.h']) +def lint_no_defines(fname, match): + s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2))) + return ("#define macros for integer constants are not allowed, please use " + "{} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide.".format(s)) + + +@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include) +def lint_no_long_delays(fname, match): + duration_ms = int(match.group(1)) + if duration_ms < 50: + return None + return ( + "{} - long calls to delay() are not allowed in ESPHome because everything executes " + "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n" + "If there's no way to work around the delay() and it doesn't execute often, please add " + "a '// NOLINT' comment to the line." + "".format(highlight(match.group(0).strip())) + ) + + +@lint_content_check(include=['esphome/const.py']) +def lint_const_ordered(fname, content): + lines = content.splitlines() errors = [] - for match in re.finditer(r'#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)', content, re.MULTILINE): - errors.append( - "#define macros for integer constants are not allowed, please use " - "`static const uint8_t {} = {};` style instead (replace uint8_t with the appropriate " - "datatype). See also Google styleguide.".format(match.group(1), match.group(2)) - ) + for start in ['CONF_', 'ICON_', 'UNIT_']: + matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)] + ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' '))) + ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] + for (mi, ml), (oi, ol) in zip(matching, ordered): + if ml == ol: + continue + target = next(i for i, l in ordered if l == ml) + target_text = next(l for i, l in matching if target == i) + errors.append("Constant {} is not ordered, please make sure all constants are ordered. " + "See line {} (should go to line {}, {})" + "".format(highlight(ml), mi, target, target_text)) + return errors + + +@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py']) +def lint_conf_matches(fname, match): + const = match.group(1) + value = match.group(2) + const_norm = const.lower() + value_norm = value.replace('.', '_') + if const_norm == value_norm: + return None + return ("Constant {} does not match value {}! Please make sure the constant's name matches its " + "value!" + "".format(highlight('CONF_' + const), highlight(value))) + + +CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' +with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle: + constants_content = f_handle.read() +CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] + +CONSTANTS_USES = collections.defaultdict(list) + + +@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py']) +def lint_conf_from_const_py(fname, match): + name = match.group(1) + if name not in CONSTANTS: + CONSTANTS_USES[name].append(fname) + return None + return ("Constant {} has already been defined in const.py - please import the constant from " + "const.py directly.".format(highlight(name))) + + +@lint_post_check +def lint_constants_usage(): + errors = [] + for constant, uses in CONSTANTS_USES.items(): + if len(uses) < 4: + continue + errors.append("Constant {} is defined in {} files. Please move all definitions of the " + "constant to const.py (Uses: {})" + "".format(highlight(constant), len(uses), ', '.join(uses))) return errors @@ -255,6 +363,8 @@ for fname in files: continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) +run_checks(LINT_POST_CHECKS, 'POST') + for f, errs in sorted(errors.items()): print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) for err in errs: From 8ff742d9ab61f2c6cb04df09bc0e792d0ba478a6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 23 Oct 2019 14:43:41 +0200 Subject: [PATCH 028/412] Implement more dump_configs (#791) --- esphome/components/as3935_i2c/as3935_i2c.cpp | 4 ++++ esphome/components/as3935_i2c/as3935_i2c.h | 3 +++ esphome/components/bang_bang/bang_bang_climate.cpp | 8 ++++++++ esphome/components/bang_bang/bang_bang_climate.h | 1 + esphome/components/binary_sensor/binary_sensor.h | 4 ++-- esphome/components/captive_portal/captive_portal.cpp | 1 + esphome/components/captive_portal/captive_portal.h | 1 + esphome/components/climate/climate.h | 5 +++++ esphome/components/climate_ir/climate_ir.cpp | 10 ++++++++++ esphome/components/climate_ir/climate_ir.h | 1 + esphome/components/cover/cover.h | 6 +++--- esphome/components/dfplayer/dfplayer.cpp | 4 ++++ esphome/components/dfplayer/dfplayer.h | 1 + esphome/components/display/display_buffer.h | 4 ++-- .../components/esp32_ble_beacon/esp32_ble_beacon.cpp | 5 +++++ .../components/esp32_ble_beacon/esp32_ble_beacon.h | 1 + esphome/components/ntc/ntc.cpp | 6 +++--- esphome/components/ntc/ntc.h | 12 ++++++------ esphome/components/pzem004t/pzem004t.cpp | 6 ++++++ esphome/components/pzem004t/pzem004t.h | 2 ++ esphome/components/pzemac/pzemac.cpp | 9 +++++++++ esphome/components/pzemac/pzemac.h | 2 ++ esphome/components/pzemdc/pzemdc.cpp | 7 +++++++ esphome/components/pzemdc/pzemdc.h | 2 ++ esphome/components/sensor/sensor.h | 12 ++++++------ esphome/components/sim800l/sim800l.cpp | 4 ++++ esphome/components/sim800l/sim800l.h | 1 + esphome/components/switch/switch.h | 8 ++++---- esphome/components/text_sensor/text_sensor.h | 6 +++--- esphome/components/uptime/uptime_sensor.cpp | 1 + esphome/components/uptime/uptime_sensor.h | 1 + script/ci-custom.py | 1 + 32 files changed, 110 insertions(+), 29 deletions(-) diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp index 7792bd3e29..a522116815 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.cpp +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -31,6 +31,10 @@ uint8_t I2CAS3935Component::read_register(uint8_t reg) { } return value; } +void I2CAS3935Component::dump_config() { + AS3935Component::dump_config(); + LOG_I2C_DEVICE(this); +} } // namespace as3935_i2c } // namespace esphome diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h index 0f4167fc64..1d16397bdf 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.h +++ b/esphome/components/as3935_i2c/as3935_i2c.h @@ -10,6 +10,9 @@ namespace esphome { namespace as3935_i2c { class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice { + public: + void dump_config() override; + protected: void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override; uint8_t read_register(uint8_t reg) override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 17c5c0bc48..978abae52a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -145,6 +145,14 @@ Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void BangBangClimate::dump_config() { + LOG_CLIMATE("", "Bang Bang Climate", this); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); +} BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low, diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 0a79c1d7af..84bcd51f34 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -21,6 +21,7 @@ class BangBangClimate : public climate::Climate, public Component { public: BangBangClimate(); void setup() override; + void dump_config() override; void set_sensor(sensor::Sensor *sensor); Trigger<> *get_idle_trigger() const; diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 51c7a57ff6..577e87258c 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -10,9 +10,9 @@ namespace binary_sensor { #define LOG_BINARY_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ } \ } diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 1b533b570e..83f85d354c 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -166,6 +166,7 @@ float CaptivePortal::get_setup_priority() const { // Before WiFi return setup_priority::WIFI + 1.0f; } +void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); } CaptivePortal *global_captive_portal = nullptr; diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 4b1717d157..cbc3ba6ccc 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -18,6 +18,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { public: CaptivePortal(web_server_base::WebServerBase *base); void setup() override; + void dump_config() override; void loop() override { if (this->dns_server_ != nullptr) this->dns_server_->processNextRequest(); diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 70c6bef13b..4dd872bbed 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -9,6 +9,11 @@ namespace esphome { namespace climate { +#define LOG_CLIMATE(prefix, type, obj) \ + if (obj != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + } + class Climate; /** This class is used to encode all control actions on a climate device. diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 3d8b97e131..995e5a7ba5 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -1,8 +1,11 @@ #include "climate_ir.h" +#include "esphome/core/log.h" namespace esphome { namespace climate { +static const char *TAG = "climate_ir"; + climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); @@ -52,6 +55,13 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->transmit_state(); this->publish_state(); } +void ClimateIR::dump_config() { + LOG_CLIMATE("", "IR Climate", this); + ESP_LOGCONFIG(TAG, " Min. Temperature: %.1f°C", this->minimum_temperature_); + ESP_LOGCONFIG(TAG, " Max. Temperature: %.1f°C", this->maximum_temperature_); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 85f56c6b6b..51ced6b900 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -25,6 +25,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: } void setup() override; + void dump_config() override; void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { this->transmitter_ = transmitter; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 12011e1b4c..839cf9207e 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -13,13 +13,13 @@ const extern float COVER_CLOSED; #define LOG_COVER(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ auto traits_ = obj->get_traits(); \ if (traits_.get_is_assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ } \ } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 9589a907c4..5ce4998796 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -115,6 +115,10 @@ void DFPlayer::loop() { this->read_pos_++; } } +void DFPlayer::dump_config() { + ESP_LOGCONFIG(TAG, "DFPlayer:"); + this->check_uart_settings(9600); +} } // namespace dfplayer } // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 2cb9eea4eb..86efd62138 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -52,6 +52,7 @@ class DFPlayer : public uart::UARTDevice, public Component { void random() { this->send_cmd_(0x18); } bool is_playing() { return is_playing_; } + void dump_config() override; void add_on_finished_playback_callback(std::function callback) { this->on_finished_playback_callback_.add(std::move(callback)); diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 57b95eee29..b12fad8c8a 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -84,8 +84,8 @@ using display_writer_t = std::function; #define LOG_DISPLAY(prefix, type, obj) \ if (obj != nullptr) { \ ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, prefix " Rotations: %d °", obj->rotation_); \ - ESP_LOGCONFIG(TAG, prefix " Dimensions: %dpx x %dpx", obj->get_width(), obj->get_height()); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, obj->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, obj->get_width(), obj->get_height()); \ } class DisplayBuffer { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index a61975d02f..b7810bd056 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -31,6 +31,11 @@ static esp_ble_adv_params_t ble_adv_params = { static esp_ble_ibeacon_head_t ibeacon_common_head = { .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; +void ESP32BLEBeacon::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:"); + ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_); +} + void ESP32BLEBeacon::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); global_esp32_ble_beacon = this; diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 1c52b41d73..aba02830b3 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -34,6 +34,7 @@ class ESP32BLEBeacon : public Component { explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} void setup() override; + void dump_config() override; float get_setup_priority() const override; void set_major(uint16_t major) { this->major_ = major; } diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 1b5c5182c7..9446508b0b 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -19,9 +19,9 @@ void NTC::process_(float value) { return; } - float lr = logf(value); - float v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; - float temp = 1 / v - 273.15f; + double lr = log(double(value)); + double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; + auto temp = float(1.0 / v - 273.15); ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); this->publish_state(temp); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index 9d6b37412d..c8592e0fe8 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -9,9 +9,9 @@ namespace ntc { class NTC : public Component, public sensor::Sensor { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } - void set_a(float a) { a_ = a; } - void set_b(float b) { b_ = b; } - void set_c(float c) { c_ = c; } + void set_a(double a) { a_ = a; } + void set_b(double b) { b_ = b; } + void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,9 +20,9 @@ class NTC : public Component, public sensor::Sensor { void process_(float value); sensor::Sensor *sensor_; - float a_; - float b_; - float c_; + double a_; + double b_; + double c_; }; } // namespace ntc diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 3f07e74f9b..cbdc14f0d0 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -98,6 +98,12 @@ void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { this->write_array(data); this->read_state_ = state; } +void PZEM004T::dump_config() { + ESP_LOGCONFIG(TAG, "PZEM004T:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} } // namespace pzem004t } // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index 7969efd78c..f0208d415a 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -17,6 +17,8 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { void update() override; + void dump_config() override; + protected: sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index 1c3957dc09..f05ce15711 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -57,6 +57,15 @@ void PZEMAC::on_modbus_data(const std::vector &data) { } void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } +void PZEMAC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMAC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Frequency", this->frequency_sensor_); + LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); +} } // namespace pzemac } // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 0ba742fafe..d396b7cddf 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -19,6 +19,8 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { void on_modbus_data(const std::vector &data) override; + void dump_config() override; + protected: sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index c85d35106d..9bd58410c0 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -47,6 +47,13 @@ void PZEMDC::on_modbus_data(const std::vector &data) { } void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } +void PZEMDC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMDC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} } // namespace pzemdc } // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index e19203dc21..d838eb4167 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -19,6 +19,8 @@ class PZEMDC : public PollingComponent, public modbus::ModbusDevice { void on_modbus_data(const std::vector &data) override; + void dump_config() override; + protected: sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 14ace91b35..f23f022767 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -9,17 +9,17 @@ namespace sensor { #define LOG_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Unit of Measurement: '%s'", obj->get_unit_of_measurement().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Accuracy Decimals: %d", obj->get_accuracy_decimals()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, obj->get_unit_of_measurement().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, obj->get_accuracy_decimals()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ } \ if (obj->get_force_update()) { \ - ESP_LOGV(TAG, prefix " Force Update: YES"); \ + ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ } \ } diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index b2d58c5043..1390ef8b49 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -255,6 +255,10 @@ void Sim800LComponent::send_sms(std::string recipient, std::string message) { this->send_pending_ = true; this->update(); } +void Sim800LComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SIM800L:"); + ESP_LOGCONFIG(TAG, " RSSI: %d dB", this->rssi_); +} } // namespace sim800l } // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 0a3f4b463b..696eb8890f 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -37,6 +37,7 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { /// Retrieve the latest sensor values. This operation takes approximately 16ms. void update() override; void loop() override; + void dump_config() override; void add_on_sms_received_callback(std::function callback) { this->callback_.add(std::move(callback)); } diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index be4fc24c4a..cd6cec429f 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -9,15 +9,15 @@ namespace switch_ { #define LOG_SWITCH(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (obj->assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (obj->is_inverted()) { \ - ESP_LOGCONFIG(TAG, prefix " Inverted: YES"); \ + ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ } \ } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 719f0b0d62..85c2b644a0 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -8,12 +8,12 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ } \ } diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp index f047724768..5d117ab61d 100644 --- a/esphome/components/uptime/uptime_sensor.cpp +++ b/esphome/components/uptime/uptime_sensor.cpp @@ -27,6 +27,7 @@ void UptimeSensor::update() { } std::string UptimeSensor::unique_id() { return get_mac_address() + "-uptime"; } float UptimeSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); } } // namespace uptime } // namespace esphome diff --git a/esphome/components/uptime/uptime_sensor.h b/esphome/components/uptime/uptime_sensor.h index 184022503d..dab380d2d9 100644 --- a/esphome/components/uptime/uptime_sensor.h +++ b/esphome/components/uptime/uptime_sensor.h @@ -9,6 +9,7 @@ namespace uptime { class UptimeSensor : public sensor::Sensor, public PollingComponent { public: void update() override; + void dump_config() override; float get_setup_priority() const override; diff --git a/script/ci-custom.py b/script/ci-custom.py index 3592faefab..b43b15bcdd 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -325,6 +325,7 @@ def lint_pragma_once(fname, content): 'esphome/components/stepper/stepper.h', 'esphome/components/switch/switch.h', 'esphome/components/text_sensor/text_sensor.h', + 'esphome/components/climate/climate.h', 'esphome/core/component.h', 'esphome/core/esphal.h', 'esphome/core/log.h', From 4c49beb3c7691ee7167084e84ff4bc16938a0dbb Mon Sep 17 00:00:00 2001 From: Chris Debenham Date: Thu, 24 Oct 2019 23:13:50 +1100 Subject: [PATCH 029/412] Add missing include - fixes missing GPIOPin definition (#794) --- esphome/components/sm16716/sm16716.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h index fe534d93fe..85f78c8cf5 100644 --- a/esphome/components/sm16716/sm16716.h +++ b/esphome/components/sm16716/sm16716.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/esphal.h" #include "esphome/components/output/float_output.h" namespace esphome { From e4f055597cf4f9f9432e8495c6e4a26c581d5d26 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 24 Oct 2019 15:19:33 +0300 Subject: [PATCH 030/412] Logger on_message trigger (#729) * on_message * Lint fix * Lint fix (2) * Lint fix (<3) * Replace cg.int_ with int * Revert * Removed strdup Co-authored-by: Otto Winter --- esphome/codegen.py | 2 +- esphome/components/logger/__init__.py | 16 +++++++++++++++- esphome/components/logger/logger.h | 16 ++++++++++++++++ esphome/cpp_types.py | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 46d652a3cd..c30b43f952 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -19,7 +19,7 @@ from esphome.cpp_helpers import ( # noqa gpio_pin_expression, register_component, build_registry_entry, build_registry_list, extract_registry_entry_config, register_parented) from esphome.cpp_types import ( # noqa - global_ns, void, nullptr, float_, double, bool_, std_ns, std_string, + global_ns, void, nullptr, float_, double, bool_, int_, std_ns, std_string, std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN, esphome_ns, App, Nameable, Component, ComponentPtr, PollingComponent, Application, optional, arduino_json_ns, JsonObject, diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3e07334313..850f955f65 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -5,7 +5,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, \ - CONF_LEVEL, CONF_LOGS, CONF_TAG, CONF_TX_BUFFER_SIZE + CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority from esphome.py_compat import text_type @@ -70,6 +70,9 @@ def validate_local_no_higher_than_global(value): Logger = logger_ns.class_('Logger', cg.Component) +LoggerMessageTrigger = logger_ns.class_('LoggerMessageTrigger', + automation.Trigger.template(cg.int_, cg.const_char_ptr, + cg.const_char_ptr)) CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = 'esp8266_store_log_strings_in_flash' CONFIG_SCHEMA = cv.All(cv.Schema({ @@ -81,6 +84,10 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_LOGS, default={}): cv.Schema({ cv.string: is_log_level, }), + cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), + cv.Optional(CONF_LEVEL, default='WARN'): is_log_level, + }), cv.SplitDefault(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True): cv.All(cv.only_on_esp8266, cv.boolean), @@ -138,6 +145,13 @@ def to_code(config): # Register at end for safe mode yield cg.register_component(log, config) + for conf in config.get(CONF_ON_MESSAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], log, + LOG_LEVEL_SEVERITY.index(conf[CONF_LEVEL])) + yield automation.build_automation(trigger, [(cg.int_, 'level'), + (cg.const_char_ptr, 'tag'), + (cg.const_char_ptr, 'message')], conf) + def maybe_simple_message(schema): def validator(value): diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 18196b68d2..3d129e335b 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" @@ -114,6 +115,21 @@ class Logger : public Component { extern Logger *global_logger; +class LoggerMessageTrigger : public Trigger { + public: + explicit LoggerMessageTrigger(Logger *parent, int level) { + this->level_ = level; + parent->add_on_log_callback([this](int level, const char *tag, const char *message) { + if (level <= this->level_) { + this->trigger(level, tag, message); + } + }); + } + + protected: + int level_; +}; + } // namespace logger } // namespace esphome diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index d3e5b2d561..4a9dce332b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -6,6 +6,7 @@ nullptr = global_ns.namespace('nullptr') float_ = global_ns.namespace('float') double = global_ns.namespace('double') bool_ = global_ns.namespace('bool') +int_ = global_ns.namespace('int') std_ns = global_ns.namespace('std') std_string = std_ns.class_('string') std_vector = std_ns.class_('vector') From 59c5956f9330abbb8c578df6426e5037af15ef89 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 20:11:17 +0200 Subject: [PATCH 031/412] Fix MQTT not showing logs with Python 3 (#797) * Fix MQTT logging for Python 3 * Also fix captive portal PACKED --- esphome/components/captive_portal/captive_portal.h | 1 + esphome/mqtt.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index cbc3ba6ccc..3af47546cf 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -2,6 +2,7 @@ #include #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/components/web_server_base/web_server_base.h" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 0e00459944..e89a6d9578 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -15,6 +15,7 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME from esphome.core import CORE, EsphomeError from esphome.helpers import color +from esphome.py_compat import decode_text from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) def initialize(config, subscriptions, on_message, username, password, client_id): def on_connect(client, userdata, flags, return_code): + _LOGGER.info("Connected to MQTT broker!") for topic in subscriptions: client.subscribe(topic) @@ -94,7 +96,8 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def on_message(client, userdata, msg): time_ = datetime.now().time().strftime(u'[%H:%M:%S]') - message = time_ + msg.payload + payload = decode_text(msg.payload) + message = time_ + payload safe_print(message) return initialize(config, [topic], on_message, username, password, client_id) From d62ef3586042c1a6da278f36fe40976630b8f0ec Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 21:24:57 +0200 Subject: [PATCH 032/412] Fix scheduler first execution (#798) * Fix scheduler first execution not immediately * Also update sensor filters --- esphome/components/sensor/__init__.py | 2 +- esphome/core/scheduler.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 11a6e5e173..605f72a103 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -259,7 +259,7 @@ def setup_sensor_core_(var, config): if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) - if CONF_FILTERS in config: + if config.get(CONF_FILTERS): # must exist and not be empty filters = yield build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3df886cb5b..88054147f8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -57,7 +57,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, item->name = name; item->type = SchedulerItem::INTERVAL; item->interval = interval; - item->last_execution = now - offset; + item->last_execution = now - offset - interval; item->last_execution_major = this->millis_major_; if (item->last_execution > now) item->last_execution_major--; @@ -106,7 +106,7 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { // Not reached timeout yet, done for this call break; uint8_t major = item->last_execution_major; - if (item->last_execution + item->interval < item->last_execution) + if (item->last_execution > now) major++; if (major != this->millis_major_) break; From bb2582717fa93e896ad3d84f941e488c0427df9d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 21:53:42 +0200 Subject: [PATCH 033/412] Make file generation saving atomic (#792) * Make file generation saving atomic * Lint * Python 2 Compat * Fix * Handle file not found error --- esphome/config_helpers.py | 12 +--- esphome/helpers.py | 113 +++++++++++++++++++++++++++++--------- esphome/py_compat.py | 24 ++++---- esphome/storage_json.py | 15 ++--- esphome/wizard.py | 6 +- esphome/writer.py | 30 +++++----- 6 files changed, 123 insertions(+), 77 deletions(-) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index ddad36f8a8..0c508d2202 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,10 +1,10 @@ from __future__ import print_function -import codecs import json import os -from esphome.core import CORE, EsphomeError +from esphome.core import CORE +from esphome.helpers import read_file from esphome.py_compat import safe_input @@ -20,10 +20,4 @@ def read_config_file(path): assert data['type'] == 'file_response' return data['content'] - try: - with codecs.open(path, encoding='utf-8') as handle: - return handle.read() - except IOError as exc: - raise EsphomeError(u"Error accessing file {}: {}".format(path, exc)) - except UnicodeDecodeError as exc: - raise EsphomeError(u"Unable to read file {}: {}".format(path, exc)) + return read_file(path) diff --git a/esphome/helpers.py b/esphome/helpers.py index 30a06d842f..6fd1fa2ad7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,10 +1,11 @@ from __future__ import print_function import codecs + import logging import os -from esphome.py_compat import char_to_byte, text_type +from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text _LOGGER = logging.getLogger(__name__) @@ -79,15 +80,15 @@ def run_system_command(*args): def mkdir_p(path): - import errno - try: os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): + except OSError as err: + import errno + if err.errno == errno.EEXIST and os.path.isdir(path): pass else: - raise + from esphome.core import EsphomeError + raise EsphomeError(u"Error creating directories {}: {}".format(path, err)) def is_ip_address(host): @@ -151,17 +152,6 @@ def is_hassio(): return get_bool_env('ESPHOME_IS_HASSIO') -def copy_file_if_changed(src, dst): - src_text = read_file(src) - if os.path.isfile(dst): - dst_text = read_file(dst) - else: - dst_text = None - if src_text == dst_text: - return - write_file(dst, src_text) - - def walk_files(path): for root, _, files in os.walk(path): for name in files: @@ -172,28 +162,99 @@ def read_file(path): try: with codecs.open(path, 'r', encoding='utf-8') as f_handle: return f_handle.read() - except OSError: + except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Could not read file at {}".format(path)) + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + except UnicodeDecodeError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + + +def _write_file(path, text): + import tempfile + directory = os.path.dirname(path) + mkdir_p(directory) + + tmp_path = None + data = encode_text(text) + try: + with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: + tmp_path = f_handle.name + f_handle.write(data) + # Newer tempfile implementations create the file with mode 0o600 + os.chmod(tmp_path, 0o644) + if IS_PY2: + if os.path.exists(path): + os.remove(path) + os.rename(tmp_path, path) + else: + # If destination exists, will be overwritten + os.replace(tmp_path, path) + finally: + if tmp_path is not None and os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except OSError as err: + _LOGGER.error("Write file cleanup failed: %s", err) def write_file(path, text): try: - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w+', encoding='utf-8') as f_handle: - f_handle.write(text) + _write_file(path, text) except OSError: from esphome.core import EsphomeError raise EsphomeError(u"Could not write file at {}".format(path)) -def write_file_if_changed(text, dst): +def write_file_if_changed(path, text): src_content = None - if os.path.isfile(dst): - src_content = read_file(dst) + if os.path.isfile(path): + src_content = read_file(path) if src_content != text: - write_file(dst, text) + write_file(path, text) + + +def copy_file_if_changed(src, dst): + import shutil + if file_compare(src, dst): + return + mkdir_p(os.path.dirname(dst)) + try: + shutil.copy(src, dst) + except OSError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err)) def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) + + +def file_compare(path1, path2): + """Return True if the files path1 and path2 have the same contents.""" + import stat + + try: + stat1, stat2 = os.stat(path1), os.stat(path2) + except OSError: + # File doesn't exist or another error -> not equal + return False + + if stat.S_IFMT(stat1.st_mode) != stat.S_IFREG or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG: + # At least one of them is not a regular file (or does not exist) + return False + if stat1.st_size != stat2.st_size: + # Different sizes + return False + + bufsize = 8*1024 + # Read files in blocks until a mismatch is found + with open(path1, 'rb') as fh1, open(path2, 'rb') as fh2: + while True: + blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize) + if blob1 != blob2: + # Different content + return False + if not blob1: + # Reached end + return True diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 6833a55801..6cdaa5b047 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -1,5 +1,6 @@ import functools import sys +import codecs PYTHON_MAJOR = sys.version_info[0] IS_PY2 = PYTHON_MAJOR == 2 @@ -75,15 +76,14 @@ def indexbytes(buf, i): return ord(buf[i]) -if IS_PY2: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (str, str, str) -> unicode - if isinstance(data, unicode): - return data - return unicode(data, encoding=encoding, errors=errors) -else: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (bytes, str, str) -> str - if isinstance(data, str): - return data - return data.decode(encoding=encoding, errors=errors) +def decode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, text_type): + return data + return codecs.decode(data, encoding, errors) + + +def encode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, binary_type): + return data + + return codecs.encode(data, encoding, errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index cac7f9d2fa..0305b59ef5 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -7,7 +7,7 @@ import os from esphome import const from esphome.core import CORE -from esphome.helpers import mkdir_p +from esphome.helpers import mkdir_p, write_file_if_changed # pylint: disable=unused-import, wrong-import-order from esphome.core import CoreType # noqa @@ -89,8 +89,7 @@ class StorageJSON(object): def save(self, path): mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def from_esphome_core(esph, old): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON @@ -130,8 +129,7 @@ class StorageJSON(object): @staticmethod def _load_impl(path): # type: (str) -> Optional[StorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] name = storage.get('name') comment = storage.get('comment') @@ -195,15 +193,12 @@ class EsphomeStorageJSON(object): return json.dumps(self.as_dict(), indent=2) + u'\n' def save(self, path): # type: (str) -> None - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] cookie_secret = storage.get('cookie_secret') last_update_check = storage.get('last_update_check') diff --git a/esphome/wizard.py b/esphome/wizard.py index f9dce7143a..8cc759f934 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,6 +1,5 @@ from __future__ import print_function -import codecs import os import random import string @@ -9,7 +8,7 @@ import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import color, get_bool_env +from esphome.helpers import color, get_bool_env, write_file # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.py_compat import safe_input, text_type @@ -104,8 +103,7 @@ def wizard_write(path, **kwargs): kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32' platform = kwargs['platform'] - with codecs.open(path, 'w', 'utf-8') as f_handle: - f_handle.write(wizard_file(**kwargs)) + write_file(path, wizard_file(**kwargs)) storage = StorageJSON.from_wizard(name, name + '.local', platform, board) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) diff --git a/esphome/writer.py b/esphome/writer.py index 8fa239d608..4c2e03569f 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -8,7 +8,8 @@ from esphome.config import iter_components from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \ HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__ from esphome.core import CORE, EsphomeError -from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files +from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \ + copy_file_if_changed from esphome.storage_json import StorageJSON, storage_path _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ def migrate_src_version_0_to_1(): "auto-generated again.", main_cpp, main_cpp) _LOGGER.info("Migration: Added include section to %s", main_cpp) - write_file_if_changed(content, main_cpp) + write_file_if_changed(main_cpp, content) def migrate_src_version(old, new): @@ -251,7 +252,7 @@ def write_platformio_ini(content): content_format = INI_BASE_FORMAT full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + content full_file += INI_AUTO_GENERATE_END + content_format[1] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def write_platformio_project(): @@ -285,7 +286,6 @@ or use the custom_components folder. def copy_src_tree(): - import filecmp import shutil source_files = {} @@ -321,9 +321,7 @@ def copy_src_tree(): os.remove(path) else: src_path = source_files_copy.pop(target) - if not filecmp.cmp(path, src_path): - # Files are not same, copy - shutil.copy(src_path, path) + copy_file_if_changed(src_path, path) # Now copy new files for target, src_path in source_files_copy.items(): @@ -332,14 +330,14 @@ def copy_src_tree(): shutil.copy(src_path, dst_path) # Finally copy defines - write_file_if_changed(generate_defines_h(), - CORE.relative_src_path('esphome', 'core', 'defines.h')) - write_file_if_changed(ESPHOME_README_TXT, - CORE.relative_src_path('esphome', 'README.txt')) - write_file_if_changed(ESPHOME_H_FORMAT.format(include_s), - CORE.relative_src_path('esphome.h')) - write_file_if_changed(VERSION_H_FORMAT.format(__version__), - CORE.relative_src_path('esphome', 'core', 'version.h')) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'), + generate_defines_h()) + write_file_if_changed(CORE.relative_src_path('esphome', 'README.txt'), + ESPHOME_README_TXT) + write_file_if_changed(CORE.relative_src_path('esphome.h'), + ESPHOME_H_FORMAT.format(include_s)) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'version.h'), + VERSION_H_FORMAT.format(__version__)) def generate_defines_h(): @@ -365,7 +363,7 @@ def write_cpp(code_s): full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def clean_build(): From 91c9b1164721ee28b0aafc84557df5ad4be88d5b Mon Sep 17 00:00:00 2001 From: Pavel Golovin Date: Fri, 25 Oct 2019 12:32:31 +0300 Subject: [PATCH 034/412] Fujitsu General climate new component (#677) * new Fujitsu-General climate component * Refactor out climate_ir CC @glmnet Refactored out climate_ir python files too. Fixed invalid namespace name for climate_ir. * Add namespace lint check * Refactor Fujitsu Climate to climate_ir Co-authored-by: Otto Winter --- esphome/components/climate_ir/__init__.py | 41 ++++ esphome/components/climate_ir/climate_ir.cpp | 4 +- esphome/components/climate_ir/climate_ir.h | 7 +- esphome/components/coolix/climate.py | 33 +-- esphome/components/coolix/coolix.h | 4 +- .../components/fujitsu_general/__init__.py | 0 esphome/components/fujitsu_general/climate.py | 18 ++ .../fujitsu_general/fujitsu_general.cpp | 212 ++++++++++++++++++ .../fujitsu_general/fujitsu_general.h | 24 ++ esphome/components/tcl112/climate.py | 33 +-- esphome/components/tcl112/tcl112.h | 4 +- script/ci-custom.py | 13 ++ 12 files changed, 332 insertions(+), 61 deletions(-) create mode 100644 esphome/components/fujitsu_general/__init__.py create mode 100644 esphome/components/fujitsu_general/climate.py create mode 100644 esphome/components/fujitsu_general/fujitsu_general.cpp create mode 100644 esphome/components/fujitsu_general/fujitsu_general.h diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index e69de29bb2..1163705faa 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, remote_receiver, sensor, remote_base +from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID +from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR +from esphome.core import coroutine + +AUTO_LOAD = ['sensor', 'remote_base'] + +climate_ir_ns = cg.esphome_ns.namespace('climate_ir') +ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, + remote_base.RemoteReceiverListener) + +CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), +}).extend(cv.COMPONENT_SCHEMA) + +CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend({ + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), +}) + + +@coroutine +def register_climate_ir(var, config): + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + if CONF_SENSOR in config: + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) + + transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 995e5a7ba5..4b9a1c0baa 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" namespace esphome { -namespace climate { +namespace climate_ir { static const char *TAG = "climate_ir"; @@ -63,5 +63,5 @@ void ClimateIR::dump_config() { ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); } -} // namespace climate +} // namespace climate_ir } // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 51ced6b900..b4c036f3d6 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -6,7 +6,7 @@ #include "esphome/components/sensor/sensor.h" namespace esphome { -namespace climate { +namespace climate_ir { /* A base for climate which works by sending (and receiving) IR codes @@ -42,7 +42,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: climate::ClimateTraits traits() override; /// Transmit via IR the state of this climate controller. - virtual void transmit_state() {} + virtual void transmit_state() = 0; bool supports_cool_{true}; bool supports_heat_{true}; @@ -50,5 +50,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; }; -} // namespace climate + +} // namespace climate_ir } // namespace esphome diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 14868a7be0..81412bb586 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,37 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, remote_receiver, sensor -from esphome.components.remote_base import CONF_TRANSMITTER_ID, CONF_RECEIVER_ID -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor', 'climate_ir'] +AUTO_LOAD = ['climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') -CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) +CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - if CONF_RECEIVER_ID in config: - receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) - cg.add(receiver.register_listener(var)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 125d8ffd37..ed03a2fd1e 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -9,9 +9,9 @@ namespace coolix { const uint8_t COOLIX_TEMP_MIN = 17; // Celsius const uint8_t COOLIX_TEMP_MAX = 30; // Celsius -class CoolixClimate : public climate::ClimateIR { +class CoolixClimate : public climate_ir::ClimateIR { public: - CoolixClimate() : climate::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} + CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} protected: /// Transmit via IR the state of this climate controller. diff --git a/esphome/components/fujitsu_general/__init__.py b/esphome/components/fujitsu_general/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py new file mode 100644 index 0000000000..a6774c397a --- /dev/null +++ b/esphome/components/fujitsu_general/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +fujitsu_general_ns = cg.esphome_ns.namespace('fujitsu_general') +FujitsuGeneralClimate = fujitsu_general_ns.class_('FujitsuGeneralClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp new file mode 100644 index 0000000000..261d8be258 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -0,0 +1,212 @@ +#include "fujitsu_general.h" + +namespace esphome { +namespace fujitsu_general { + +static const char *TAG = "fujitsu_general.climate"; + +// Control packet +const uint16_t FUJITSU_GENERAL_STATE_LENGTH = 16; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE0 = 0x14; +const uint8_t FUJITSU_GENERAL_BASE_BYTE1 = 0x63; +const uint8_t FUJITSU_GENERAL_BASE_BYTE2 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE3 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE4 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE5 = 0xFE; +const uint8_t FUJITSU_GENERAL_BASE_BYTE6 = 0x09; +const uint8_t FUJITSU_GENERAL_BASE_BYTE7 = 0x30; + +// Temperature and POWER ON +const uint8_t FUJITSU_GENERAL_POWER_ON_MASK_BYTE8 = 0b00000001; +const uint8_t FUJITSU_GENERAL_BASE_BYTE8 = 0x40; + +// Mode +const uint8_t FUJITSU_GENERAL_MODE_AUTO_BYTE9 = 0x00; +const uint8_t FUJITSU_GENERAL_MODE_HEAT_BYTE9 = 0x04; +const uint8_t FUJITSU_GENERAL_MODE_COOL_BYTE9 = 0x01; +const uint8_t FUJITSU_GENERAL_MODE_DRY_BYTE9 = 0x02; +const uint8_t FUJITSU_GENERAL_MODE_FAN_BYTE9 = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_10C_BYTE9 = 0x0B; +const uint8_t FUJITSU_GENERAL_BASE_BYTE9 = 0x01; + +// Fan speed and swing +const uint8_t FUJITSU_GENERAL_FAN_AUTO_BYTE10 = 0x00; +const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01; +const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02; +const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03; +const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04; +const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000; +const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE12 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE13 = 0x00; + +// Outdoor Unit Low Noise +const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; +const uint8_t FUJITSU_GENERAL_BASE_BYTE14 = 0x20; + +// CRC +const uint8_t FUJITSU_GENERAL_BASE_BYTE15 = 0x6F; + +// Power off packet is specific +const uint16_t FUJITSU_GENERAL_OFF_LENGTH = 7; + +const uint8_t FUJITSU_GENERAL_OFF_BYTE0 = FUJITSU_GENERAL_BASE_BYTE0; +const uint8_t FUJITSU_GENERAL_OFF_BYTE1 = FUJITSU_GENERAL_BASE_BYTE1; +const uint8_t FUJITSU_GENERAL_OFF_BYTE2 = FUJITSU_GENERAL_BASE_BYTE2; +const uint8_t FUJITSU_GENERAL_OFF_BYTE3 = FUJITSU_GENERAL_BASE_BYTE3; +const uint8_t FUJITSU_GENERAL_OFF_BYTE4 = FUJITSU_GENERAL_BASE_BYTE4; +const uint8_t FUJITSU_GENERAL_OFF_BYTE5 = 0x02; +const uint8_t FUJITSU_GENERAL_OFF_BYTE6 = 0xFD; + +const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius +const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius + +const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300; +const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600; +const uint16_t FUJITSU_GENERAL_BIT_MARK = 420; +const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200; +const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420; +const uint16_t FUJITSU_GENERAL_TRL_MARK = 420; +const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000; + +const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000; + +FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {} + +void FujitsuGeneralClimate::transmit_state() { + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->transmit_off_(); + return; + } + uint8_t remote_state[FUJITSU_GENERAL_STATE_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_BASE_BYTE0; + remote_state[1] = FUJITSU_GENERAL_BASE_BYTE1; + remote_state[2] = FUJITSU_GENERAL_BASE_BYTE2; + remote_state[3] = FUJITSU_GENERAL_BASE_BYTE3; + remote_state[4] = FUJITSU_GENERAL_BASE_BYTE4; + remote_state[5] = FUJITSU_GENERAL_BASE_BYTE5; + remote_state[6] = FUJITSU_GENERAL_BASE_BYTE6; + remote_state[7] = FUJITSU_GENERAL_BASE_BYTE7; + remote_state[8] = FUJITSU_GENERAL_BASE_BYTE8; + remote_state[9] = FUJITSU_GENERAL_BASE_BYTE9; + remote_state[10] = FUJITSU_GENERAL_BASE_BYTE10; + remote_state[11] = FUJITSU_GENERAL_BASE_BYTE11; + remote_state[12] = FUJITSU_GENERAL_BASE_BYTE12; + remote_state[13] = FUJITSU_GENERAL_BASE_BYTE13; + remote_state[14] = FUJITSU_GENERAL_BASE_BYTE14; + remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15; + + // Set temperature + uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN); + safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX); + remote_state[8] = (byte) safecelsius - 16; + remote_state[8] = remote_state[8] << 4; + + // If not powered - set power on flag + if (!this->power_) { + remote_state[8] = (byte) remote_state[8] | FUJITSU_GENERAL_POWER_ON_MASK_BYTE8; + } + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state[9] = FUJITSU_GENERAL_MODE_COOL_BYTE9; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9; + break; + case climate::CLIMATE_MODE_AUTO: + default: + remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9; + break; + // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome + } + + // TODO: missing support for fan speed + remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + + // TODO: missing support for swing + // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10; + + // TODO: missing support for outdoor unit low noise + // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14; + + // CRC + remote_state[15] = 0; + for (int i = 7; i < 15; i++) { + remote_state[15] += (byte) remote_state[i]; // Addiction + } + remote_state[15] = 0x100 - remote_state[15]; // mod 256 + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = true; +} + +void FujitsuGeneralClimate::transmit_off_() { + uint8_t remote_state[FUJITSU_GENERAL_OFF_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_OFF_BYTE0; + remote_state[1] = FUJITSU_GENERAL_OFF_BYTE1; + remote_state[2] = FUJITSU_GENERAL_OFF_BYTE2; + remote_state[3] = FUJITSU_GENERAL_OFF_BYTE3; + remote_state[4] = FUJITSU_GENERAL_OFF_BYTE4; + remote_state[5] = FUJITSU_GENERAL_OFF_BYTE5; + remote_state[6] = FUJITSU_GENERAL_OFF_BYTE6; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = false; +} + +} // namespace fujitsu_general +} // namespace esphome diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h new file mode 100644 index 0000000000..80db81a167 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace fujitsu_general { + +class FujitsuGeneralClimate : public climate_ir::ClimateIR { + public: + FujitsuGeneralClimate(); + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Transmit via IR power off command. + void transmit_off_(); + + bool power_{false}; +}; + +} // namespace fujitsu_general +} // namespace esphome diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 66af291a17..3c94f4a243 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,37 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, remote_receiver, sensor -from esphome.components.remote_base import CONF_TRANSMITTER_ID, CONF_RECEIVER_ID -from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor', 'climate_ir'] +AUTO_LOAD = ['climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') -Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) +Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - if CONF_RECEIVER_ID in config: - receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) - cg.add(receiver.register_listener(var)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 7c5257a0f3..273162662d 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -9,9 +9,9 @@ namespace tcl112 { const float TCL112_TEMP_MAX = 31.0; const float TCL112_TEMP_MIN = 16.0; -class Tcl112Climate : public climate::ClimateIR { +class Tcl112Climate : public climate_ir::ClimateIR { public: - Tcl112Climate() : climate::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} + Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} protected: /// Transmit via IR the state of this climate controller. diff --git a/script/ci-custom.py b/script/ci-custom.py index b43b15bcdd..51b7b9d9b5 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -298,6 +298,19 @@ def lint_relative_py_import(fname): ' from . import abc_ns\n\n') +@lint_content_check(include=['esphome/components/*.h', 'esphome/components/*.cpp', + 'esphome/components/*.tcc']) +def lint_namespace(fname, content): + expected_name = re.match(r'^esphome/components/([^/]+)/.*', + fname.replace(os.path.sep, '/')).group(1) + search = 'namespace {}'.format(expected_name) + if search in content: + return None + return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ + 'functions in a separate namespace that matches the integration\'s name. ' \ + 'Please make sure the file contains {}'.format(highlight(search)) + + @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h']) def lint_esphome_h(fname): return ("File contains reference to 'esphome.h' - This file is " From 5a67e723895297f78bae960cbfd38c787192a85e Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Sun, 27 Oct 2019 07:05:13 -0400 Subject: [PATCH 035/412] Added more power data to the atm90e32 component (#799) * Added more data to atm90e32 component * ignore * correction * Delete 6chan_energy_meter.yaml * Update sensor.py fixed indents * Update atm90e32.h * Update esphome/components/atm90e32/sensor.py Co-Authored-By: Otto Winter * PR request changes * repository test branch * Update setup.py * Update const.py * backslash * comma! * delete test yaml * corrected chip temp * change to signed int for get_pf_ functions * Update atm90e32.h formatting * adjusted function & variable names * Update atm90e32.h formatting * Update sensor.py Import CONF_POWER_FACTOR from const.py * travis formatting * Update esphome/components/atm90e32/sensor.py Co-Authored-By: Otto Winter * Update esphome/components/atm90e32/atm90e32.h Co-Authored-By: Otto Winter --- esphome/components/atm90e32/atm90e32.cpp | 58 +++++++++++++++++++++++- esphome/components/atm90e32/atm90e32.h | 15 ++++++ esphome/components/atm90e32/sensor.py | 23 ++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 283319994e..bc1e326147 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -40,9 +40,30 @@ void ATM90E32Component::update() { if (this->phase_[2].power_sensor_ != nullptr) { this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_()); } + if (this->phase_[0].reactive_power_sensor_ != nullptr) { + this->phase_[0].reactive_power_sensor_->publish_state(this->get_reactive_power_a_()); + } + if (this->phase_[1].reactive_power_sensor_ != nullptr) { + this->phase_[1].reactive_power_sensor_->publish_state(this->get_reactive_power_b_()); + } + if (this->phase_[2].reactive_power_sensor_ != nullptr) { + this->phase_[2].reactive_power_sensor_->publish_state(this->get_reactive_power_c_()); + } + if (this->phase_[0].power_factor_sensor_ != nullptr) { + this->phase_[0].power_factor_sensor_->publish_state(this->get_power_factor_a_()); + } + if (this->phase_[1].power_factor_sensor_ != nullptr) { + this->phase_[1].power_factor_sensor_->publish_state(this->get_power_factor_b_()); + } + if (this->phase_[2].power_factor_sensor_ != nullptr) { + this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); + } if (this->freq_sensor_ != nullptr) { this->freq_sensor_->publish_state(this->get_frequency_()); } + if (this->chip_temperature_sensor_ != nullptr) { + this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); + } this->status_clear_warning(); } @@ -89,13 +110,20 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Voltage A", this->phase_[0].voltage_sensor_); LOG_SENSOR(" ", "Current A", this->phase_[0].current_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); + LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); + LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); - LOG_SENSOR(" ", "Frequency", this->freq_sensor_) + LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); + LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_); + LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; } @@ -180,9 +208,37 @@ float ATM90E32Component::get_active_power_c_() { int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB); return val * 0.00032f; } +float ATM90E32Component::get_reactive_power_a_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANA, ATM90E32_REGISTER_QMEANALSB); + return val * 0.00032f; +} +float ATM90E32Component::get_reactive_power_b_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANB, ATM90E32_REGISTER_QMEANBLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_reactive_power_c_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANC, ATM90E32_REGISTER_QMEANCLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_power_factor_a_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANA); + return (float) pf / 1000; +} +float ATM90E32Component::get_power_factor_b_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANB); + return (float) pf / 1000; +} +float ATM90E32Component::get_power_factor_c_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); + return (float) pf / 1000; +} float ATM90E32Component::get_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; } +float ATM90E32Component::get_chip_temperature_() { + uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP); + return (float) ctemp; +} } // namespace atm90e32 } // namespace esphome diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 4dd2bd5784..3daa31d15d 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -19,10 +19,15 @@ class ATM90E32Component : public PollingComponent, void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } + void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { + chip_temperature_sensor_ = chip_temperature_sensor; + } void set_line_freq(int freq) { line_freq_ = freq; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } @@ -40,7 +45,14 @@ class ATM90E32Component : public PollingComponent, float get_active_power_a_(); float get_active_power_b_(); float get_active_power_c_(); + float get_reactive_power_a_(); + float get_reactive_power_b_(); + float get_reactive_power_c_(); + float get_power_factor_a_(); + float get_power_factor_b_(); + float get_power_factor_c_(); float get_frequency_(); + float get_chip_temperature_(); struct ATM90E32Phase { uint16_t volt_gain_{41820}; @@ -48,8 +60,11 @@ class ATM90E32Component : public PollingComponent, sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; } phase_[3]; sensor::Sensor *freq_sensor_{nullptr}; + sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; }; diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 7b62740f8e..030ff90a77 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -2,14 +2,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import \ - CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_FREQUENCY, \ - ICON_FLASH, UNIT_HZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT + CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, \ + ICON_FLASH, ICON_LIGHTBULB, ICON_CURRENT_AC, ICON_THERMOMETER, \ + UNIT_HZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, UNIT_CELSIUS CONF_PHASE_A = 'phase_a' CONF_PHASE_B = 'phase_b' CONF_PHASE_C = 'phase_c' +CONF_REACTIVE_POWER = 'reactive_power' CONF_LINE_FREQUENCY = 'line_frequency' +CONF_CHIP_TEMPERATURE = 'chip_temperature' CONF_GAIN_PGA = 'gain_pga' CONF_GAIN_VOLTAGE = 'gain_voltage' CONF_GAIN_CT = 'gain_ct' @@ -28,8 +31,10 @@ ATM90E32Component = atm90e32_ns.class_('ATM90E32Component', cg.PollingComponent, ATM90E32_PHASE_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_EMPTY, ICON_LIGHTBULB, 2), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t, }) @@ -39,7 +44,8 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, - cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HZ, ICON_FLASH, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HZ, ICON_CURRENT_AC, 1), + cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), }).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) @@ -65,8 +71,17 @@ def to_code(config): if CONF_POWER in conf: sens = yield sensor.new_sensor(conf[CONF_POWER]) cg.add(var.set_power_sensor(i, sens)) + if CONF_REACTIVE_POWER in conf: + sens = yield sensor.new_sensor(conf[CONF_REACTIVE_POWER]) + cg.add(var.set_react_pow_sensor(i, sens)) + if CONF_POWER_FACTOR in conf: + sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) + cg.add(var.set_pf_sensor(i, sens)) if CONF_FREQUENCY in config: sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) + if CONF_CHIP_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) + cg.add(var.set_chip_temp_sensor(sens)) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) From c1f5e04d6cd2a051b6082c65a2d41b82e84a07c5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 27 Oct 2019 12:27:46 +0100 Subject: [PATCH 036/412] Warn when UART and logger operating on same bus (#803) --- esphome/components/logger/logger.h | 1 + esphome/components/uart/uart.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3d129e335b..9d252af515 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -31,6 +31,7 @@ class Logger : public Component { /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); + uint32_t get_baud_rate() const { return baud_rate_; } /// Get the UART used by the logger. UARTSelection get_uart() const; diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index fd27a8f897..3284d4cb67 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -2,6 +2,11 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif namespace esphome { namespace uart { @@ -41,6 +46,12 @@ void UARTComponent::dump_config() { } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { @@ -145,6 +156,13 @@ void UARTComponent::dump_config() { } else { ESP_LOGCONFIG(TAG, " Using software serial"); } + +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { From 7bf6fd316fe155f9b44c199db50bd628e11934c1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 27 Oct 2019 12:28:01 +0100 Subject: [PATCH 037/412] Add Tuya message for no datapoints (#804) See also https://github.com/esphome/feature-requests/issues/352#issuecomment-546579206 --- esphome/components/tuya/tuya.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 4efcf08fe6..cb796644c8 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -36,6 +36,9 @@ void Tuya::dump_config() { else ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); } + if (this->datapoints_.empty()) { + ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); + } this->check_uart_settings(9600); } From b0bb692af448956d0179eb1341d12eb9f2f2f846 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 27 Oct 2019 12:30:19 +0100 Subject: [PATCH 038/412] AS3935 Use normal pin polling for IRQ (#805) * AS3935 Use normal pin polling for IRQ See also https://github.com/esphome/feature-requests/issues/452 * Fix tests --- esphome/components/as3935/__init__.py | 11 +++++------ esphome/components/as3935/as3935.cpp | 13 ++++--------- esphome/components/as3935/as3935.h | 13 ++----------- tests/test1.yaml | 2 +- tests/test2.yaml | 2 +- 5 files changed, 13 insertions(+), 28 deletions(-) diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index f8ac4eea01..de25060623 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -1,12 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_PIN, CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \ +from esphome.const import CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \ CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \ CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAPACITANCE from esphome.core import coroutine - AUTO_LOAD = ['sensor', 'binary_sensor'] MULTI_CONF = True @@ -15,10 +14,10 @@ CONF_AS3935_ID = 'as3935_id' as3935_ns = cg.esphome_ns.namespace('as3935') AS3935 = as3935_ns.class_('AS3935Component', cg.Component) +CONF_IRQ_PIN = 'irq_pin' AS3935_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(AS3935), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, - pins.validate_has_interrupt), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_INDOOR, default=True): cv.boolean, cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7), @@ -35,8 +34,8 @@ AS3935_SCHEMA = cv.Schema({ def setup_as3935(var, config): yield cg.register_component(var, config) - pin = yield cg.gpio_pin_expression(config[CONF_PIN]) - cg.add(var.set_pin(pin)) + irq_pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(irq_pin)) cg.add(var.set_indoor(config[CONF_INDOOR])) cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL])) cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD])) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 0fc8f086d2..f8272e6036 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -9,10 +9,8 @@ static const char *TAG = "as3935"; void AS3935Component::setup() { ESP_LOGCONFIG(TAG, "Setting up AS3935..."); - this->pin_->setup(); - this->store_.pin = this->pin_->to_isr(); - LOG_PIN(" Interrupt Pin: ", this->pin_); - this->pin_->attach_interrupt(AS3935ComponentStore::gpio_intr, &this->store_, RISING); + this->irq_pin_->setup(); + LOG_PIN(" IRQ Pin: ", this->irq_pin_); // Write properties to sensor this->write_indoor(this->indoor_); @@ -27,13 +25,13 @@ void AS3935Component::setup() { void AS3935Component::dump_config() { ESP_LOGCONFIG(TAG, "AS3935:"); - LOG_PIN(" Interrupt Pin: ", this->pin_); + LOG_PIN(" Interrupt Pin: ", this->irq_pin_); } float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } void AS3935Component::loop() { - if (!this->store_.interrupt) + if (!this->irq_pin_->digital_read()) return; uint8_t int_value = this->read_interrupt_register_(); @@ -53,7 +51,6 @@ void AS3935Component::loop() { this->energy_sensor_->publish_state(energy); } this->thunder_alert_binary_sensor_->publish_state(false); - this->store_.interrupt = false; } void AS3935Component::write_indoor(bool indoor) { @@ -222,7 +219,5 @@ uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) { return value; } -void ICACHE_RAM_ATTR AS3935ComponentStore::gpio_intr(AS3935ComponentStore *arg) { arg->interrupt = true; } - } // namespace as3935 } // namespace esphome diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index ca7d409282..d0e53e7832 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -50,14 +50,6 @@ enum AS3935Values { NOISE_INT = 0x01 }; -/// Store data in a class that doesn't use multiple-inheritance (vtables in flash) -struct AS3935ComponentStore { - volatile bool interrupt; - - ISRInternalGPIOPin *pin; - static void gpio_intr(AS3935ComponentStore *arg); -}; - class AS3935Component : public Component { public: void setup() override; @@ -65,7 +57,7 @@ class AS3935Component : public Component { float get_setup_priority() const override; void loop() override; - void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } void set_distance_sensor(sensor::Sensor *distance_sensor) { distance_sensor_ = distance_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_thunder_alert_binary_sensor(binary_sensor::BinarySensor *thunder_alert_binary_sensor) { @@ -102,8 +94,7 @@ class AS3935Component : public Component { sensor::Sensor *distance_sensor_; sensor::Sensor *energy_sensor_; binary_sensor::BinarySensor *thunder_alert_binary_sensor_; - GPIOPin *pin_; - AS3935ComponentStore store_; + GPIOPin *irq_pin_; bool indoor_; uint8_t noise_level_; diff --git a/tests/test1.yaml b/tests/test1.yaml index 2fbef9d4f7..66f0220836 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -172,7 +172,7 @@ dallas: as3935_spi: cs_pin: GPIO12 - pin: GPIO13 + irq_pin: GPIO13 sensor: - platform: adc diff --git a/tests/test2.yaml b/tests/test2.yaml index 1f4245d81d..097c29285a 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -54,7 +54,7 @@ deep_sleep: sleep_duration: 50s as3935_i2c: - pin: GPIO12 + irq_pin: GPIO12 sensor: From 969bdb06ce105c60c655138ebf7715a92fe2d3e6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 27 Oct 2019 12:30:36 +0100 Subject: [PATCH 039/412] Fix modbus register (#806) Fixes https://github.com/esphome/feature-requests/issues/49#issuecomment-546555289 Co-Authored-By: tsunglung --- esphome/components/modbus/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 88105b7baf..cada835905 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -41,3 +41,4 @@ def register_modbus_device(var, config): parent = yield cg.get_variable(config[CONF_MODBUS_ID]) cg.add(var.set_parent(parent)) cg.add(var.set_address(config[CONF_ADDRESS])) + cg.add(parent.register_device(var)) From 08148c58306e62763c0ae8bd08beb0c421f3f440 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 27 Oct 2019 12:30:48 +0100 Subject: [PATCH 040/412] Fix web server transition length truncated (#807) Fixes https://github.com/esphome/issues/issues/772 --- esphome/components/web_server/web_server.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b51ad2cf51..4fdbbbce7d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -416,11 +416,15 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch ma if (request->hasParam("color_temp")) call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); - if (request->hasParam("flash")) - call.set_flash_length((uint32_t) request->getParam("flash")->value().toFloat() * 1000); + if (request->hasParam("flash")) { + float length_s = request->getParam("flash")->value().toFloat(); + call.set_flash_length(static_cast(length_s * 1000)); + } - if (request->hasParam("transition")) - call.set_transition_length((uint32_t) request->getParam("transition")->value().toFloat() * 1000); + if (request->hasParam("transition")) { + float length_s = request->getParam("transition")->value().toFloat(); + call.set_transition_length(static_cast(length_s * 1000)); + } if (request->hasParam("effect")) { const char *effect = request->getParam("effect")->value().c_str(); From 07286d1d76967a47ddeb4880877ea292f663be65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20GR=C3=89A?= Date: Wed, 30 Oct 2019 16:16:14 +0100 Subject: [PATCH 041/412] Add check if middle_text is too short (#811) * Add check if middle_text is too short * Use int division as suggested --- esphome/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index bf2d68d834..a78413ff04 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -364,7 +364,7 @@ def command_update_all(args): def print_bar(middle_text): middle_text = " {} ".format(middle_text) width = len(click.unstyle(middle_text)) - half_line = "=" * ((twidth - width) / 2) + half_line = "=" * ((twidth - width) // 2) click.echo("%s%s%s" % (half_line, middle_text, half_line)) for f in files: From 41233d7f25d2e88b6a4ca88409ee183214a3d1b7 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 31 Oct 2019 17:10:52 +0300 Subject: [PATCH 042/412] [Hotfix] Dashboard authentication on Py3 (#812) * Fix * Review fix * Reverted first fix --- esphome/dashboard/dashboard.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0927fe2dfe..c934626da8 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -29,7 +29,7 @@ import tornado.websocket from esphome import const, util from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2, decode_text +from esphome.py_compat import IS_PY2, decode_text, encode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote @@ -85,12 +85,11 @@ class DashboardSettings(object): def check_password(self, username, password): if not self.using_auth: return True + if username != self.username: + return False - if IS_PY2: - password = hmac.new(password).digest() - else: - password = hmac.new(password.encode()).digest() - return username == self.username and hmac.compare_digest(self.password_digest, password) + password_digest = hmac.new(encode_text(password)).digest() + return hmac.compare_digest(self.password_digest, password_digest) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -610,8 +609,8 @@ class LoginHandler(BaseHandler): 'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'), } data = { - 'username': str(self.get_argument('username', '')), - 'password': str(self.get_argument('password', '')) + 'username': decode_text(self.get_argument('username', '')), + 'password': decode_text(self.get_argument('password', '')) } try: req = requests.post('http://hassio/auth', headers=headers, data=data) @@ -628,8 +627,8 @@ class LoginHandler(BaseHandler): self.render_login_page(error="Invalid username or password") def post_native_login(self): - username = str(self.get_argument("username", '').encode('utf-8')) - password = str(self.get_argument("password", '').encode('utf-8')) + username = decode_text(self.get_argument("username", '')) + password = decode_text(self.get_argument("password", '')) if settings.check_password(username, password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") From 69fd3e89378e777a26cb3b907020bf03dd0443bd Mon Sep 17 00:00:00 2001 From: Lumpusz Date: Thu, 31 Oct 2019 15:34:19 +0100 Subject: [PATCH 043/412] service uuid based ble tracking (#800) * service uuid based ble tracking * code review fixes * fix import, format * fix indentation * reformat --- .../components/ble_presence/binary_sensor.py | 22 +++++-- .../ble_presence/ble_presence_device.h | 62 +++++++++++++++++-- esphome/components/ble_rssi/ble_rssi_sensor.h | 62 +++++++++++++++++-- esphome/components/ble_rssi/sensor.py | 22 +++++-- .../components/esp32_ble_tracker/__init__.py | 45 ++++++++++++++ .../esp32_ble_tracker/esp32_ble_tracker.cpp | 1 + .../esp32_ble_tracker/esp32_ble_tracker.h | 2 + esphome/const.py | 1 + tests/test2.yaml | 18 ++++++ 9 files changed, 215 insertions(+), 20 deletions(-) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 43ec9455d4..1cf8009384 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_ID +from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID DEPENDENCIES = ['esp32_ble_tracker'] @@ -9,10 +9,12 @@ ble_presence_ns = cg.esphome_ns.namespace('ble_presence') BLEPresenceDevice = ble_presence_ns.class_('BLEPresenceDevice', binary_sensor.BinarySensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BLEPresenceDevice), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( + cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) def to_code(config): @@ -21,4 +23,14 @@ def to_code(config): yield esp32_ble_tracker.register_ble_device(var, config) yield binary_sensor.register_binary_sensor(var, config) - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if CONF_MAC_ADDRESS in config: + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_SERVICE_UUID in config: + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 262cc3eedf..9d7ec83bc7 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -13,17 +13,67 @@ class BLEPresenceDevice : public binary_sensor::BinarySensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: - void set_address(uint64_t address) { address_ = address; } + void set_address(uint64_t address) { + this->by_address_ = true; + this->address_ = address; + } + void set_service_uuid16(uint16_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); + } + void set_service_uuid32(uint32_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); + } + void set_service_uuid128(uint8_t *uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; - return true; + if (this->by_address_) { + if (device.address_uint64() == this->address_) { + this->publish_state(true); + this->found_ = true; + return true; + } + } else { + for (auto uuid : device.get_service_uuids()) { + switch (this->uuid_.get_uuid().len) { + case ESP_UUID_LEN_16: + if (uuid.get_uuid().len == ESP_UUID_LEN_16 && + uuid.get_uuid().uuid.uuid16 == this->uuid_.get_uuid().uuid.uuid16) { + this->publish_state(true); + this->found_ = true; + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.get_uuid().len == ESP_UUID_LEN_32 && + uuid.get_uuid().uuid.uuid32 == this->uuid_.get_uuid().uuid.uuid32) { + this->publish_state(true); + this->found_ = true; + return true; + } + break; + case ESP_UUID_LEN_128: + if (uuid.get_uuid().len == ESP_UUID_LEN_128) { + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (this->uuid_.get_uuid().uuid.uuid128[i] != uuid.get_uuid().uuid.uuid128[i]) { + return false; + } + } + this->publish_state(true); + this->found_ = true; + return true; + } + break; + } + } } return false; } @@ -32,7 +82,9 @@ class BLEPresenceDevice : public binary_sensor::BinarySensor, protected: bool found_{false}; + bool by_address_{false}; uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 2c296b3831..17dd0d4a7d 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -11,17 +11,67 @@ namespace ble_rssi { class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: - void set_address(uint64_t address) { address_ = address; } + void set_address(uint64_t address) { + this->by_address_ = true; + this->address_ = address; + } + void set_service_uuid16(uint16_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); + } + void set_service_uuid32(uint32_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); + } + void set_service_uuid128(uint8_t *uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } void on_scan_end() override { if (!this->found_) this->publish_state(NAN); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() == this->address_) { - this->publish_state(device.get_rssi()); - this->found_ = true; - return true; + if (this->by_address_) { + if (device.address_uint64() == this->address_) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } else { + for (auto uuid : device.get_service_uuids()) { + switch (this->uuid_.get_uuid().len) { + case ESP_UUID_LEN_16: + if (uuid.get_uuid().len == ESP_UUID_LEN_16 && + uuid.get_uuid().uuid.uuid16 == this->uuid_.get_uuid().uuid.uuid16) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.get_uuid().len == ESP_UUID_LEN_32 && + uuid.get_uuid().uuid.uuid32 == this->uuid_.get_uuid().uuid.uuid32) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + break; + case ESP_UUID_LEN_128: + if (uuid.get_uuid().len == ESP_UUID_LEN_128) { + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (uuid.get_uuid().uuid.uuid128[i] != this->uuid_.get_uuid().uuid.uuid128[i]) { + return false; + } + } + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + break; + } + } } return false; } @@ -30,7 +80,9 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi protected: bool found_{false}; + bool by_address_{false}; uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; }; } // namespace ble_rssi diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index ee8f71632f..76a27e6f2b 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_ID, UNIT_DECIBEL, ICON_SIGNAL +from esphome.const import CONF_SERVICE_UUID, CONF_MAC_ADDRESS, CONF_ID, UNIT_DECIBEL, ICON_SIGNAL DEPENDENCIES = ['esp32_ble_tracker'] @@ -9,10 +9,12 @@ ble_rssi_ns = cg.esphome_ns.namespace('ble_rssi') BLERSSISensor = ble_rssi_ns.class_('BLERSSISensor', sensor.Sensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DECIBEL, ICON_SIGNAL, 0).extend({ +CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_DECIBEL, ICON_SIGNAL, 0).extend({ cv.GenerateID(): cv.declare_id(BLERSSISensor), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( + cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) def to_code(config): @@ -21,4 +23,14 @@ def to_code(config): yield esp32_ble_tracker.register_ble_device(var, config) yield sensor.register_sensor(var, config) - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if CONF_MAC_ADDRESS in config: + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_SERVICE_UUID in config: + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add(var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add(var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 7e998e77b1..4aa5b1610a 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,3 +1,4 @@ +import re import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ @@ -32,6 +33,50 @@ def validate_scan_parameters(config): return config +bt_uuid16_format = 'XXXX' +bt_uuid32_format = 'XXXXXXXX' +bt_uuid128_format = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' + + +def bt_uuid(value): + in_value = cv.string_strict(value) + value = in_value.upper() + + if len(value) == len(bt_uuid16_format): + pattern = re.compile("^[A-F|0-9]{4,}$") + if not pattern.match(value): + raise cv.Invalid( + u"Invalid hexadecimal value for 16 bit UUID format: '{}'".format(in_value)) + return value + if len(value) == len(bt_uuid32_format): + pattern = re.compile("^[A-F|0-9]{8,}$") + if not pattern.match(value): + raise cv.Invalid( + u"Invalid hexadecimal value for 32 bit UUID format: '{}'".format(in_value)) + return value + if len(value) == len(bt_uuid128_format): + pattern = re.compile( + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$") + if not pattern.match(value): + raise cv.Invalid( + u"Invalid hexadecimal value for 128 UUID format: '{}'".format(in_value)) + return value + raise cv.Invalid( + u"Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( + bt_uuid16_format, bt_uuid32_format, bt_uuid128_format)) + + +def as_hex(value): + return cg.RawExpression('0x{}ULL'.format(value)) + + +def as_hex_array(value): + value = value.replace("-", "") + cpp_array = ['0x{}'.format(part) for part in [value[i:i+2] for i in range(0, len(value), 2)]] + return cg.RawExpression( + '(uint8_t*)(const uint8_t[16]){{{}}}'.format(','.join(reversed(cpp_array)))) + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 7a5bd733a2..4b67277f16 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -245,6 +245,7 @@ bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { } return false; } +esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 82e8e553fc..280c3fc45f 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -25,6 +25,8 @@ class ESPBTUUID { bool contains(uint8_t data1, uint8_t data2) const; + esp_bt_uuid_t get_uuid(); + std::string to_string(); protected: diff --git a/esphome/const.py b/esphome/const.py index ffef106945..dc607d62ff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -382,6 +382,7 @@ CONF_SENSORS = 'sensors' CONF_SEQUENCE = 'sequence' CONF_SERVERS = 'servers' CONF_SERVICE = 'service' +CONF_SERVICE_UUID = 'service_uuid' CONF_SERVICES = 'services' CONF_SETUP_MODE = 'setup_mode' CONF_SETUP_PRIORITY = 'setup_priority' diff --git a/tests/test2.yaml b/tests/test2.yaml index 097c29285a..78e1ab149a 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -61,6 +61,15 @@ sensor: - platform: ble_rssi mac_address: AC:37:43:77:5F:4C name: "BLE Google Home Mini RSSI value" + - platform: ble_rssi + service_uuid: '11aa' + name: "BLE Test Service 16" + - platform: ble_rssi + service_uuid: '11223344' + name: "BLE Test Service 32" + - platform: ble_rssi + service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + name: "BLE Test Service 128" - platform: xiaomi_hhccjcy01 mac_address: 94:2B:FF:5C:91:61 temperature: @@ -170,6 +179,15 @@ binary_sensor: - platform: ble_presence mac_address: AC:37:43:77:5F:4C name: "ESP32 BLE Tracker Google Home Mini" + - platform: ble_presence + service_uuid: '11aa' + name: "BLE Test Service 16 Presence" + - platform: ble_presence + service_uuid: '11223344' + name: "BLE Test Service 32 Presence" + - platform: ble_presence + service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + name: "BLE Test Service 128 Presence" - platform: esp32_touch name: "ESP32 Touch Pad GPIO27" pin: GPIO27 From a6faccb4d9fee7bdf92fb665247e6c73815c5f66 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 20:09:07 +0100 Subject: [PATCH 044/412] Uppercase ESPHome (#814) --- esphome/core/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index fbd3c56e54..2600ace218 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -97,7 +97,7 @@ void Application::loop() { if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ == 0) { - ESP_LOGI(TAG, "esphome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); } this->components_[this->dump_config_at_]->dump_config(); From 742c21506c8a11f5286ff060da5925215b55a048 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 20:09:23 +0100 Subject: [PATCH 045/412] Print update interval for pulse counter (#816) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 6503711e35..c71e51eb32 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -149,6 +149,7 @@ void PulseCounterSensor::dump_config() { ESP_LOGCONFIG(TAG, " Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]); ESP_LOGCONFIG(TAG, " Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]); ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->storage_.filter_us); + LOG_UPDATE_INTERVAL(this); } void PulseCounterSensor::update() { From 864c5d89085876432ac8e466709a92c513a3a7c9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 20:09:57 +0100 Subject: [PATCH 046/412] Allow TimePeriod for time_period_str_unit (#815) --- esphome/config_validation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 405d695b51..956779f655 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -461,6 +461,8 @@ def time_period_str_unit(value): if isinstance(value, int): raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " "'{0}s'?".format(value)) + if isinstance(value, TimePeriod): + value = str(value) if not isinstance(value, string_types): raise Invalid("Expected string for time period with unit.") From 560251ab2af52c2d52755d2c3effda2d1951636b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 20:25:16 +0100 Subject: [PATCH 047/412] Scheduler fixes (#813) * Scheduler fixes Fixes https://github.com/esphome/issues/issues/789, fixes https://github.com/esphome/issues/issues/788 Also changes to use unique_ptr - this should be much safer than the raw pointers form before (though the scoping rules might cause some issues, but looking closely I didn't find anything) * Disable debugging * Format --- esphome/core/scheduler.cpp | 179 ++++++++++++++++++++----------------- esphome/core/scheduler.h | 18 +++- 2 files changed, 112 insertions(+), 85 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 88054147f8..cc4331b38e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -9,6 +9,9 @@ static const char *TAG = "scheduler"; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +// Uncomment to debug scheduler +// #define ESPHOME_DEBUG_SCHEDULER + void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func) { const uint32_t now = this->millis_(); @@ -21,7 +24,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); - auto *item = new SchedulerItem(); + auto item = make_unique(); item->component = component; item->name = name; item->type = SchedulerItem::TIMEOUT; @@ -30,7 +33,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u item->last_execution_major = this->millis_major_; item->f = std::move(func); item->remove = false; - this->push_(item); + this->push_(std::move(item)); } bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); @@ -52,7 +55,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); - auto *item = new SchedulerItem(); + auto item = make_unique(); item->component = component; item->name = name; item->type = SchedulerItem::INTERVAL; @@ -63,7 +66,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, item->last_execution_major--; item->f = std::move(func); item->remove = false; - this->push_(item); + this->push_(std::move(item)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::INTERVAL); @@ -71,7 +74,7 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam optional HOT Scheduler::next_schedule_in() { if (this->empty_()) return {}; - auto *item = this->items_[0]; + auto &item = this->items_[0]; const uint32_t now = this->millis_(); uint32_t next_time = item->last_execution + item->interval; if (next_time < now) @@ -82,98 +85,103 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { const uint32_t now = this->millis_(); this->process_to_add(); - // Uncomment for debugging the scheduler: +#ifdef ESPHOME_DEBUG_SCHEDULER + static uint32_t last_print = 0; - // if (random_uint32() % 400 == 0) { - // std::vector old_items = this->items_; - // ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); - // while (!this->empty_()) { - // auto *item = this->items_[0]; - // const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; - // ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u", - // type, item->name.c_str(), item->interval, item->last_execution, item->last_execution_major, - // item->last_execution + item->interval); - // this->pop_raw_(); - // } - // ESP_LOGVV(TAG, "\n"); - // this->items_ = old_items; - //} + if (now - last_print > 2000) { + last_print = now; + std::vector> old_items; + ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); + while (!this->empty_()) { + auto item = std::move(this->items_[0]); + const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; + ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(), + item->interval, item->last_execution, item->last_execution_major, item->next_execution(), + item->next_execution_major()); + + this->pop_raw_(); + old_items.push_back(std::move(item)); + } + ESP_LOGVV(TAG, "\n"); + this->items_ = std::move(old_items); + } +#endif // ESPHOME_DEBUG_SCHEDULER while (!this->empty_()) { - // Don't copy-by value yet - auto *item = this->items_[0]; - if ((now - item->last_execution) < item->interval) - // Not reached timeout yet, done for this call - break; - uint8_t major = item->last_execution_major; - if (item->last_execution > now) - major++; - if (major != this->millis_major_) - break; + // use scoping to indicate visibility of `item` variable + { + // Don't copy-by value yet + auto &item = this->items_[0]; + if ((now - item->last_execution) < item->interval) + // Not reached timeout yet, done for this call + break; + uint8_t major = item->next_execution_major(); + if (this->millis_major_ - major > 1) + break; - // Don't run on failed components - if (item->component != nullptr && item->component->is_failed()) { - this->pop_raw_(); - delete item; - continue; - } + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + this->pop_raw_(); + continue; + } #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; - ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(), - item->interval, item->last_execution, now); + const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; + ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(), + item->interval, item->last_execution, now); #endif - // Warning: During f(), a lot of stuff can happen, including: - // - timeouts/intervals get added, potentially invalidating vector pointers - // - timeouts/intervals get cancelled - item->f(); - - // Only pop after function call, this ensures we were reachable - // during the function call and know if we were cancelled. - this->pop_raw_(); - - if (item->remove) { - // We were removed/cancelled in the function call, stop - delete item; - continue; + // Warning: During f(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + item->f(); } - if (item->type == SchedulerItem::INTERVAL) { - if (item->interval != 0) { - const uint32_t before = item->last_execution; - const uint32_t amount = (now - item->last_execution) / item->interval; - item->last_execution += amount * item->interval; - if (item->last_execution < before) - item->last_execution_major++; + { + // new scope, item from before might have been moved in the vector + auto item = std::move(this->items_[0]); + + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (item->remove) { + // We were removed/cancelled in the function call, stop + continue; + } + + if (item->type == SchedulerItem::INTERVAL) { + if (item->interval != 0) { + const uint32_t before = item->last_execution; + const uint32_t amount = (now - item->last_execution) / item->interval; + item->last_execution += amount * item->interval; + if (item->last_execution < before) + item->last_execution_major++; + } + this->push_(std::move(item)); } - this->push_(item); - } else { - delete item; } } this->process_to_add(); } void HOT Scheduler::process_to_add() { - for (auto *it : this->to_add_) { + for (auto &it : this->to_add_) { if (it->remove) { - delete it; continue; } - this->items_.push_back(it); + this->items_.push_back(std::move(it)); std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); } void HOT Scheduler::cleanup_() { while (!this->items_.empty()) { - auto item = this->items_[0]; + auto &item = this->items_[0]; if (!item->remove) return; - delete item; this->pop_raw_(); } } @@ -181,15 +189,15 @@ void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->items_.pop_back(); } -void HOT Scheduler::push_(Scheduler::SchedulerItem *item) { this->to_add_.push_back(item); } +void HOT Scheduler::push_(std::unique_ptr item) { this->to_add_.push_back(std::move(item)); } bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { bool ret = false; - for (auto *it : this->items_) + for (auto &it : this->items_) if (it->component == component && it->name == name && it->type == type) { it->remove = true; ret = true; } - for (auto *it : this->to_add_) + for (auto &it : this->to_add_) if (it->component == component && it->name == name && it->type == type) { it->remove = true; ret = true; @@ -206,21 +214,30 @@ uint32_t Scheduler::millis_() { return now; } -bool HOT Scheduler::SchedulerItem::cmp(Scheduler::SchedulerItem *a, Scheduler::SchedulerItem *b) { +bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, + const std::unique_ptr &b) { // min-heap // return true if *a* will happen after *b* - uint32_t a_next_exec = a->last_execution + a->timeout; - uint8_t a_next_exec_major = a->last_execution_major; - if (a_next_exec < a->last_execution) - a_next_exec_major++; - - uint32_t b_next_exec = b->last_execution + b->timeout; - uint8_t b_next_exec_major = b->last_execution_major; - if (b_next_exec < b->last_execution) - b_next_exec_major++; + uint32_t a_next_exec = a->next_execution(); + uint8_t a_next_exec_major = a->next_execution_major(); + uint32_t b_next_exec = b->next_execution(); + uint8_t b_next_exec_major = b->next_execution_major(); if (a_next_exec_major != b_next_exec_major) { - return a_next_exec_major > b_next_exec_major; + // The "major" calculation is quite complicated. + // Basically, we need to check if the major value lies in the future or + // + + // Here are some cases to think about: + // Format: a_major,b_major -> expected result (a-b, b-a) + // a=255,b=0 -> false (255, 1) + // a=0,b=1 -> false (255, 1) + // a=1,b=0 -> true (1, 255) + // a=0,b=255 -> true (1, 255) + + uint8_t diff1 = a_next_exec_major - b_next_exec_major; + uint8_t diff2 = b_next_exec_major - a_next_exec_major; + return diff1 < diff2; } return a_next_exec > b_next_exec; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1faadfabd0..5688058a1e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include +#include namespace esphome { @@ -34,21 +35,30 @@ class Scheduler { bool remove; uint8_t last_execution_major; - static bool cmp(SchedulerItem *a, SchedulerItem *b); + inline uint32_t next_execution() { return this->last_execution + this->timeout; } + inline uint8_t next_execution_major() { + uint32_t next_exec = this->next_execution(); + uint8_t next_exec_major = this->last_execution_major; + if (next_exec < this->last_execution) + next_exec_major++; + return next_exec_major; + } + + static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); }; uint32_t millis_(); void cleanup_(); void pop_raw_(); - void push_(SchedulerItem *item); + void push_(std::unique_ptr item); bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); bool empty_() { this->cleanup_(); return this->items_.empty(); } - std::vector items_; - std::vector to_add_; + std::vector> items_; + std::vector> to_add_; uint32_t last_millis_{0}; uint8_t millis_major_{0}; }; From 80aaf669634223ace4c7163aed4d13328f2cef04 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 20:31:58 +0100 Subject: [PATCH 048/412] Fix fan oscillating (#818) Fixes https://github.com/esphome/issues/issues/783 --- esphome/components/binary/fan/__init__.py | 2 +- esphome/components/speed/fan/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index 6ba04ce355..dbfe1a8286 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -24,4 +24,4 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) - cg.add(var.set_oscillation(oscillation_output)) + cg.add(var.set_oscillating(oscillation_output)) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index ae6d09dfac..65ee5960f0 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -29,4 +29,4 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) - cg.add(var.set_oscillation(oscillation_output)) + cg.add(var.set_oscillating(oscillation_output)) From adf2a463fd60c818aeeeee520f1cf7ac19095da8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 31 Oct 2019 21:03:57 +0100 Subject: [PATCH 049/412] Fix some binary_sensor not having an initial state (#819) Fixes https://github.com/home-assistant/home-assistant/issues/28384 --- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/binary_sensor/binary_sensor.h | 7 ++++++- esphome/components/ble_presence/ble_presence_device.h | 2 +- esphome/components/nextion/nextion.h | 2 +- esphome/components/rdm6300/rdm6300.h | 2 +- esphome/components/remote_base/remote_base.h | 4 ++-- esphome/components/status/status_binary_sensor.cpp | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c391e12895..c082e2e9af 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -23,6 +23,7 @@ IS_PLATFORM_COMPONENT = True binary_sensor_ns = cg.esphome_ns.namespace('binary_sensor') BinarySensor = binary_sensor_ns.class_('BinarySensor', cg.Nameable) +BinarySensorInitiallyOff = binary_sensor_ns.class_('BinarySensorInitiallyOff', BinarySensor) BinarySensorPtr = BinarySensor.operator('ptr') # Triggers diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 577e87258c..f91c93c424 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -67,7 +67,7 @@ class BinarySensor : public Nameable { void send_state_internal(bool state, bool is_initial); /// Return whether this binary sensor has outputted a state. - bool has_state() const; + virtual bool has_state() const; virtual bool is_status_binary_sensor() const; @@ -86,5 +86,10 @@ class BinarySensor : public Nameable { Deduplicator publish_dedup_; }; +class BinarySensorInitiallyOff : public BinarySensor { + public: + bool has_state() const override { return true; } +}; + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 9d7ec83bc7..e721db7dcd 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -9,7 +9,7 @@ namespace esphome { namespace ble_presence { -class BLEPresenceDevice : public binary_sensor::BinarySensor, +class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 92b41a88af..bd37e241e9 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -394,7 +394,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { bool wait_for_ack_{true}; }; -class NextionTouchComponent : public binary_sensor::BinarySensor { +class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { public: void set_page_id(uint8_t page_id) { page_id_ = page_id; } void set_component_id(uint8_t component_id) { component_id_ = component_id; } diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index a67b6e7ce8..13df400754 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -28,7 +28,7 @@ class RDM6300Component : public Component, public uart::UARTDevice { uint32_t last_id_{0}; }; -class RDM6300BinarySensor : public binary_sensor::BinarySensor { +class RDM6300BinarySensor : public binary_sensor::BinarySensorInitiallyOff { public: void set_id(uint32_t id) { id_ = id; } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 6035e2fd57..36be25add7 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -267,11 +267,11 @@ class RemoteReceiverBase : public RemoteComponentBase { uint8_t tolerance_{25}; }; -class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensor, +class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitiallyOff, public Component, public RemoteReceiverListener { public: - explicit RemoteReceiverBinarySensorBase() : BinarySensor() {} + explicit RemoteReceiverBinarySensorBase() : BinarySensorInitiallyOff() {} void dump_config() override; virtual bool matches(RemoteReceiveData src) = 0; bool on_receive(RemoteReceiveData src) override { diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 7fbeb8c171..90ac1faad7 100644 --- a/esphome/components/status/status_binary_sensor.cpp +++ b/esphome/components/status/status_binary_sensor.cpp @@ -30,7 +30,7 @@ void StatusBinarySensor::loop() { this->publish_state(status); } -void StatusBinarySensor::setup() { this->publish_state(false); } +void StatusBinarySensor::setup() { this->publish_initial_state(false); } void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); } } // namespace status From 1df9ae53f8f6059e155cbbe9f3f0a2a8d24ab0b3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 1 Nov 2019 11:50:02 +0100 Subject: [PATCH 050/412] Add ci-reporter bot --- .github/ci-reporter.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ci-reporter.yml diff --git a/.github/ci-reporter.yml b/.github/ci-reporter.yml new file mode 100644 index 0000000000..243e671532 --- /dev/null +++ b/.github/ci-reporter.yml @@ -0,0 +1,8 @@ +# Set to false to create a new comment instead of updating the app's first one +updateComment: true + +# Use a custom string, or set to false to disable +before: "✨ Good work on this PR so far! ✨ Unfortunately, the [ build]() is failing as of . Here's the output:" + +# Use a custom string, or set to false to disable +after: "Thanks for contributing to this project!" From 442030b6ca9e54df2fc2d9759cff65ac0c4d39f9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 1 Nov 2019 11:50:12 +0100 Subject: [PATCH 051/412] Add sentiment-bot --- .github/config.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/config.yml diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000000..f2b357cc95 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,11 @@ +# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot + +# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic +# Anything higher than this threshold will be marked as toxic and commented on +sentimentBotToxicityThreshold: .8 + +# *Required* Comment to reply with +sentimentBotReplyComment: > + Please be sure to review the code of conduct and be respectful of other users. + +# Note: the bot will only work if your repository has a Code of Conduct From 042ccde44122b3501aea352d00a91a186322a5fa Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 1 Nov 2019 11:50:26 +0100 Subject: [PATCH 052/412] Add lock probot --- .github/lock.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/lock.yml diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..0680577b2e --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,36 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 7 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: + - keep-open + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: false + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo From 09fd505f08599d97b1c5ad3760fd335108381fb2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 1 Nov 2019 11:50:36 +0100 Subject: [PATCH 053/412] Add stale probot --- .github/stale.yml | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..225c029bd9 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,59 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - not-stale + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +only: pulls + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed From 90f909d2ea1a01db1cd514adb2477c5215148272 Mon Sep 17 00:00:00 2001 From: Alexander Leisentritt Date: Sat, 2 Nov 2019 18:55:10 +0100 Subject: [PATCH 054/412] refactored xiaomi ble data parsing (#823) --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 48 +++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 1172f6ee0a..e6884e5ea6 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -91,17 +91,6 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d return {}; } - uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; - - const uint8_t raw_type = raw[raw_offset]; - const uint8_t data_length = raw[raw_offset + 2]; - const uint8_t *data = &raw[raw_offset + 3]; - const uint8_t expected_length = data_length + raw_offset + 3; - const uint8_t actual_length = device.get_service_data().size(); - if (expected_length != actual_length) { - // ESP_LOGV(TAG, "Xiaomi %s data length mismatch (%u != %d)", type, expected_length, actual_length); - return {}; - } XiaomiParseResult result; result.type = XiaomiParseResult::TYPE_HHCCJCY01; if (is_lywsdcgq) { @@ -111,7 +100,42 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d } else if (is_cgg1) { result.type = XiaomiParseResult::TYPE_CGG1; } - bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); + + uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; + + // Data point specs + // Byte 0: type + // Byte 1: fixed 0x10 + // Byte 2: length + // Byte 3..3+len-1: data point value + + const uint8_t *raw_data = &raw[raw_offset]; + uint8_t data_offset = 0; + uint8_t data_length = device.get_service_data().size() - raw_offset; + bool success = false; + + while (true) { + if (data_length < 4) + // at least 4 bytes required + // type, fixed 0x10, length, 1 byte value + break; + + const uint8_t datapoint_type = raw_data[data_offset + 0]; + const uint8_t datapoint_length = raw_data[data_offset + 2]; + + if (data_length < 3 + datapoint_length) + // 3 fixed bytes plus value length + break; + + const uint8_t *datapoint_data = &raw_data[data_offset + 3]; + + if (parse_xiaomi_data_byte(datapoint_type, datapoint_data, datapoint_length, result)) + success = true; + + data_length -= data_offset + 3 + datapoint_length; + data_offset += 3 + datapoint_length; + } + if (!success) return {}; return result; From be6b4ee47fd812fdbd137c4e32b0c780ae349f33 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 19:35:37 +0100 Subject: [PATCH 055/412] Fix wizard mkdir (#824) * Fix CLI wizard mkdir_p with empty path Fixes https://github.com/esphome/issues/issues/796 * Cleanup * Lint --- esphome/helpers.py | 3 +++ esphome/storage_json.py | 3 +-- esphome/writer.py | 5 +---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 6fd1fa2ad7..48607dbff5 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -80,6 +80,9 @@ def run_system_command(*args): def mkdir_p(path): + if not path: + # Empty path - means create current dir + return try: os.makedirs(path) except OSError as err: diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 0305b59ef5..e1b4070d3b 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -7,7 +7,7 @@ import os from esphome import const from esphome.core import CORE -from esphome.helpers import mkdir_p, write_file_if_changed +from esphome.helpers import write_file_if_changed # pylint: disable=unused-import, wrong-import-order from esphome.core import CoreType # noqa @@ -88,7 +88,6 @@ class StorageJSON(object): return json.dumps(self.as_dict(), indent=2) + u'\n' def save(self, path): - mkdir_p(os.path.dirname(path)) write_file_if_changed(path, self.to_json()) @staticmethod diff --git a/esphome/writer.py b/esphome/writer.py index 4c2e03569f..1961af5a15 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -286,8 +286,6 @@ or use the custom_components folder. def copy_src_tree(): - import shutil - source_files = {} for _, component, _ in iter_components(CORE.config): source_files.update(component.source_files) @@ -326,8 +324,7 @@ def copy_src_tree(): # Now copy new files for target, src_path in source_files_copy.items(): dst_path = CORE.relative_src_path(*target.split('/')) - mkdir_p(os.path.dirname(dst_path)) - shutil.copy(src_path, dst_path) + copy_file_if_changed(src_path, dst_path) # Finally copy defines write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'), From 66aa02fc34b44df567198d2c89a4879b48003822 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 19:35:45 +0100 Subject: [PATCH 056/412] Move native API enums to new namespace (#825) Fixes https://github.com/esphome/issues/issues/801 --- esphome/components/api/api_connection.cpp | 19 +-- esphome/components/api/api_pb2.cpp | 154 +++++++++++----------- esphome/components/api/api_pb2.h | 67 +++++----- esphome/components/api/user_services.cpp | 20 +-- esphome/components/api/user_services.h | 4 +- script/api_protobuf/api_protobuf.py | 12 +- 6 files changed, 145 insertions(+), 131 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4a595a3f99..e786fe61be 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -185,11 +185,12 @@ bool APIConnection::send_cover_state(cover::Cover *cover) { auto traits = cover->get_traits(); CoverStateResponse resp{}; resp.key = cover->get_object_id_hash(); - resp.legacy_state = (cover->position == cover::COVER_OPEN) ? LEGACY_COVER_STATE_OPEN : LEGACY_COVER_STATE_CLOSED; + resp.legacy_state = + (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED; resp.position = cover->position; if (traits.get_supports_tilt()) resp.tilt = cover->tilt; - resp.current_operation = static_cast(cover->current_operation); + resp.current_operation = static_cast(cover->current_operation); return this->send_cover_state_response(resp); } bool APIConnection::send_cover_info(cover::Cover *cover) { @@ -213,13 +214,13 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { auto call = cover->make_call(); if (msg.has_legacy_command) { switch (msg.legacy_command) { - case LEGACY_COVER_COMMAND_OPEN: + case enums::LEGACY_COVER_COMMAND_OPEN: call.set_command_open(); break; - case LEGACY_COVER_COMMAND_CLOSE: + case enums::LEGACY_COVER_COMMAND_CLOSE: call.set_command_close(); break; - case LEGACY_COVER_COMMAND_STOP: + case enums::LEGACY_COVER_COMMAND_STOP: call.set_command_stop(); break; } @@ -246,7 +247,7 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { if (traits.supports_oscillation()) resp.oscillating = fan->oscillating; if (traits.supports_speed()) - resp.speed = static_cast(fan->speed); + resp.speed = static_cast(fan->speed); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -441,8 +442,8 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { auto traits = climate->get_traits(); ClimateStateResponse resp{}; resp.key = climate->get_object_id_hash(); - resp.mode = static_cast(climate->mode); - resp.action = static_cast(climate->action); + resp.mode = static_cast(climate->mode); + resp.action = static_cast(climate->action); if (traits.get_supports_current_temperature()) resp.current_temperature = climate->current_temperature; if (traits.get_supports_two_point_target_temperature()) { @@ -467,7 +468,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT}) { if (traits.supports_mode(mode)) - msg.supported_modes.push_back(static_cast(mode)); + msg.supported_modes.push_back(static_cast(mode)); } msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3f635d1cdb..7f45d38f1b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -4,115 +4,115 @@ namespace esphome { namespace api { -template<> const char *proto_enum_to_string(EnumLegacyCoverState value) { +template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { switch (value) { - case LEGACY_COVER_STATE_OPEN: + case enums::LEGACY_COVER_STATE_OPEN: return "LEGACY_COVER_STATE_OPEN"; - case LEGACY_COVER_STATE_CLOSED: + case enums::LEGACY_COVER_STATE_CLOSED: return "LEGACY_COVER_STATE_CLOSED"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumCoverOperation value) { +template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { - case COVER_OPERATION_IDLE: + case enums::COVER_OPERATION_IDLE: return "COVER_OPERATION_IDLE"; - case COVER_OPERATION_IS_OPENING: + case enums::COVER_OPERATION_IS_OPENING: return "COVER_OPERATION_IS_OPENING"; - case COVER_OPERATION_IS_CLOSING: + case enums::COVER_OPERATION_IS_CLOSING: return "COVER_OPERATION_IS_CLOSING"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumLegacyCoverCommand value) { +template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) { switch (value) { - case LEGACY_COVER_COMMAND_OPEN: + case enums::LEGACY_COVER_COMMAND_OPEN: return "LEGACY_COVER_COMMAND_OPEN"; - case LEGACY_COVER_COMMAND_CLOSE: + case enums::LEGACY_COVER_COMMAND_CLOSE: return "LEGACY_COVER_COMMAND_CLOSE"; - case LEGACY_COVER_COMMAND_STOP: + case enums::LEGACY_COVER_COMMAND_STOP: return "LEGACY_COVER_COMMAND_STOP"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumFanSpeed value) { +template<> const char *proto_enum_to_string(enums::FanSpeed value) { switch (value) { - case FAN_SPEED_LOW: + case enums::FAN_SPEED_LOW: return "FAN_SPEED_LOW"; - case FAN_SPEED_MEDIUM: + case enums::FAN_SPEED_MEDIUM: return "FAN_SPEED_MEDIUM"; - case FAN_SPEED_HIGH: + case enums::FAN_SPEED_HIGH: return "FAN_SPEED_HIGH"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumLogLevel value) { +template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { - case LOG_LEVEL_NONE: + case enums::LOG_LEVEL_NONE: return "LOG_LEVEL_NONE"; - case LOG_LEVEL_ERROR: + case enums::LOG_LEVEL_ERROR: return "LOG_LEVEL_ERROR"; - case LOG_LEVEL_WARN: + case enums::LOG_LEVEL_WARN: return "LOG_LEVEL_WARN"; - case LOG_LEVEL_INFO: + case enums::LOG_LEVEL_INFO: return "LOG_LEVEL_INFO"; - case LOG_LEVEL_DEBUG: + case enums::LOG_LEVEL_DEBUG: return "LOG_LEVEL_DEBUG"; - case LOG_LEVEL_VERBOSE: + case enums::LOG_LEVEL_VERBOSE: return "LOG_LEVEL_VERBOSE"; - case LOG_LEVEL_VERY_VERBOSE: + case enums::LOG_LEVEL_VERY_VERBOSE: return "LOG_LEVEL_VERY_VERBOSE"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumServiceArgType value) { +template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { - case SERVICE_ARG_TYPE_BOOL: + case enums::SERVICE_ARG_TYPE_BOOL: return "SERVICE_ARG_TYPE_BOOL"; - case SERVICE_ARG_TYPE_INT: + case enums::SERVICE_ARG_TYPE_INT: return "SERVICE_ARG_TYPE_INT"; - case SERVICE_ARG_TYPE_FLOAT: + case enums::SERVICE_ARG_TYPE_FLOAT: return "SERVICE_ARG_TYPE_FLOAT"; - case SERVICE_ARG_TYPE_STRING: + case enums::SERVICE_ARG_TYPE_STRING: return "SERVICE_ARG_TYPE_STRING"; - case SERVICE_ARG_TYPE_BOOL_ARRAY: + case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: return "SERVICE_ARG_TYPE_BOOL_ARRAY"; - case SERVICE_ARG_TYPE_INT_ARRAY: + case enums::SERVICE_ARG_TYPE_INT_ARRAY: return "SERVICE_ARG_TYPE_INT_ARRAY"; - case SERVICE_ARG_TYPE_FLOAT_ARRAY: + case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; - case SERVICE_ARG_TYPE_STRING_ARRAY: + case enums::SERVICE_ARG_TYPE_STRING_ARRAY: return "SERVICE_ARG_TYPE_STRING_ARRAY"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumClimateMode value) { +template<> const char *proto_enum_to_string(enums::ClimateMode value) { switch (value) { - case CLIMATE_MODE_OFF: + case enums::CLIMATE_MODE_OFF: return "CLIMATE_MODE_OFF"; - case CLIMATE_MODE_AUTO: + case enums::CLIMATE_MODE_AUTO: return "CLIMATE_MODE_AUTO"; - case CLIMATE_MODE_COOL: + case enums::CLIMATE_MODE_COOL: return "CLIMATE_MODE_COOL"; - case CLIMATE_MODE_HEAT: + case enums::CLIMATE_MODE_HEAT: return "CLIMATE_MODE_HEAT"; default: return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(EnumClimateAction value) { +template<> const char *proto_enum_to_string(enums::ClimateAction value) { switch (value) { - case CLIMATE_ACTION_OFF: + case enums::CLIMATE_ACTION_OFF: return "CLIMATE_ACTION_OFF"; - case CLIMATE_ACTION_COOLING: + case enums::CLIMATE_ACTION_COOLING: return "CLIMATE_ACTION_COOLING"; - case CLIMATE_ACTION_HEATING: + case enums::CLIMATE_ACTION_HEATING: return "CLIMATE_ACTION_HEATING"; default: return "UNKNOWN"; @@ -535,11 +535,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->legacy_state = value.as_enum(); + this->legacy_state = value.as_enum(); return true; } case 5: { - this->current_operation = value.as_enum(); + this->current_operation = value.as_enum(); return true; } default: @@ -566,10 +566,10 @@ bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_enum(2, this->legacy_state); + buffer.encode_enum(2, this->legacy_state); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); - buffer.encode_enum(5, this->current_operation); + buffer.encode_enum(5, this->current_operation); } void CoverStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -580,7 +580,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_state: "); - out.append(proto_enum_to_string(this->legacy_state)); + out.append(proto_enum_to_string(this->legacy_state)); out.append("\n"); out.append(" position: "); @@ -594,7 +594,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); + out.append(proto_enum_to_string(this->current_operation)); out.append("\n"); out.append("}"); } @@ -605,7 +605,7 @@ bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 3: { - this->legacy_command = value.as_enum(); + this->legacy_command = value.as_enum(); return true; } case 4: { @@ -645,7 +645,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->has_legacy_command); - buffer.encode_enum(3, this->legacy_command); + buffer.encode_enum(3, this->legacy_command); buffer.encode_bool(4, this->has_position); buffer.encode_float(5, this->position); buffer.encode_bool(6, this->has_tilt); @@ -665,7 +665,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_command: "); - out.append(proto_enum_to_string(this->legacy_command)); + out.append(proto_enum_to_string(this->legacy_command)); out.append("\n"); out.append(" has_position: "); @@ -781,7 +781,7 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 4: { - this->speed = value.as_enum(); + this->speed = value.as_enum(); return true; } default: @@ -802,7 +802,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); - buffer.encode_enum(4, this->speed); + buffer.encode_enum(4, this->speed); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -821,7 +821,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); + out.append(proto_enum_to_string(this->speed)); out.append("\n"); out.append("}"); } @@ -840,7 +840,7 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 5: { - this->speed = value.as_enum(); + this->speed = value.as_enum(); return true; } case 6: { @@ -870,7 +870,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->has_state); buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_speed); - buffer.encode_enum(5, this->speed); + buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); } @@ -895,7 +895,7 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); + out.append(proto_enum_to_string(this->speed)); out.append("\n"); out.append(" has_oscillating: "); @@ -1740,7 +1740,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { - this->level = value.as_enum(); + this->level = value.as_enum(); return true; } case 2: { @@ -1752,14 +1752,14 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } } void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->level); + buffer.encode_enum(1, this->level); buffer.encode_bool(2, this->dump_config); } void SubscribeLogsRequest::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsRequest {\n"); out.append(" level: "); - out.append(proto_enum_to_string(this->level)); + out.append(proto_enum_to_string(this->level)); out.append("\n"); out.append(" dump_config: "); @@ -1770,7 +1770,7 @@ void SubscribeLogsRequest::dump_to(std::string &out) const { bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { - this->level = value.as_enum(); + this->level = value.as_enum(); return true; } case 4: { @@ -1796,7 +1796,7 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite } } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->level); + buffer.encode_enum(1, this->level); buffer.encode_string(2, this->tag); buffer.encode_string(3, this->message); buffer.encode_bool(4, this->send_failed); @@ -1805,7 +1805,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsResponse {\n"); out.append(" level: "); - out.append(proto_enum_to_string(this->level)); + out.append(proto_enum_to_string(this->level)); out.append("\n"); out.append(" tag: "); @@ -2010,7 +2010,7 @@ void GetTimeResponse::dump_to(std::string &out) const { bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->type = value.as_enum(); + this->type = value.as_enum(); return true; } default: @@ -2029,7 +2029,7 @@ bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthD } void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); - buffer.encode_enum(2, this->type); + buffer.encode_enum(2, this->type); } void ListEntitiesServicesArgument::dump_to(std::string &out) const { char buffer[64]; @@ -2039,7 +2039,7 @@ void ListEntitiesServicesArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" type: "); - out.append(proto_enum_to_string(this->type)); + out.append(proto_enum_to_string(this->type)); out.append("\n"); out.append("}"); } @@ -2408,7 +2408,7 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v return true; } case 7: { - this->supported_modes.push_back(value.as_enum()); + this->supported_modes.push_back(value.as_enum()); return true; } case 11: { @@ -2471,7 +2471,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (auto &it : this->supported_modes) { - buffer.encode_enum(7, it, true); + buffer.encode_enum(7, it, true); } buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); @@ -2509,7 +2509,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { for (const auto &it : this->supported_modes) { out.append(" supported_modes: "); - out.append(proto_enum_to_string(it)); + out.append(proto_enum_to_string(it)); out.append("\n"); } @@ -2540,7 +2540,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->mode = value.as_enum(); + this->mode = value.as_enum(); return true; } case 7: { @@ -2548,7 +2548,7 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 8: { - this->action = value.as_enum(); + this->action = value.as_enum(); return true; } default: @@ -2583,13 +2583,13 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_enum(2, this->mode); + buffer.encode_enum(2, this->mode); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); - buffer.encode_enum(8, this->action); + buffer.encode_enum(8, this->action); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2600,7 +2600,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); + out.append(proto_enum_to_string(this->mode)); out.append("\n"); out.append(" current_temperature: "); @@ -2628,7 +2628,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" action: "); - out.append(proto_enum_to_string(this->action)); + out.append(proto_enum_to_string(this->action)); out.append("\n"); out.append("}"); } @@ -2639,7 +2639,7 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) return true; } case 3: { - this->mode = value.as_enum(); + this->mode = value.as_enum(); return true; } case 4: { @@ -2691,7 +2691,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->has_mode); - buffer.encode_enum(3, this->mode); + buffer.encode_enum(3, this->mode); buffer.encode_bool(4, this->has_target_temperature); buffer.encode_float(5, this->target_temperature); buffer.encode_bool(6, this->has_target_temperature_low); @@ -2714,7 +2714,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); + out.append(proto_enum_to_string(this->mode)); out.append("\n"); out.append(" has_target_temperature: "); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2e685959bf..eb6a15afd4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -5,26 +5,28 @@ namespace esphome { namespace api { -enum EnumLegacyCoverState : uint32_t { +namespace enums { + +enum LegacyCoverState : uint32_t { LEGACY_COVER_STATE_OPEN = 0, LEGACY_COVER_STATE_CLOSED = 1, }; -enum EnumCoverOperation : uint32_t { +enum CoverOperation : uint32_t { COVER_OPERATION_IDLE = 0, COVER_OPERATION_IS_OPENING = 1, COVER_OPERATION_IS_CLOSING = 2, }; -enum EnumLegacyCoverCommand : uint32_t { +enum LegacyCoverCommand : uint32_t { LEGACY_COVER_COMMAND_OPEN = 0, LEGACY_COVER_COMMAND_CLOSE = 1, LEGACY_COVER_COMMAND_STOP = 2, }; -enum EnumFanSpeed : uint32_t { +enum FanSpeed : uint32_t { FAN_SPEED_LOW = 0, FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; -enum EnumLogLevel : uint32_t { +enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, LOG_LEVEL_WARN = 2, @@ -33,7 +35,7 @@ enum EnumLogLevel : uint32_t { LOG_LEVEL_VERBOSE = 5, LOG_LEVEL_VERY_VERBOSE = 6, }; -enum EnumServiceArgType : uint32_t { +enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, SERVICE_ARG_TYPE_FLOAT = 2, @@ -43,17 +45,20 @@ enum EnumServiceArgType : uint32_t { SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, SERVICE_ARG_TYPE_STRING_ARRAY = 7, }; -enum EnumClimateMode : uint32_t { +enum ClimateMode : uint32_t { CLIMATE_MODE_OFF = 0, CLIMATE_MODE_AUTO = 1, CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, }; -enum EnumClimateAction : uint32_t { +enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, CLIMATE_ACTION_COOLING = 2, CLIMATE_ACTION_HEATING = 3, }; + +} // namespace enums + class HelloRequest : public ProtoMessage { public: std::string client_info{}; // NOLINT @@ -212,11 +217,11 @@ class ListEntitiesCoverResponse : public ProtoMessage { }; class CoverStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - EnumLegacyCoverState legacy_state{}; // NOLINT - float position{0.0f}; // NOLINT - float tilt{0.0f}; // NOLINT - EnumCoverOperation current_operation{}; // NOLINT + uint32_t key{0}; // NOLINT + enums::LegacyCoverState legacy_state{}; // NOLINT + float position{0.0f}; // NOLINT + float tilt{0.0f}; // NOLINT + enums::CoverOperation current_operation{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -226,14 +231,14 @@ class CoverStateResponse : public ProtoMessage { }; class CoverCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_legacy_command{false}; // NOLINT - EnumLegacyCoverCommand legacy_command{}; // NOLINT - bool has_position{false}; // NOLINT - float position{0.0f}; // NOLINT - bool has_tilt{false}; // NOLINT - float tilt{0.0f}; // NOLINT - bool stop{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool has_legacy_command{false}; // NOLINT + enums::LegacyCoverCommand legacy_command{}; // NOLINT + bool has_position{false}; // NOLINT + float position{0.0f}; // NOLINT + bool has_tilt{false}; // NOLINT + float tilt{0.0f}; // NOLINT + bool stop{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -262,7 +267,7 @@ class FanStateResponse : public ProtoMessage { uint32_t key{0}; // NOLINT bool state{false}; // NOLINT bool oscillating{false}; // NOLINT - EnumFanSpeed speed{}; // NOLINT + enums::FanSpeed speed{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -276,7 +281,7 @@ class FanCommandRequest : public ProtoMessage { bool has_state{false}; // NOLINT bool state{false}; // NOLINT bool has_speed{false}; // NOLINT - EnumFanSpeed speed{}; // NOLINT + enums::FanSpeed speed{}; // NOLINT bool has_oscillating{false}; // NOLINT bool oscillating{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; @@ -448,7 +453,7 @@ class TextSensorStateResponse : public ProtoMessage { }; class SubscribeLogsRequest : public ProtoMessage { public: - EnumLogLevel level{}; // NOLINT + enums::LogLevel level{}; // NOLINT bool dump_config{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -458,7 +463,7 @@ class SubscribeLogsRequest : public ProtoMessage { }; class SubscribeLogsResponse : public ProtoMessage { public: - EnumLogLevel level{}; // NOLINT + enums::LogLevel level{}; // NOLINT std::string tag{}; // NOLINT std::string message{}; // NOLINT bool send_failed{false}; // NOLINT @@ -544,8 +549,8 @@ class GetTimeResponse : public ProtoMessage { }; class ListEntitiesServicesArgument : public ProtoMessage { public: - std::string name{}; // NOLINT - EnumServiceArgType type{}; // NOLINT + std::string name{}; // NOLINT + enums::ServiceArgType type{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -639,7 +644,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::string unique_id{}; // NOLINT bool supports_current_temperature{false}; // NOLINT bool supports_two_point_target_temperature{false}; // NOLINT - std::vector supported_modes{}; // NOLINT + std::vector supported_modes{}; // NOLINT float visual_min_temperature{0.0f}; // NOLINT float visual_max_temperature{0.0f}; // NOLINT float visual_temperature_step{0.0f}; // NOLINT @@ -656,13 +661,13 @@ class ListEntitiesClimateResponse : public ProtoMessage { class ClimateStateResponse : public ProtoMessage { public: uint32_t key{0}; // NOLINT - EnumClimateMode mode{}; // NOLINT + enums::ClimateMode mode{}; // NOLINT float current_temperature{0.0f}; // NOLINT float target_temperature{0.0f}; // NOLINT float target_temperature_low{0.0f}; // NOLINT float target_temperature_high{0.0f}; // NOLINT bool away{false}; // NOLINT - EnumClimateAction action{}; // NOLINT + enums::ClimateAction action{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -674,7 +679,7 @@ class ClimateCommandRequest : public ProtoMessage { public: uint32_t key{0}; // NOLINT bool has_mode{false}; // NOLINT - EnumClimateMode mode{}; // NOLINT + enums::ClimateMode mode{}; // NOLINT bool has_target_temperature{false}; // NOLINT float target_temperature{0.0f}; // NOLINT bool has_target_temperature_low{false}; // NOLINT diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 0667d26ff6..39e42bcc02 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -25,14 +25,18 @@ template<> std::vector get_execute_arg_value EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_BOOL; } -template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_INT; } -template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_FLOAT; } -template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_STRING; } -template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_BOOL_ARRAY; } -template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_INT_ARRAY; } -template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_FLOAT_ARRAY; } -template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_STRING_ARRAY; } +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_BOOL; } +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } +template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } +template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; } +template<> enums::ServiceArgType to_service_arg_type>() { + return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; +} +template<> enums::ServiceArgType to_service_arg_type>() { + return enums::SERVICE_ARG_TYPE_STRING_ARRAY; +} } // namespace api } // namespace esphome diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 3b99d426a9..3094ba397c 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -16,7 +16,7 @@ class UserServiceDescriptor { template T get_execute_arg_value(const ExecuteServiceArgument &arg); -template EnumServiceArgType to_service_arg_type(); +template enums::ServiceArgType to_service_arg_type(); template class UserServiceBase : public UserServiceDescriptor { public: @@ -29,7 +29,7 @@ template class UserServiceBase : public UserServiceDescriptor { ListEntitiesServicesResponse msg; msg.name = this->name_; msg.key = this->key_; - std::array arg_types = {to_service_arg_type()...}; + std::array arg_types = {to_service_arg_type()...}; for (int i = 0; i < sizeof...(Ts); i++) { ListEntitiesServicesArgument arg; arg.type = arg_types[i]; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 6357ae38ed..a8f81c9daf 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -344,7 +344,7 @@ class UInt32Type(TypeInfo): class EnumType(TypeInfo): @property def cpp_type(self): - return "Enum" + self._field.type_name[1:] + return f'enums::{self._field.type_name[1:]}' @property def decode_varint(self): @@ -497,17 +497,17 @@ class RepeatedTypeInfo(TypeInfo): def build_enum_type(desc): - name = "Enum" + desc.name + name = desc.name out = f"enum {name} : uint32_t {{\n" for v in desc.value: out += f' {v.name} = {v.number},\n' out += '};\n' cpp = f"template<>\n" - cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n" + cpp += f"const char *proto_enum_to_string(enums::{name} value) {{\n" cpp += f" switch (value) {{\n" for v in desc.value: - cpp += f' case {v.name}: return "{v.name}";\n' + cpp += f' case enums::{v.name}: return "{v.name}";\n' cpp += f' default: return "UNKNOWN";\n' cpp += f' }}\n' cpp += f'}}\n' @@ -636,11 +636,15 @@ namespace api { ''' +content += 'namespace enums {\n\n' + for enum in file.enum_type: s, c = build_enum_type(enum) content += s cpp += c +content += '\n} // namespace enums\n\n' + mt = file.message_type for m in mt: From 65d3dc9cb82c43cd5b1b7598ccfc00629de72321 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 19:35:55 +0100 Subject: [PATCH 057/412] Fix update-all input in dashboard (#826) Fixes https://github.com/esphome/issues/issues/798 --- esphome/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index a78413ff04..6d3705c87e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -371,7 +371,8 @@ def command_update_all(args): print("Updating {}".format(color('cyan', f))) print('-' * twidth) print() - rc = run_external_process('esphome', '--dashboard', f, 'run', '--no-logs') + rc = run_external_process('esphome', '--dashboard', f, 'run', '--no-logs', '--upload-port', + 'OTA') if rc == 0: print_bar("[{}] {}".format(color('bold_green', 'SUCCESS'), f)) success[f] = True From 75275c4e934746f33f81f6cc4aea251d04e88783 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 20:31:39 +0100 Subject: [PATCH 058/412] Remove PCF8574 input_pullup mode and cleanup (#828) Fixes https://github.com/esphome/issues/issues/755 Closes https://github.com/esphome/esphome/pull/822 Fixes https://github.com/esphome/issues/issues/667 Closes https://github.com/esphome/esphome/pull/808 Co-Authored-By: Amish Vishwakarma Co-Authored-By: S-Przybylski --- esphome/components/pcf8574/__init__.py | 13 ++++-- esphome/components/pcf8574/pcf8574.cpp | 59 ++++++++++++-------------- esphome/components/pcf8574/pcf8574.h | 8 ++-- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index ad74ac70a9..daf367c089 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -11,7 +11,6 @@ pcf8574_ns = cg.esphome_ns.namespace('pcf8574') PCF8574GPIOMode = pcf8574_ns.enum('PCF8574GPIOMode') PCF8674_GPIO_MODES = { 'INPUT': PCF8574GPIOMode.PCF8574_INPUT, - 'INPUT_PULLUP': PCF8574GPIOMode.PCF8574_INPUT_PULLUP, 'OUTPUT': PCF8574GPIOMode.PCF8574_OUTPUT, } @@ -33,16 +32,24 @@ def to_code(config): cg.add(var.set_pcf8575(config[CONF_PCF8575])) +def validate_pcf8574_gpio_mode(value): + value = cv.string(value) + if value.upper() == 'INPUT_PULLUP': + raise cv.Invalid("INPUT_PULLUP mode has been removed in 1.14 and been combined into " + "INPUT mode (they were the same thing). Please use INPUT instead.") + return cv.enum(PCF8674_GPIO_MODES, upper=True)(value) + + PCF8574_OUTPUT_PIN_SCHEMA = cv.Schema({ cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(PCF8674_GPIO_MODES, upper=True), + cv.Optional(CONF_MODE, default="OUTPUT"): validate_pcf8574_gpio_mode, cv.Optional(CONF_INVERTED, default=False): cv.boolean, }) PCF8574_INPUT_PIN_SCHEMA = cv.Schema({ cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): cv.enum(PCF8674_GPIO_MODES, upper=True), + cv.Optional(CONF_MODE, default="INPUT"): validate_pcf8574_gpio_mode, cv.Optional(CONF_INVERTED, default=False): cv.boolean, }) diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 50922e2f48..6cadf565de 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -31,9 +31,9 @@ bool PCF8574Component::digital_read(uint8_t pin) { } void PCF8574Component::digital_write(uint8_t pin, bool value) { if (value) { - this->port_mask_ |= (1 << pin); + this->output_mask_ |= (1 << pin); } else { - this->port_mask_ &= ~(1 << pin); + this->output_mask_ &= ~(1 << pin); } this->write_gpio_(); @@ -41,16 +41,14 @@ void PCF8574Component::digital_write(uint8_t pin, bool value) { void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) { switch (mode) { case PCF8574_INPUT: - this->ddr_mask_ &= ~(1 << pin); - this->port_mask_ &= ~(1 << pin); - break; - case PCF8574_INPUT_PULLUP: - this->ddr_mask_ &= ~(1 << pin); - this->port_mask_ |= (1 << pin); + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + // Write GPIO to enable input mode + this->write_gpio_(); break; case PCF8574_OUTPUT: - this->ddr_mask_ |= (1 << pin); - this->port_mask_ &= ~(1 << pin); + // Set mode mask bit + this->mode_mask_ |= 1 << pin; break; default: break; @@ -59,21 +57,20 @@ void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) { bool PCF8574Component::read_gpio_() { if (this->is_failed()) return false; - + bool success; + uint8_t data[2]; if (this->pcf8575_) { - if (!this->parent_->raw_receive_16(this->address_, &this->input_mask_, 1)) { - this->status_set_warning(); - return false; - } + success = this->read_bytes_raw(data, 2); + this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); } else { - uint8_t data; - if (!this->parent_->raw_receive(this->address_, &data, 1)) { - this->status_set_warning(); - return false; - } - this->input_mask_ = data; + success = this->read_bytes_raw(data, 1); + this->input_mask_ = data[0]; } + if (!success) { + this->status_set_warning(); + return false; + } this->status_clear_warning(); return true; } @@ -81,20 +78,20 @@ bool PCF8574Component::write_gpio_() { if (this->is_failed()) return false; - uint16_t value = (this->input_mask_ & ~this->ddr_mask_) | this->port_mask_; + uint16_t value = 0; + // Pins in OUTPUT mode and where pin is HIGH. + value |= this->mode_mask_ & this->output_mask_; + // Pins in INPUT mode must also be set here + value |= ~this->mode_mask_; - this->parent_->raw_begin_transmission(this->address_); - uint8_t data = value & 0xFF; - this->parent_->raw_write(this->address_, &data, 1); - - if (this->pcf8575_) { - data = (value >> 8) & 0xFF; - this->parent_->raw_write(this->address_, &data, 1); - } - if (!this->parent_->raw_end_transmission(this->address_)) { + uint8_t data[2]; + data[0] = value; + data[1] = value >> 8; + if (!this->write_bytes_raw(data, this->pcf8575_ ? 2 : 1)) { this->status_set_warning(); return false; } + this->status_clear_warning(); return true; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 390031c391..925fa30899 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -10,7 +10,6 @@ namespace pcf8574 { /// Modes for PCF8574 pins enum PCF8574GPIOMode : uint8_t { PCF8574_INPUT = INPUT, - PCF8574_INPUT_PULLUP = INPUT_PULLUP, PCF8574_OUTPUT = OUTPUT, }; @@ -38,9 +37,12 @@ class PCF8574Component : public Component, public i2c::I2CDevice { bool write_gpio_(); - uint16_t ddr_mask_{0x00}; + /// Mask for the pin mode - 1 means output, 0 means input + uint16_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint16_t output_mask_{0x00}; + /// The state read in read_gpio_ - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; - uint16_t port_mask_{0x00}; bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574 }; From dc2279b74fd385a4c07754271e33b0dddf196fe0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 20:56:42 +0100 Subject: [PATCH 059/412] Add servo missing restore option to codegen (#829) See also https://github.com/esphome/issues/issues/609 --- esphome/components/servo/__init__.py | 4 +++- esphome/components/servo/servo.h | 1 + tests/test3.yaml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index 92243ee18d..9b06159c13 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -4,7 +4,7 @@ from esphome import automation from esphome.automation import maybe_simple_id from esphome.components.output import FloatOutput from esphome.const import CONF_ID, CONF_IDLE_LEVEL, CONF_MAX_LEVEL, CONF_MIN_LEVEL, CONF_OUTPUT, \ - CONF_LEVEL + CONF_LEVEL, CONF_RESTORE servo_ns = cg.esphome_ns.namespace('servo') Servo = servo_ns.class_('Servo', cg.Component) @@ -18,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_MIN_LEVEL, default='3%'): cv.percentage, cv.Optional(CONF_IDLE_LEVEL, default='7.5%'): cv.percentage, cv.Optional(CONF_MAX_LEVEL, default='12%'): cv.percentage, + cv.Optional(CONF_RESTORE, default=False): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) @@ -30,6 +31,7 @@ def to_code(config): cg.add(var.set_min_level(config[CONF_MIN_LEVEL])) cg.add(var.set_idle_level(config[CONF_IDLE_LEVEL])) cg.add(var.set_max_level(config[CONF_MAX_LEVEL])) + cg.add(var.set_restore(config[CONF_RESTORE])) @automation.register_action('servo.write', ServoWriteAction, cv.Schema({ diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 3a0993c0c4..19165be23d 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -47,6 +47,7 @@ class Servo : public Component { void set_min_level(float min_level) { min_level_ = min_level; } void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } + void set_restore(bool restore) { restore_ = restore; } protected: void save_level_(float v) { this->rtc_.save(&v); } diff --git a/tests/test3.yaml b/tests/test3.yaml index d1596c8f41..6d70f60764 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -620,6 +620,7 @@ light: servo: id: my_servo output: out + restore: true ttp229_lsf: From 16c2dc2aaf6ff0f5d95833353aafa869c3b120c1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 2 Nov 2019 21:19:15 +0100 Subject: [PATCH 060/412] Fix stack trace decode for latest platformio (#830) --- esphome/platformio_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 5cc4fad998..36e451c21d 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -101,12 +101,14 @@ def run_idedata(config): args = ['-t', 'idedata'] stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) stdout = decode_text(stdout) - match = re.search(r'{.*}', stdout) + match = re.search(r'{\s*".*}', stdout) if match is None: + _LOGGER.debug("Could not match IDEData for %s", stdout) return IDEData(None) try: return IDEData(json.loads(match.group())) except ValueError: + _LOGGER.debug("Could not load IDEData for %s", stdout, exc_info=1) return IDEData(None) @@ -165,11 +167,13 @@ ESP8266_EXCEPTION_CODES = { def _decode_pc(config, addr): idedata = get_idedata(config) if not idedata.addr2line_path or not idedata.firmware_elf_path: + _LOGGER.debug("decode_pc no addr2line") return command = [idedata.addr2line_path, '-pfiaC', '-e', idedata.firmware_elf_path, addr] try: - translation = subprocess.check_output(command).strip() + translation = decode_text(subprocess.check_output(command)).strip() except Exception: # pylint: disable=broad-except + _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return if "?? ??:0" in translation: From 8027facb39841277bd44ddca5497e7c0ab6c0b45 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 3 Nov 2019 00:19:57 +0100 Subject: [PATCH 061/412] Fix weird ESP8266 wifi crashes (#831) * Try fix ESP8266 weird crashes * Only call disconnect if STA is active --- .../wifi/wifi_component_esp8266.cpp | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ed6e616b3d..3363b9e3a9 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -394,24 +394,6 @@ bool WiFiComponent::wifi_sta_pre_setup_() { if (!this->wifi_mode_(true, {})) return false; - // Clear saved STA config - station_config default_config{}; - wifi_station_get_config_default(&default_config); - bool is_zero = default_config.ssid[0] == '\0' && default_config.password[0] == '\0' && default_config.bssid[0] == 0 && - default_config.bssid_set == 0; - if (!is_zero) { - ESP_LOGV(TAG, "Clearing default wifi STA config"); - - memset(&default_config, 0, sizeof(default_config)); - ETS_UART_INTR_DISABLE(); - bool ret = wifi_station_set_config(&default_config); - ETS_UART_INTR_ENABLE(); - - if (!ret) { - ESP_LOGW(TAG, "Clearing default wif STA config failed!"); - } - } - bool ret1, ret2; ETS_UART_INTR_DISABLE(); ret1 = wifi_station_set_auto_connect(0); @@ -496,11 +478,14 @@ bool WiFiComponent::wifi_scan_start_() { return ret; } bool WiFiComponent::wifi_disconnect_() { + bool ret = true; + // Only call disconnect if interface is up + if (wifi_get_opmode() & WIFI_STA) + ret = wifi_station_disconnect(); station_config conf{}; memset(&conf, 0, sizeof(conf)); ETS_UART_INTR_DISABLE(); - wifi_station_set_config(&conf); - bool ret = wifi_station_disconnect(); + wifi_station_set_config_current(&conf); ETS_UART_INTR_ENABLE(); return ret; } From 0cbd3738171111c040b6b3d08a29cc21ee7cc8eb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 21:56:35 +0100 Subject: [PATCH 062/412] ESP8266 remove default opmode check (#835) --- esphome/components/wifi/wifi_component_esp8266.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3363b9e3a9..e218a12907 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -410,19 +410,6 @@ bool WiFiComponent::wifi_sta_pre_setup_() { void WiFiComponent::wifi_pre_setup_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); - // Make sure the default opmode is OFF - uint8_t default_opmode = wifi_get_opmode_default(); - if (default_opmode != 0) { - ESP_LOGV(TAG, "Setting default WiFi Mode to 0 (was %u)", default_opmode); - - ETS_UART_INTR_DISABLE(); - bool ret = wifi_set_opmode(0); - ETS_UART_INTR_ENABLE(); - - if (!ret) { - ESP_LOGW(TAG, "Setting default WiFi mode failed!"); - } - } // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); From 85c46becdfe4b280f8895e896126b5cc4ae66fbc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 22:11:15 +0100 Subject: [PATCH 063/412] WiFi AP apply manual ip settings (#836) --- esphome/components/wifi/__init__.py | 3 ++- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 93f2d59564..818d3c84e0 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -179,7 +179,8 @@ def to_code(config): if CONF_AP in config: conf = config[CONF_AP] - cg.add(var.set_ap(wifi_network(conf, config.get(CONF_MANUAL_IP)))) + ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) + cg.add(var.set_ap(wifi_network(conf, ip_config))) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index e218a12907..ff0dd57ed4 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -566,7 +566,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str()); conf.ssid_len = static_cast(ap.get_ssid().size()); conf.channel = ap.get_channel().value_or(1); - conf.ssid_hidden = 0; + conf.ssid_hidden = ap.get_hidden(); conf.max_connection = 5; conf.beacon_interval = 100; From 3fdb68cba88f13f5b63ed0b0d630467bc6a1d6d6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 22:26:06 +0100 Subject: [PATCH 064/412] Fix ESP32 rotary encoder (#834) * Fix ESP32 rotary encoder Fixes https://github.com/esphome/issues/issues/672 * Update rotary_encoder.cpp * Lint --- esphome/components/rotary_encoder/rotary_encoder.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 949a301661..769d3e1103 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -32,7 +32,13 @@ static const uint16_t STATE_DECREMENT_COUNTER_1 = 0x1000; // Bit 2&3 (0x0C) encodes state S0-S3 // Bit 4 (0x10) encodes clockwise/counter-clockwise rotation -static const uint16_t STATE_LOOKUP_TABLE[32] = { +// Only apply if DRAM_ATTR exists on this platform (exists on ESP32, not on ESP8266) +#ifndef DRAM_ATTR +#define DRAM_ATTR +#endif +// array needs to be placed in .dram1 for ESP32 +// otherwise it will automatically go into flash, and cause cache disabled issues +static const uint16_t DRAM_ATTR STATE_LOOKUP_TABLE[32] = { // act state S0 in CCW direction STATE_CCW | STATE_S0, // 0x00: stay here STATE_CW | STATE_S1 | STATE_INCREMENT_COUNTER_1, // 0x01: goto CW+S1 and increment counter (dir change) From 5c56f15c6774910234dfb1b22493c26ae2d6c910 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 22:27:35 +0100 Subject: [PATCH 065/412] Fix homeassistant.service schema lambda (#833) * Fix homeassistant.service schema lambda Fixes https://github.com/esphome/issues/issues/820 * Improve * Fix --- esphome/components/api/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 43c7d71e74..a0568863ad 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -70,14 +70,14 @@ def to_code(config): cg.add_global(api_ns.using) -KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string)}) +KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(APIServer), cv.Required(CONF_SERVICE): cv.templatable(cv.string), cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema({cv.string: cv.returning_lambda}), }) From d97bc9579891d8e30a04eb24770fcfed45204167 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 22:28:19 +0100 Subject: [PATCH 066/412] Update platformio libraries (#837) * Update platformio libraries * Lint --- esphome/components/async_tcp/__init__.py | 14 ++------------ esphome/components/mqtt/__init__.py | 2 +- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 6 +++--- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 4cbd5358ce..cf9d2f1585 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,23 +1,13 @@ # Dummy integration to allow relying on AsyncTCP import esphome.codegen as cg -from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP32_1_0_1, \ - ARDUINO_VERSION_ESP32_1_0_2 from esphome.core import CORE, coroutine_with_priority @coroutine_with_priority(200.0) def to_code(config): if CORE.is_esp32: - # https://github.com/me-no-dev/AsyncTCP/blob/master/library.json - versions_requiring_older_asynctcp = [ - ARDUINO_VERSION_ESP32_1_0_0, - ARDUINO_VERSION_ESP32_1_0_1, - ARDUINO_VERSION_ESP32_1_0_2, - ] - if CORE.arduino_version in versions_requiring_older_asynctcp: - cg.add_library('AsyncTCP', '1.0.3') - else: - cg.add_library('AsyncTCP', '1.1.1') + # https://github.com/OttoWinter/AsyncTCP/blob/master/library.json + cg.add_library('AsyncTCP-esphome', '1.1.1') elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP cg.add_library('ESPAsyncTCP-esphome', '1.2.2') diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 073bb3cede..20d7f5aafe 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -155,7 +155,7 @@ def to_code(config): yield cg.register_component(var, config) # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library('AsyncMqttClient-esphome', '0.8.3') + cg.add_library('AsyncMqttClient-esphome', '0.8.4') cg.add_define('USE_MQTT') cg.add_global(mqtt_ns.using) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 923a594eb8..d2faaf7162 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -23,4 +23,4 @@ def to_code(config): if CORE.is_esp32: cg.add_library('FS', None) # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library('ESPAsyncWebServer-esphome', '1.2.5') + cg.add_library('ESPAsyncWebServer-esphome', '1.2.6') diff --git a/platformio.ini b/platformio.ini index 19b850c546..4e1ef1d294 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,10 +10,10 @@ include_dir = include [common] lib_deps = - AsyncTCP@1.1.1 - AsyncMqttClient-esphome@0.8.3 + AsyncTCP-esphome@1.1.1 + AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.5 + ESPAsyncWebServer-esphome@1.2.6 FastLED@3.2.9 NeoPixelBus-esphome@2.5.2 ESPAsyncTCP-esphome@1.2.2 From 1ed8e63d599408160446e307c4b4cded9a704c70 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Nov 2019 22:45:31 +0100 Subject: [PATCH 067/412] Remove useless code See also https://github.com/esphome/esphome/pull/801/files#r342813117 --- esphome/components/ina219/ina219.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 8dbf501e0e..1150f7c661 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -134,7 +134,6 @@ void INA219Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication with INA219 failed!"); - this->mark_failed(); return; } LOG_UPDATE_INTERVAL(this); From f94e9b6b1eea43048fcbcdfc4a29211b03b007b2 Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Wed, 6 Nov 2019 05:56:43 -0700 Subject: [PATCH 068/412] Add MAX31865 sensor support, fix MAX31855 sensor (#832) * Add MAX31865 sensor support, fix MAX31855 sensor. # MAX31865 Added support for the MAX31865 RTD-to-Digital Converter to measure PT100 and similar RTDs. Verified with an Adafruit unit (product ID: 3328) and a PT100 probe. # MAX31855 This was setup for incorrect SPI clock polarity and phase, and would return bad data due to a race condition measuring on the wrong edge (verified with Saleae Logic scope). Selecting the correct configuration fixes that problem. Re-wrote the decode off the datasheet to handle error states better (sends NaN as an update on failure to read temperature, which shows the value as Unknown in Home Assistant). Added the *optional* ability to monitor the internal high-precision temperature sensor, which can be nice in some applications. * Tests for MAX31855/MAX38165. * Update style to match project rules. Also fix CONF_REFERENCE_RESISTANCE and CONF_REFERENCE_TEMPERATURE being defined multiple places. Missed this when I added them to const.py. * Update style to match project rules. Pylint line limit 101/100 ("missed it by that much"). Also apparently I can't read and patched the wrong line in max31855.cpp. * Minor string/style cleanup. There was a copy-paste leftover in max31855.cpp and max31865/sensor.py had unnecessary whitespace. * Improve MAX31865 fault detection and logging. Log levels are more in-line with the documented descriptions. Fault detection code is improved. A transient fault between reads is still reported, but now only faults *during* a read cause the sensor to fail and return NAN ("unknown" in Home Assistant). * Update style to match project rules. I just now realized the .clang-format and pylintrc files are included. D'oh! * MAX31855 & MAX31865 code style alignment. @OttoWinter caught some style mismatches, updated to match project better. * Fix a lost '\' in max31865/sensor.py. --- esphome/components/max31855/max31855.cpp | 110 +++++++----- esphome/components/max31855/max31855.h | 7 +- esphome/components/max31855/sensor.py | 7 +- esphome/components/max31865/__init__.py | 0 esphome/components/max31865/max31865.cpp | 214 +++++++++++++++++++++++ esphome/components/max31865/max31865.h | 56 ++++++ esphome/components/max31865/sensor.py | 34 ++++ esphome/components/ntc/sensor.py | 7 +- esphome/const.py | 5 + tests/test1.yaml | 8 + 10 files changed, 397 insertions(+), 51 deletions(-) create mode 100644 esphome/components/max31865/__init__.py create mode 100644 esphome/components/max31865/max31865.cpp create mode 100644 esphome/components/max31865/max31865.h create mode 100644 esphome/components/max31865/sensor.py diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 0462ed4342..88f9e836f9 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -1,10 +1,11 @@ #include "max31855.h" + #include "esphome/core/log.h" namespace esphome { namespace max31855 { -static const char *TAG = "max31855"; +static const char* TAG = "max31855"; void MAX31855Sensor::update() { this->enable(); @@ -22,9 +23,15 @@ void MAX31855Sensor::setup() { this->spi_setup(); } void MAX31855Sensor::dump_config() { - LOG_SENSOR("", "MAX31855", this); + ESP_LOGCONFIG(TAG, "MAX31855:"); LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Thermocouple", this); + if (this->temperature_reference_) { + LOG_SENSOR(" ", "Reference", this->temperature_reference_); + } else { + ESP_LOGCONFIG(TAG, " Reference temperature disabled."); + } } float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { @@ -32,53 +39,68 @@ void MAX31855Sensor::read_data_() { delay(1); uint8_t data[4]; this->read_array(data, 4); - - // val is 14 bits of signed temperature data followed by 2 bits of status flags - int16_t val = data[1] | data[0] << 8; - - // test data from MAX31855 datasheet - // val = 0x6400 // 1600.00°C - // val = 0x3E80 // 1000.00°C - // val = 0x064C // 100.75°C - // val = 0x0190 // 25.00°C - // val = 0x0000 // 0.00°C - // val = 0xFFFC // -0.25°C - // val = 0xFFF0 // -1.00°C - // val = 0xF060 // -250.00°C - this->disable(); - if ((data[3] & 0x01) != 0) { - ESP_LOGW(TAG, "Got thermocouple not connected from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x02) != 0) { - ESP_LOGW(TAG, "Got short circuit to ground from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x04) != 0) { - ESP_LOGW(TAG, "Got short circuit to power from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[1] & 0x01) != 0) { - ESP_LOGW(TAG, "Got faulty reading from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((val & 0x8000) != 0) { - // Negative value, drop the lower 2 bits and explicitly extend sign bits. - val = 0xE000 | ((val >> 2) & 0x1FFF); + const uint32_t mem = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3] << 0; + + // Verify we got data + if (mem != 0 && mem != 0xFFFFFFFF) { + this->status_clear_error(); } else { - // Positive value, just drop the lower 2 bits. - val >>= 2; + ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); + this->publish_state(NAN); + if (this->temperature_reference_) { + this->temperature_reference_->publish_state(NAN); + } + this->status_set_error(); + return; } - float temperature = float(val) / 4.0f; - ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", this->name_.c_str(), temperature); - this->publish_state(temperature); + // Internal reference temperature always works + if (this->temperature_reference_) { + int16_t val = (mem & 0x0000FFF0) >> 4; + if (val & 0x0800) { + val |= 0xF000; // Pad out 2's complement + } + const float t_ref = float(val) * 0.0625f; + ESP_LOGD(TAG, "Got reference temperature: %.4f°C", t_ref); + this->temperature_reference_->publish_state(t_ref); + } + + // Check thermocouple faults + if (mem & 0x00000001) { + ESP_LOGW(TAG, "Thermocouple open circuit (not connected) fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000002) { + ESP_LOGW(TAG, "Thermocouple short circuit to ground fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000004) { + ESP_LOGW(TAG, "Thermocouple short circuit to VCC fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00010000) { + ESP_LOGW(TAG, "Got faulty reading from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + + // Decode thermocouple temperature + int16_t val = (mem & 0xFFFC0000) >> 18; + if (val & 0x2000) { + val |= 0xC000; // Pad out 2's complement + } + const float t_sense = float(val) * 0.25f; + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", t_sense); + this->publish_state(t_sense); this->status_clear_warning(); } diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 1d0fc79ac0..c0ed8a467d 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -9,9 +9,11 @@ namespace max31855 { class MAX31855Sensor : public sensor::Sensor, public PollingComponent, - public spi::SPIDevice { + public spi::SPIDevice { public: + void set_reference_sensor(sensor::Sensor *temperature_sensor) { temperature_reference_ = temperature_sensor; } + void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,6 +22,7 @@ class MAX31855Sensor : public sensor::Sensor, protected: void read_data_(); + sensor::Sensor *temperature_reference_{nullptr}; }; } // namespace max31855 diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index b4d9f82b03..dce28bd542 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import CONF_ID, CONF_REFERENCE_TEMPERATURE, ICON_THERMOMETER, UNIT_CELSIUS max31855_ns = cg.esphome_ns.namespace('max31855') MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingComponent, @@ -9,6 +9,8 @@ MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingC CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31855Sensor), + cv.Optional(CONF_REFERENCE_TEMPERATURE): + sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), }).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) @@ -17,3 +19,6 @@ def to_code(config): yield cg.register_component(var, config) yield spi.register_spi_device(var, config) yield sensor.register_sensor(var, config) + if CONF_REFERENCE_TEMPERATURE in config: + tc_ref = yield sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) + cg.add(var.set_reference_sensor(tc_ref)) diff --git a/esphome/components/max31865/__init__.py b/esphome/components/max31865/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp new file mode 100644 index 0000000000..500b5b2883 --- /dev/null +++ b/esphome/components/max31865/max31865.cpp @@ -0,0 +1,214 @@ +#include "max31865.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31865 { + +static const char* TAG = "max31865"; + +void MAX31865Sensor::update() { + // Check new faults since last measurement + if (!has_fault_) { + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if (faults & 0b11111100) { + if (faults & (1 << 2)) { + ESP_LOGW(TAG, "Overvoltage/undervoltage fault between measurements"); + } + if (faults & (1 << 3)) { + ESP_LOGW(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 4)) { + ESP_LOGW(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 5)) { + ESP_LOGW(TAG, "REFIN- > 0.85 x V_BIAS between measurements"); + } + if (!has_warn_) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold between measurements"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold between measurements"); + } + } + } + } + + // Run fault detection + write_register_(CONFIGURATION_REG, 0b11101110, 0b10000110); + const uint32_t start_time = micros(); + uint8_t config; + uint32_t fault_detect_time; + do { + config = this->read_register_(CONFIGURATION_REG); + fault_detect_time = micros() - start_time; + if ((fault_detect_time >= 6000) && (config & 0b00001100)) { + ESP_LOGE(TAG, "Fault detection incomplete (0x%02X) after %uμs (datasheet spec is 600μs max)! Aborting read.", + config, fault_detect_time); + this->publish_state(NAN); + this->status_set_error(); + return; + } + } while (config & 0b00001100); + ESP_LOGV(TAG, "Fault detection completed in %uμs.", fault_detect_time); + + // Start 1-shot conversion + this->write_register_(CONFIGURATION_REG, 0b11100000, 0b10100000); + + // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz + auto f = std::bind(&MAX31865Sensor::read_data_, this); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); +} + +void MAX31865Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31865Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // Build configuration + uint8_t config = 0b00000010; + config |= (filter_ & 1) << 0; + if (rtd_wires_ == 3) { + config |= 1 << 4; + } + this->write_register_(CONFIGURATION_REG, 0b11111111, config); +} + +void MAX31865Sensor::dump_config() { + LOG_SENSOR("", "MAX31865", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Reference Resistance: %.2fΩ", reference_resistance_); + ESP_LOGCONFIG(TAG, " RTD: %u-wire %.2fΩ", rtd_wires_, rtd_nominal_resistance_); + ESP_LOGCONFIG(TAG, " Filter: %s", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); +} + +float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MAX31865Sensor::read_data_() { + // Read temperature, disable V_BIAS (save power) + const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); + this->write_register_(CONFIGURATION_REG, 0b11000000, 0b00000000); + + // Check faults + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if ((has_fault_ = faults & 0b00111100)) { + if (faults & (1 << 2)) { + ESP_LOGE(TAG, "Overvoltage/undervoltage fault"); + } + if (faults & (1 << 3)) { + ESP_LOGE(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 4)) { + ESP_LOGE(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 5)) { + ESP_LOGE(TAG, "REFIN- > 0.85 x V_BIAS"); + } + this->publish_state(NAN); + this->status_set_error(); + return; + } else { + this->status_clear_error(); + } + if ((has_warn_ = faults & 0b11000000)) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold"); + } + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + + // Process temperature + if (rtd_resistance_register & 0x0001) { + ESP_LOGW(TAG, "RTD Resistance Registers fault bit set! (0x%04X)", rtd_resistance_register); + this->status_set_warning(); + } + const float rtd_ratio = static_cast(rtd_resistance_register >> 1) / static_cast((1 << 15) - 1); + const float temperature = this->calc_temperature_(rtd_ratio); + ESP_LOGV(TAG, "RTD read complete. %.5f (ratio) * %.1fΩ (reference) = %.2fΩ --> %.2f°C", rtd_ratio, + reference_resistance_, reference_resistance_ * rtd_ratio, temperature); + this->publish_state(temperature); +} + +void MAX31865Sensor::write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) { + uint8_t value = this->read_register_(reg); + + value &= (~mask); + value |= (bits << start_position); + + this->enable(); + this->write_byte(reg |= SPI_WRITE_M); + this->write_byte(value); + this->disable(); + ESP_LOGVV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +const uint8_t MAX31865Sensor::read_register_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + this->disable(); + ESP_LOGVV(TAG, "read_register_ 0x%02X: 0x%02X", reg, value); + return value; +} + +const uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + const uint8_t lsb(this->read_byte()); + this->disable(); + const uint16_t value((msb << 8) | lsb); + ESP_LOGVV(TAG, "read_register_16_ 0x%02X: 0x%04X", reg, value); + return value; +} + +float MAX31865Sensor::calc_temperature_(const float& rtd_ratio) { + // Based loosely on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31865 + // Mainly based on formulas provided by Analog: + // http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf + + const float a = 3.9083e-3; + const float b = -5.775e-7; + const float z1 = -a; + const float z2 = a * a - 4 * b; + const float z3 = 4 * b / rtd_nominal_resistance_; + const float z4 = 2 * b; + + float rtd_resistance = rtd_ratio * reference_resistance_; + + // ≥ 0°C Formula + const float pos_temp = (z1 + std::sqrt(z2 + (z3 * rtd_resistance))) / z4; + if (pos_temp >= 0) { + return pos_temp; + } + + // < 0°C Formula + if (rtd_nominal_resistance_ != 100) { + // Normalize RTD to 100Ω + rtd_resistance /= rtd_nominal_resistance_; + rtd_resistance *= 100; + } + float rpoly = rtd_resistance; + float neg_temp = -242.02; + neg_temp += 2.2228 * rpoly; + rpoly *= rtd_resistance; // square + neg_temp += 2.5859e-3 * rpoly; + rpoly *= rtd_resistance; // ^3 + neg_temp -= 4.8260e-6 * rpoly; + rpoly *= rtd_resistance; // ^4 + neg_temp -= 2.8183e-8 * rpoly; + rpoly *= rtd_resistance; // ^5 + neg_temp += 1.5243e-10 * rpoly; + return neg_temp; +} + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h new file mode 100644 index 0000000000..e63be8e6c5 --- /dev/null +++ b/esphome/components/max31865/max31865.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace max31865 { + +enum MAX31865RegisterMasks { SPI_WRITE_M = 0x80 }; +enum MAX31865Registers { + CONFIGURATION_REG = 0x00, + RTD_RESISTANCE_MSB_REG = 0x01, + RTD_RESISTANCE_LSB_REG = 0x02, + FAULT_THRESHOLD_H_MSB_REG = 0x03, + FAULT_THRESHOLD_H_LSB_REG = 0x04, + FAULT_THRESHOLD_L_MSB_REG = 0x05, + FAULT_THRESHOLD_L_LSB_REG = 0x06, + FAULT_STATUS_REG = 0x07, +}; +enum MAX31865ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31865Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void set_reference_resistance(float reference_resistance) { reference_resistance_ = reference_resistance; } + void set_nominal_resistance(float nominal_resistance) { rtd_nominal_resistance_ = nominal_resistance; } + void set_filter(MAX31865ConfigFilter filter) { filter_ = filter; } + void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void update() override; + + protected: + float reference_resistance_; + float rtd_nominal_resistance_; + MAX31865ConfigFilter filter_; + uint8_t rtd_wires_; + bool has_fault_ = false; + bool has_warn_ = false; + void read_data_(); + void write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position = 0); + const uint8_t read_register_(uint8_t reg); + const uint16_t read_register_16_(uint8_t reg); + float calc_temperature_(const float& rtd_ratio); +}; + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py new file mode 100644 index 0000000000..ff1df9c5c8 --- /dev/null +++ b/esphome/components/max31865/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import CONF_ID, CONF_MAINS_FILTER, CONF_REFERENCE_RESISTANCE, \ + CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, ICON_THERMOMETER, UNIT_CELSIUS + +max31865_ns = cg.esphome_ns.namespace('max31865') +MAX31865Sensor = max31865_ns.class_('MAX31865Sensor', sensor.Sensor, cg.PollingComponent, + spi.SPIDevice) + +MAX31865ConfigFilter = max31865_ns.enum('MAX31865ConfigFilter') +FILTER = { + '50HZ': MAX31865ConfigFilter.FILTER_50HZ, + '60HZ': MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ + cv.GenerateID(): cv.declare_id(MAX31865Sensor), + cv.Required(CONF_REFERENCE_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=10000)), + cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), + cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), + cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), +}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield sensor.register_sensor(var, config) + cg.add(var.set_reference_resistance(config[CONF_REFERENCE_RESISTANCE])) + cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE])) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) + cg.add(var.set_num_rtd_wires(config[CONF_RTD_WIRES])) diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index a528183ac8..7ff4f4e137 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -4,15 +4,14 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg from esphome.components import sensor -from esphome.const import UNIT_CELSIUS, ICON_THERMOMETER, CONF_SENSOR, CONF_TEMPERATURE, \ - CONF_VALUE, CONF_CALIBRATION, CONF_ID +from esphome.const import CONF_CALIBRATION, CONF_ID, CONF_REFERENCE_RESISTANCE, \ + CONF_REFERENCE_TEMPERATURE, CONF_SENSOR, CONF_TEMPERATURE, CONF_VALUE, ICON_THERMOMETER, \ + UNIT_CELSIUS ntc_ns = cg.esphome_ns.namespace('ntc') NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor) CONF_B_CONSTANT = 'b_constant' -CONF_REFERENCE_TEMPERATURE = 'reference_temperature' -CONF_REFERENCE_RESISTANCE = 'reference_resistance' CONF_A = 'a' CONF_B = 'b' CONF_C = 'c' diff --git a/esphome/const.py b/esphome/const.py index dc607d62ff..cde9134eae 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -224,6 +224,7 @@ CONF_LOGS = 'logs' CONF_LOW = 'low' CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' CONF_MAC_ADDRESS = 'mac_address' +CONF_MAINS_FILTER = 'mains_filter' CONF_MAKE_ID = 'make_id' CONF_MANUAL_IP = 'manual_ip' CONF_MASK_DISTURBER = 'mask_disturber' @@ -343,6 +344,8 @@ CONF_RAW = 'raw' CONF_REBOOT_TIMEOUT = 'reboot_timeout' CONF_RECEIVE_TIMEOUT = 'receive_timeout' CONF_RED = 'red' +CONF_REFERENCE_RESISTANCE = 'reference_resistance' +CONF_REFERENCE_TEMPERATURE = 'reference_temperature' CONF_REPEAT = 'repeat' CONF_REPOSITORY = 'repository' CONF_RESET_PIN = 'reset_pin' @@ -358,6 +361,8 @@ CONF_RGBW = 'rgbw' CONF_RISING_EDGE = 'rising_edge' CONF_ROTATION = 'rotation' CONF_RS_PIN = 'rs_pin' +CONF_RTD_NOMINAL_RESISTANCE = 'rtd_nominal_resistance' +CONF_RTD_WIRES = 'rtd_wires' CONF_RUN_CYCLES = 'run_cycles' CONF_RUN_DURATION = 'run_duration' CONF_RW_PIN = 'rw_pin' diff --git a/tests/test1.yaml b/tests/test1.yaml index 66f0220836..60f3b7f418 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -451,6 +451,14 @@ sensor: name: "Den Temperature" cs_pin: GPIO23 update_interval: 15s + reference_temperature: + name: "MAX31855 Internal Temperature" + - platform: max31865 + name: "Water Tank Temperature" + cs_pin: GPIO23 + update_interval: 15s + reference_resistance: "430 Ω" + rtd_nominal_resistance: "100 Ω" - platform: mhz19 co2: name: "MH-Z19 CO2 Value" From a919b015b40dbc10eef69ad0a17e00c68a1a0208 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 6 Nov 2019 13:59:00 +0100 Subject: [PATCH 069/412] Add support for INA226 Current/Power Monitor (#801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for INA226 Current/Power Monitor * fix lint errors * fix narrowing conversion * Remove useless code Co-authored-by: Sergio Muñoz --- esphome/components/ina226/__init__.py | 0 esphome/components/ina226/ina226.cpp | 140 ++++++++++++++++++++++++++ esphome/components/ina226/ina226.h | 35 +++++++ esphome/components/ina226/sensor.py | 48 +++++++++ tests/test1.yaml | 13 +++ 5 files changed, 236 insertions(+) create mode 100644 esphome/components/ina226/__init__.py create mode 100644 esphome/components/ina226/ina226.cpp create mode 100644 esphome/components/ina226/ina226.h create mode 100644 esphome/components/ina226/sensor.py diff --git a/esphome/components/ina226/__init__.py b/esphome/components/ina226/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp new file mode 100644 index 0000000000..cbb06d73b6 --- /dev/null +++ b/esphome/components/ina226/ina226.cpp @@ -0,0 +1,140 @@ +#include "ina226.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ina226 { + +static const char *TAG = "ina226"; + +// | A0 | A1 | Address | +// | GND | GND | 0x40 | +// | GND | V_S+ | 0x41 | +// | GND | SDA | 0x42 | +// | GND | SCL | 0x43 | +// | V_S+ | GND | 0x44 | +// | V_S+ | V_S+ | 0x45 | +// | V_S+ | SDA | 0x46 | +// | V_S+ | SCL | 0x47 | +// | SDA | GND | 0x48 | +// | SDA | V_S+ | 0x49 | +// | SDA | SDA | 0x4A | +// | SDA | SCL | 0x4B | +// | SCL | GND | 0x4C | +// | SCL | V_S+ | 0x4D | +// | SCL | SDA | 0x4E | +// | SCL | SCL | 0x4F | + +static const uint8_t INA226_REGISTER_CONFIG = 0x00; +static const uint8_t INA226_REGISTER_SHUNT_VOLTAGE = 0x01; +static const uint8_t INA226_REGISTER_BUS_VOLTAGE = 0x02; +static const uint8_t INA226_REGISTER_POWER = 0x03; +static const uint8_t INA226_REGISTER_CURRENT = 0x04; +static const uint8_t INA226_REGISTER_CALIBRATION = 0x05; + +void INA226Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up INA226..."); + // Config Register + // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) + if (!this->write_byte_16(INA226_REGISTER_CONFIG, 0x8000)) { + this->mark_failed(); + return; + } + delay(1); + + uint16_t config = 0x0000; + + // Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples) + config |= 0b0000001000000000; + + // Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms) + config |= 0b0000000100000000; + + // Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms) + config |= 0b0000000000100000; + + // Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous) + config |= 0b0000000000000111; + + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config)) { + this->mark_failed(); + return; + } + + // lsb is multiplied by 1000000 to store it as an integer value + uint32_t lsb = static_cast(ceilf(this->max_current_a_ * 1000000.0f / 32768)); + + this->calibration_lsb_ = lsb; + + auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); + + ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); + + if (!this->write_byte_16(INA226_REGISTER_CALIBRATION, calibration)) { + this->mark_failed(); + return; + } +} + +void INA226Component::dump_config() { + ESP_LOGCONFIG(TAG, "INA226:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with INA226 failed!"); + return; + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); +} + +float INA226Component::get_setup_priority() const { return setup_priority::DATA; } + +void INA226Component::update() { + if (this->bus_voltage_sensor_ != nullptr) { + uint16_t raw_bus_voltage; + if (!this->read_byte_16(INA226_REGISTER_BUS_VOLTAGE, &raw_bus_voltage, 1)) { + this->status_set_warning(); + return; + } + float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + this->bus_voltage_sensor_->publish_state(bus_voltage_v); + } + + if (this->shunt_voltage_sensor_ != nullptr) { + uint16_t raw_shunt_voltage; + if (!this->read_byte_16(INA226_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage, 1)) { + this->status_set_warning(); + } + float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f; + this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); + } + + if (this->current_sensor_ != nullptr) { + uint16_t raw_current; + if (!this->read_byte_16(INA226_REGISTER_CURRENT, &raw_current, 1)) { + this->status_set_warning(); + return; + } + float current_ma = int16_t(raw_current) * (this->calibration_lsb_ / 1000.0f); + this->current_sensor_->publish_state(current_ma / 1000.0f); + } + + if (this->power_sensor_ != nullptr) { + uint16_t raw_power; + if (!this->read_byte_16(INA226_REGISTER_POWER, &raw_power, 1)) { + this->status_set_warning(); + return; + } + float power_mw = int16_t(raw_power) * (this->calibration_lsb_ * 25.0f / 1000.0f); + this->power_sensor_->publish_state(power_mw / 1000.0f); + } + + this->status_clear_warning(); +} + +} // namespace ina226 +} // namespace esphome diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h new file mode 100644 index 0000000000..a551cb3430 --- /dev/null +++ b/esphome/components/ina226/ina226.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ina226 { + +class INA226Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } + void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; } + void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + protected: + float shunt_resistance_ohm_; + float max_current_a_; + uint32_t calibration_lsb_; + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *shunt_voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; +}; + +} // namespace ina226 +} // namespace esphome diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py new file mode 100644 index 0000000000..02a13f98c4 --- /dev/null +++ b/esphome/components/ina226/sensor.py @@ -0,0 +1,48 @@ +# coding=utf-8 +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_BUS_VOLTAGE, CONF_CURRENT, CONF_ID, \ + CONF_MAX_CURRENT, CONF_POWER, CONF_SHUNT_RESISTANCE, \ + CONF_SHUNT_VOLTAGE, ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['i2c'] + +ina226_ns = cg.esphome_ns.namespace('ina226') +INA226Component = ina226_ns.class_('INA226Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(INA226Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(cv.resistance, cv.Range(min=0.0)), + cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(cv.current, cv.Range(min=0.0)), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) + + cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + + if CONF_BUS_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + cg.add(var.set_bus_voltage_sensor(sens)) + + if CONF_SHUNT_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) + cg.add(var.set_shunt_voltage_sensor(sens)) + + if CONF_CURRENT in config: + sens = yield sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_POWER in config: + sens = yield sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 60f3b7f418..0905c51efa 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -424,6 +424,19 @@ sensor: max_voltage: 32.0V max_current: 3.2A update_interval: 15s + - platform: ina226 + address: 0x40 + shunt_resistance: 0.1 ohm + current: + name: "INA226 Current" + power: + name: "INA226 Power" + bus_voltage: + name: "INA226 Bus Voltage" + shunt_voltage: + name: "INA226 Shunt Voltage" + max_current: 3.2A + update_interval: 15s - platform: ina3221 address: 0x40 channel_1: From b5af3aa04880c33a7a572937052f8b3bb87b48f3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 6 Nov 2019 22:35:22 +0100 Subject: [PATCH 070/412] Update variable in scheduler (#838) Fixes https://github.com/esphome/issues/issues/826 --- esphome/core/scheduler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index cc4331b38e..e61b2b13c6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -211,6 +211,7 @@ uint32_t Scheduler::millis_() { ESP_LOGD(TAG, "Incrementing scheduler major"); this->millis_major_++; } + this->last_millis_ = now; return now; } From 003326f2eb0f52e974325347bcc2e1d76d3c2283 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 6 Nov 2019 22:49:38 +0100 Subject: [PATCH 071/412] Fix calculations for negative sun declination (#839) Fixes https://github.com/esphome/issues/issues/793 Also adds a clampd function that operates with doubles, not floats --- esphome/components/sun/sun.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index e0da63bb4b..6744a418c5 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -52,6 +52,14 @@ double Sun::azimuth() { return NAN; return this->azimuth_(time); } +// like clamp, but with doubles +double clampd(double val, double min, double max) { + if (val < min) + return min; + if (val > max) + return max; + return val; +} double Sun::sun_declination_(double sun_time) { double n = sun_time - 1.0; // maximum declination @@ -67,7 +75,7 @@ double Sun::sun_declination_(double sun_time) { const double c = TAU / 365.24; double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion)); // Make sure value is in range (double error may lead to results slightly larger than 1) - double x = clamp(tot * v, 0, 1); + double x = clampd(tot * v, -1.0, 1.0); return asin(x); } double Sun::elevation_ratio_(double sun_time) { @@ -75,7 +83,7 @@ double Sun::elevation_ratio_(double sun_time) { double hangle = this->hour_angle_(sun_time); double a = sin(this->latitude_rad_()) * sin(decl); double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle); - double val = clamp(a + b, -1.0, 1.0); + double val = clampd(a + b, -1.0, 1.0); return val; } double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; } @@ -92,7 +100,7 @@ double Sun::azimuth_rad_(double sun_time) { double zen = this->zenith_rad_(sun_time); double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl); double denom = sin(zen) * cos(this->latitude_rad_()); - double v = clamp(nom / denom, -1.0, 1.0); + double v = clampd(nom / denom, -1.0, 1.0); double az = PI - acos(v); if (hangle > 0) az = -az; From 3e8fd48dc0d6e90665c3c997ffc32a9a24f59215 Mon Sep 17 00:00:00 2001 From: Alexander Leisentritt Date: Thu, 7 Nov 2019 22:10:09 +0100 Subject: [PATCH 072/412] implemented ruuvi_ble and ruuvitag with RAWv1 and RAWv2 protocol (#810) * implemented ruuvi_ble and ruuvitag with RAWv1 protocol fixes esphome/feature-requests#313 * lint * updated data calculations * cpp lint * use string directly in message Co-Authored-By: Otto Winter * add RAWv2 protocol support * fix ICON_SIGNAL * typo * calculation correction and cleaning * c++ lint * added acceleration and fixed typo * removed remote_receiver to reduce firmware size remote_receiver also in test1.yaml --- .../components/esp32_ble_tracker/__init__.py | 2 +- esphome/components/ruuvi_ble/__init__.py | 18 +++ esphome/components/ruuvi_ble/ruuvi_ble.cpp | 147 ++++++++++++++++++ esphome/components/ruuvi_ble/ruuvi_ble.h | 37 +++++ esphome/components/ruuvitag/__init__.py | 0 esphome/components/ruuvitag/ruuvitag.cpp | 29 ++++ esphome/components/ruuvitag/ruuvitag.h | 84 ++++++++++ esphome/components/ruuvitag/sensor.py | 76 +++++++++ esphome/const.py | 15 +- tests/test2.yaml | 28 +++- 10 files changed, 429 insertions(+), 7 deletions(-) create mode 100644 esphome/components/ruuvi_ble/__init__.py create mode 100644 esphome/components/ruuvi_ble/ruuvi_ble.cpp create mode 100644 esphome/components/ruuvi_ble/ruuvi_ble.h create mode 100644 esphome/components/ruuvitag/__init__.py create mode 100644 esphome/components/ruuvitag/ruuvitag.cpp create mode 100644 esphome/components/ruuvitag/ruuvitag.h create mode 100644 esphome/components/ruuvitag/sensor.py diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 4aa5b1610a..3d48eafde4 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ from esphome.core import coroutine ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -AUTO_LOAD = ['xiaomi_ble'] +AUTO_LOAD = ['xiaomi_ble', 'ruuvi_ble'] CONF_ESP32_BLE_ID = 'esp32_ble_id' CONF_SCAN_PARAMETERS = 'scan_parameters' diff --git a/esphome/components/ruuvi_ble/__init__.py b/esphome/components/ruuvi_ble/__init__.py new file mode 100644 index 0000000000..05ba008dd0 --- /dev/null +++ b/esphome/components/ruuvi_ble/__init__.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +ruuvi_ble_ns = cg.esphome_ns.namespace('ruuvi_ble') +RuuviListener = ruuvi_ble_ns.class_('RuuviListener', esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(RuuviListener), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp new file mode 100644 index 0000000000..28a689fee4 --- /dev/null +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -0,0 +1,147 @@ +#include "ruuvi_ble.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvi_ble { + +static const char *TAG = "ruuvi_ble"; + +bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t *data, RuuviParseResult &result) { + switch (data_type) { + case 0x03: { // RAWv1 + if (data_length != 16) + return false; + + const uint8_t temp_sign = (data[1] >> 7) & 1; + const float temp_val = (data[1] & 0x7F) + (data[2] / 100.0f); + const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val; + + const float humidity = data[0] * 0.5f; + const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f; + const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f; + const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f; + const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f; + const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f; + + result.humidity = humidity; + result.temperature = temperature; + result.pressure = pressure; + result.acceleration_x = acceleration_x; + result.acceleration_y = acceleration_y; + result.acceleration_z = acceleration_z; + result.acceleration = + sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + result.battery_voltage = battery_voltage; + + return true; + } + case 0x05: { // RAWv2 + if (data_length != 26) + return false; + + const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; + const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f; + const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f; + const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f; + const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; + const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; + + const uint8_t power_info = (data[12] << 8) | data[13]; + const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; + const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; + + const float movement_counter = float(data[14]); + const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16])); + + result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature; + result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity; + result.pressure = data[4] == 0xFF && data[5] == 0xFF ? NAN : pressure; + result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x; + result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y; + result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; + result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN + ? NAN + : sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + + acceleration_z * acceleration_z); + result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; + result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; + result.movement_counter = movement_counter; + result.measurement_sequence_number = measurement_sequence_number; + + return true; + } + default: + return false; + } +} +optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device) { + const auto *raw = reinterpret_cast(device.get_manufacturer_data().data()); + + bool is_ruuvi = raw[0] == 0x99 && raw[1] == 0x04; + + if (!is_ruuvi) { + return {}; + } + + const uint8_t data_length = device.get_manufacturer_data().size(); + const uint8_t format = raw[2]; + const uint8_t *data = &raw[3]; + + RuuviParseResult result; + + bool success = parse_ruuvi_data_byte(format, data_length, data, result); + if (!success) + return {}; + return result; +} + +bool RuuviListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + auto res = parse_ruuvi(device); + if (!res.has_value()) + return false; + + ESP_LOGD(TAG, "Got RuuviTag (%s):", device.address_str().c_str()); + + if (res->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.2f%%", *res->humidity); + } + if (res->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.2f°C", *res->temperature); + } + if (res->pressure.has_value()) { + ESP_LOGD(TAG, " Pressure: %.2fhPa", *res->pressure); + } + if (res->acceleration.has_value()) { + ESP_LOGD(TAG, " Acceleration: %.3fG", *res->acceleration); + } + if (res->acceleration_x.has_value()) { + ESP_LOGD(TAG, " Acceleration X: %.3fG", *res->acceleration_x); + } + if (res->acceleration_y.has_value()) { + ESP_LOGD(TAG, " Acceleration Y: %.3fG", *res->acceleration_y); + } + if (res->acceleration_z.has_value()) { + ESP_LOGD(TAG, " Acceleration Z: %.3fG", *res->acceleration_z); + } + if (res->battery_voltage.has_value()) { + ESP_LOGD(TAG, " Battery Voltage: %.3fV", *res->battery_voltage); + } + if (res->tx_power.has_value()) { + ESP_LOGD(TAG, " TX Power: %.0fdBm", *res->tx_power); + } + if (res->movement_counter.has_value()) { + ESP_LOGD(TAG, " Movement Counter: %.0f", *res->movement_counter); + } + if (res->measurement_sequence_number.has_value()) { + ESP_LOGD(TAG, " Measurement Sequence Number: %.0f", *res->measurement_sequence_number); + } + + return true; +} + +} // namespace ruuvi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.h b/esphome/components/ruuvi_ble/ruuvi_ble.h new file mode 100644 index 0000000000..848004f3d7 --- /dev/null +++ b/esphome/components/ruuvi_ble/ruuvi_ble.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvi_ble { + +struct RuuviParseResult { + optional humidity; + optional temperature; + optional pressure; + optional acceleration; + optional acceleration_x; + optional acceleration_y; + optional acceleration_z; + optional battery_voltage; + optional tx_power; + optional movement_counter; + optional measurement_sequence_number; +}; + +bool parse_ruuvi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, RuuviParseResult &result); + +optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device); + +class RuuviListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace ruuvi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/__init__.py b/esphome/components/ruuvitag/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ruuvitag/ruuvitag.cpp b/esphome/components/ruuvitag/ruuvitag.cpp new file mode 100644 index 0000000000..2963b777d1 --- /dev/null +++ b/esphome/components/ruuvitag/ruuvitag.cpp @@ -0,0 +1,29 @@ +#include "ruuvitag.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvitag { + +static const char *TAG = "ruuvitag"; + +void RuuviTag::dump_config() { + ESP_LOGCONFIG(TAG, "RuuviTag"); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Pressure", this->pressure_); + LOG_SENSOR(" ", "Acceleration", this->acceleration_); + LOG_SENSOR(" ", "Acceleration X", this->acceleration_x_); + LOG_SENSOR(" ", "Acceleration Y", this->acceleration_y_); + LOG_SENSOR(" ", "Acceleration Z", this->acceleration_z_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "TX Power", this->tx_power_); + LOG_SENSOR(" ", "Movement Counter", this->movement_counter_); + LOG_SENSOR(" ", "Measurement Sequence Number", this->measurement_sequence_number_); +} + +} // namespace ruuvitag +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h new file mode 100644 index 0000000000..863c5775c2 --- /dev/null +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/ruuvi_ble/ruuvi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvitag { + +class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = ruuvi_ble::parse_ruuvi(device); + if (!res.has_value()) + return false; + + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->pressure.has_value() && this->pressure_ != nullptr) + this->pressure_->publish_state(*res->pressure); + if (res->acceleration.has_value() && this->acceleration_ != nullptr) + this->acceleration_->publish_state(*res->acceleration); + if (res->acceleration_x.has_value() && this->acceleration_x_ != nullptr) + this->acceleration_x_->publish_state(*res->acceleration_x); + if (res->acceleration_y.has_value() && this->acceleration_y_ != nullptr) + this->acceleration_y_->publish_state(*res->acceleration_y); + if (res->acceleration_z.has_value() && this->acceleration_z_ != nullptr) + this->acceleration_z_->publish_state(*res->acceleration_z); + if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) + this->battery_voltage_->publish_state(*res->battery_voltage); + if (res->tx_power.has_value() && this->tx_power_ != nullptr) + this->tx_power_->publish_state(*res->tx_power); + if (res->movement_counter.has_value() && this->movement_counter_ != nullptr) + this->movement_counter_->publish_state(*res->movement_counter); + if (res->measurement_sequence_number.has_value() && this->measurement_sequence_number_ != nullptr) + this->measurement_sequence_number_->publish_state(*res->measurement_sequence_number); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } + void set_acceleration(sensor::Sensor *acceleration) { acceleration_ = acceleration; } + void set_acceleration_x(sensor::Sensor *acceleration_x) { acceleration_x_ = acceleration_x; } + void set_acceleration_y(sensor::Sensor *acceleration_y) { acceleration_y_ = acceleration_y; } + void set_acceleration_z(sensor::Sensor *acceleration_z) { acceleration_z_ = acceleration_z; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_tx_power(sensor::Sensor *tx_power) { tx_power_ = tx_power; } + void set_movement_counter(sensor::Sensor *movement_counter) { movement_counter_ = movement_counter; } + void set_measurement_sequence_number(sensor::Sensor *measurement_sequence_number) { + measurement_sequence_number_ = measurement_sequence_number; + } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *pressure_{nullptr}; + sensor::Sensor *acceleration_{nullptr}; + sensor::Sensor *acceleration_x_{nullptr}; + sensor::Sensor *acceleration_y_{nullptr}; + sensor::Sensor *acceleration_z_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *tx_power_{nullptr}; + sensor::Sensor *movement_counter_{nullptr}; + sensor::Sensor *measurement_sequence_number_{nullptr}; +}; + +} // namespace ruuvitag +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py new file mode 100644 index 0000000000..3d9c724e7b --- /dev/null +++ b/esphome/components/ruuvitag/sensor.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + CONF_PRESSURE, CONF_ACCELERATION, CONF_ACCELERATION_X, CONF_ACCELERATION_Y, \ + CONF_ACCELERATION_Z, CONF_BATTERY_VOLTAGE, CONF_TX_POWER, \ + CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, UNIT_CELSIUS, \ + ICON_THERMOMETER, UNIT_PERCENT, UNIT_VOLT, UNIT_HECTOPASCAL, UNIT_G, \ + UNIT_DECIBEL_MILLIWATT, UNIT_EMPTY, ICON_WATER_PERCENT, ICON_BATTERY, \ + ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, ICON_ACCELERATION_Y, \ + ICON_ACCELERATION_Z, ICON_SIGNAL, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['ruuvi_ble'] + +ruuvitag_ns = cg.esphome_ns.namespace('ruuvitag') +RuuviTag = ruuvitag_ns.class_( + 'RuuviTag', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(RuuviTag), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 2), + cv.Optional(CONF_ACCELERATION): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION, 3), + cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_X, 3), + cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Y, 3), + cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Z, 3), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_BATTERY, 3), + cv.Optional(CONF_TX_POWER): sensor.sensor_schema(UNIT_DECIBEL_MILLIWATT, ICON_SIGNAL, 0), + cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), + cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_PRESSURE in config: + sens = yield sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure(sens)) + if CONF_ACCELERATION in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION]) + cg.add(var.set_acceleration(sens)) + if CONF_ACCELERATION_X in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_X]) + cg.add(var.set_acceleration_x(sens)) + if CONF_ACCELERATION_Y in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Y]) + cg.add(var.set_acceleration_y(sens)) + if CONF_ACCELERATION_Z in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Z]) + cg.add(var.set_acceleration_z(sens)) + if CONF_BATTERY_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + cg.add(var.set_battery_voltage(sens)) + if CONF_TX_POWER in config: + sens = yield sensor.new_sensor(config[CONF_TX_POWER]) + cg.add(var.set_tx_power(sens)) + if CONF_MOVEMENT_COUNTER in config: + sens = yield sensor.new_sensor(config[CONF_MOVEMENT_COUNTER]) + cg.add(var.set_movement_counter(sens)) + if CONF_MEASUREMENT_SEQUENCE_NUMBER in config: + sens = yield sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER]) + cg.add(var.set_measurement_sequence_number(sens)) diff --git a/esphome/const.py b/esphome/const.py index cde9134eae..2af143984c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -29,6 +29,9 @@ HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} CONF_ABOVE = 'above' CONF_ACCELERATION = 'acceleration' +CONF_ACCELERATION_X = 'acceleration_x' +CONF_ACCELERATION_Y = 'acceleration_y' +CONF_ACCELERATION_Z = 'acceleration_z' CONF_ACCURACY = 'accuracy' CONF_ACCURACY_DECIMALS = 'accuracy_decimals' CONF_ACTION_ID = 'action_id' @@ -47,6 +50,7 @@ CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' CONF_AWAY_CONFIG = 'away_config' CONF_BATTERY_LEVEL = 'battery_level' +CONF_BATTERY_VOLTAGE = 'battery_voltage' CONF_BAUD_RATE = 'baud_rate' CONF_BELOW = 'below' CONF_BINARY = 'binary' @@ -239,6 +243,7 @@ CONF_MAX_TEMPERATURE = 'max_temperature' CONF_MAX_VALUE = 'max_value' CONF_MAX_VOLTAGE = 'max_voltage' CONF_MEASUREMENT_DURATION = 'measurement_duration' +CONF_MEASUREMENT_SEQUENCE_NUMBER = 'measurement_sequence_number' CONF_MEDIUM = 'medium' CONF_METHOD = 'method' CONF_MIN_LENGTH = 'min_length' @@ -254,6 +259,7 @@ CONF_MODEL = 'model' CONF_MOISTURE = 'moisture' CONF_MONTHS = 'months' CONF_MOSI_PIN = 'mosi_pin' +CONF_MOVEMENT_COUNTER = 'movement_counter' CONF_MQTT = 'mqtt' CONF_MQTT_ID = 'mqtt_id' CONF_MULTIPLEXER = 'multiplexer' @@ -449,6 +455,7 @@ CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_TURN_ON_ACTION = 'turn_on_action' CONF_TX_BUFFER_SIZE = 'tx_buffer_size' CONF_TX_PIN = 'tx_pin' +CONF_TX_POWER = 'tx_power' CONF_TYPE = 'type' CONF_TYPE_ID = 'type_id' CONF_UART_ID = 'uart_id' @@ -483,6 +490,10 @@ CONF_WIND_SPEED = 'wind_speed' CONF_WINDOW_SIZE = 'window_size' CONF_ZERO = 'zero' +ICON_ACCELERATION = 'mdi:axis-arrow' +ICON_ACCELERATION_X = 'mdi:axis-x-arrow' +ICON_ACCELERATION_Y = 'mdi:axis-y-arrow' +ICON_ACCELERATION_Z = 'mdi:axis-z-arrow' ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' @@ -508,7 +519,7 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGN_DIRECTION = 'mdi:sign-direction' -ICON_SIGNAL = 'mdi: signal-distance-variant' +ICON_SIGNAL = 'mdi:signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' ICON_TIMER = 'mdi:timer' @@ -522,9 +533,11 @@ ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' UNIT_CELSIUS = u'°C' UNIT_DECIBEL = 'dB' +UNIT_DECIBEL_MILLIWATT = 'dBm' UNIT_DEGREE_PER_SECOND = u'°/s' UNIT_DEGREES = u'°' UNIT_EMPTY = '' +UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' UNIT_HZ = 'hz' UNIT_KELVIN = 'K' diff --git a/tests/test2.yaml b/tests/test2.yaml index 78e1ab149a..935f9edb84 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -95,7 +95,7 @@ sensor: temperature: name: "Xiaomi LYWSD02 Temperature" humidity: - name: "Xiaomi LYWSD02 Humidity" + name: "Xiaomi LYWSD02 Humidity" - platform: xiaomi_cgg1 mac_address: 7A:80:8E:19:36:BA temperature: @@ -160,6 +160,28 @@ sensor: name: "Lightning Energy" distance: name: "Distance Storm" + - platform: ruuvitag + mac_address: FF:56:D3:2F:7D:E8 + humidity: + name: "RuuviTag Humidity" + temperature: + name: "RuuviTag Temperature" + pressure: + name: "RuuviTag Pressure" + acceleration_x: + name: "RuuviTag Acceleration X" + acceleration_y: + name: "RuuviTag Acceleration Y" + acceleration_z: + name: "RuuviTag Acceleration Z" + battery_voltage: + name: "RuuviTag Battery Voltage" + tx_power: + name: "RuuviTag TX Power" + movement_counter: + name: "RuuviTag Movement Counter" + measurement_sequence_number: + name: "RuuviTag Measurement Sequence Number" time: - platform: homeassistant @@ -210,10 +232,6 @@ binary_sensor: - platform: as3935 name: "Storm Alert" -remote_receiver: - pin: GPIO32 - dump: [] - esp32_ble_tracker: #esp32_ble_beacon: From f8d98ac4948b0b14702cd5a79fe2967dce452e6e Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Sat, 9 Nov 2019 20:37:52 +0300 Subject: [PATCH 073/412] http_request component (#719) * It works * Template doesn't work * Template fix * CA Certificate untested * ESP32 done * URL validation * Lint fix * Lint fix (2) * Lint fix (<3) * Support unsecure requests with framework >=2.5.0 * Removed fingerprint, payload renamed to body * Removed add_extra * Review * Review * New HTTP methods * Check recommended version * Removed dead code * Small improvement * Small improvement * CONF_METHOD from const * JSON support * New JSON syntax * Templatable headers * verify_ssl param * verify_ssl param (fix) * Lint * nolint * JSON string_strict * Two json syntax * Lambda url fix validation * CI fix * CI fix --- esphome/automation.py | 8 +- esphome/components/http_request/__init__.py | 148 ++++++++++++++++++ .../components/http_request/http_request.cpp | 63 ++++++++ .../components/http_request/http_request.h | 121 ++++++++++++++ esphome/const.py | 2 +- esphome/core_config.py | 4 +- 6 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 esphome/components/http_request/__init__.py create mode 100644 esphome/components/http_request/http_request.cpp create mode 100644 esphome/components/http_request/http_request.h diff --git a/esphome/automation.py b/esphome/automation.py index 9f9ad35520..f758191268 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -7,13 +7,17 @@ from esphome.util import Registry def maybe_simple_id(*validators): + return maybe_conf(CONF_ID, *validators) + + +def maybe_conf(conf, *validators): validator = cv.All(*validators) def validate(value): if isinstance(value, dict): return validator(value) - with cv.remove_prepend_path([CONF_ID]): - return validator({CONF_ID: value}) + with cv.remove_prepend_path([conf]): + return validator({conf: value}) return validate diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py new file mode 100644 index 0000000000..23fc38ba40 --- /dev/null +++ b/esphome/components/http_request/__init__.py @@ -0,0 +1,148 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ + CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_0 +from esphome.core import CORE, Lambda +from esphome.core_config import PLATFORMIO_ESP8266_LUT +from esphome.py_compat import IS_PY3 + +if IS_PY3: + import urllib.parse as urlparse # pylint: disable=no-name-in-module,import-error +else: + import urlparse # pylint: disable=import-error + +DEPENDENCIES = ['network'] +AUTO_LOAD = ['json'] + +http_request_ns = cg.esphome_ns.namespace('http_request') +HttpRequestComponent = http_request_ns.class_('HttpRequestComponent', cg.Component) +HttpRequestSendAction = http_request_ns.class_('HttpRequestSendAction', automation.Action) + +CONF_URL = 'url' +CONF_HEADERS = 'headers' +CONF_USERAGENT = 'useragent' +CONF_BODY = 'body' +CONF_JSON = 'json' +CONF_VERIFY_SSL = 'verify_ssl' + + +def validate_framework(config): + if CORE.is_esp32: + return config + + version = 'RECOMMENDED' + if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]: + version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION] + + if version in ['LATEST', 'DEV']: + return config + + framework = PLATFORMIO_ESP8266_LUT[version] if version in PLATFORMIO_ESP8266_LUT else version + if framework < ARDUINO_VERSION_ESP8266_2_5_0: + raise cv.Invalid('This component is not supported on arduino framework version below 2.5.0') + return config + + +def validate_url(value): + value = cv.string(value) + try: + parsed = list(urlparse.urlparse(value)) + except Exception: + raise cv.Invalid('Invalid URL') + + if not parsed[0] or not parsed[1]: + raise cv.Invalid('URL must have a URL scheme and host') + + if parsed[0] not in ['http', 'https']: + raise cv.Invalid('Scheme must be http or https') + + if not parsed[2]: + parsed[2] = '/' + + return urlparse.urlunparse(parsed) + + +def validate_secure_url(config): + url_ = config[CONF_URL] + if config.get(CONF_VERIFY_SSL) and not isinstance(url_, Lambda) \ + and url_.lower().startswith('https:'): + raise cv.Invalid('Currently ESPHome doesn\'t support SSL verification. ' + 'Set \'verify_ssl: false\' to make insecure HTTPS requests.') + return config + + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(HttpRequestComponent), + cv.Optional(CONF_USERAGENT, 'ESPHome'): cv.string, + cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, +}).add_extra(validate_framework).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_useragent(config[CONF_USERAGENT])) + yield cg.register_component(var, config) + + +HTTP_REQUEST_ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(HttpRequestComponent), + cv.Required(CONF_URL): cv.templatable(validate_url), + cv.Optional(CONF_HEADERS): cv.All(cv.Schema({cv.string: cv.templatable(cv.string)})), + cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, +}).add_extra(validate_secure_url) +HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf( + CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Optional(CONF_METHOD, default='GET'): cv.one_of('GET', upper=True), + }) +) +HTTP_REQUEST_POST_ACTION_SCHEMA = automation.maybe_conf( + CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Optional(CONF_METHOD, default='POST'): cv.one_of('POST', upper=True), + cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, 'body'): cv.Any( + cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), + }) +) +HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Required(CONF_METHOD): cv.one_of('GET', 'POST', 'PUT', 'DELETE', 'PATCH', upper=True), + cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, 'body'): cv.Any( + cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), +}) + + +@automation.register_action('http_request.get', HttpRequestSendAction, + HTTP_REQUEST_GET_ACTION_SCHEMA) +@automation.register_action('http_request.post', HttpRequestSendAction, + HTTP_REQUEST_POST_ACTION_SCHEMA) +@automation.register_action('http_request.send', HttpRequestSendAction, + HTTP_REQUEST_SEND_ACTION_SCHEMA) +def http_request_action_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = yield cg.templatable(config[CONF_URL], args, cg.const_char_ptr) + cg.add(var.set_url(template_)) + cg.add(var.set_method(config[CONF_METHOD])) + if CONF_BODY in config: + template_ = yield cg.templatable(config[CONF_BODY], args, cg.std_string) + cg.add(var.set_body(template_)) + if CONF_JSON in config: + json_ = config[CONF_JSON] + if isinstance(json_, Lambda): + args_ = args + [(cg.JsonObjectRef, 'root')] + lambda_ = yield cg.process_lambda(json_, args_, return_type=cg.void) + cg.add(var.set_json(lambda_)) + else: + for key in json_: + template_ = yield cg.templatable(json_[key], args, cg.std_string) + cg.add(var.add_json(key, template_)) + for key in config.get(CONF_HEADERS, []): + template_ = yield cg.templatable(config[CONF_HEADERS][key], args, cg.const_char_ptr) + cg.add(var.add_header(key, template_)) + + yield var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp new file mode 100644 index 0000000000..9df7cf7913 --- /dev/null +++ b/esphome/components/http_request/http_request.cpp @@ -0,0 +1,63 @@ +#include "http_request.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace http_request { + +static const char *TAG = "http_request"; + +void HttpRequestComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HTTP Request:"); + ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_); + ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); +} + +void HttpRequestComponent::send() { + bool begin_status = false; +#ifdef ARDUINO_ARCH_ESP32 + begin_status = this->client_.begin(this->url_); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + begin_status = this->client_.begin(*this->wifi_client_, this->url_); + this->client_.setFollowRedirects(true); + this->client_.setRedirectLimit(3); +#endif +#endif + + if (!begin_status) { + this->client_.end(); + this->status_set_warning(); + ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration"); + return; + } + + this->client_.setTimeout(this->timeout_); + if (this->useragent_ != nullptr) { + this->client_.setUserAgent(this->useragent_); + } + for (const auto &header : this->headers_) { + this->client_.addHeader(header.name, header.value, false, true); + } + + int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + this->client_.end(); + + if (http_code < 0) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_, HTTPClient::errorToString(http_code).c_str()); + this->status_set_warning(); + return; + } + + if (http_code < 200 || http_code >= 300) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_, http_code); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_, http_code); +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h new file mode 100644 index 0000000000..4f772d6826 --- /dev/null +++ b/esphome/components/http_request/http_request.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/json/json_util.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace http_request { + +struct Header { + const char *name; + const char *value; +}; + +class HttpRequestComponent : public Component { + public: + void setup() override { +#ifdef ARDUINO_ARCH_ESP8266 + this->wifi_client_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_->setInsecure(); +#endif + } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void set_url(const char *url) { this->url_ = url; } + void set_method(const char *method) { this->method_ = method; } + void set_useragent(const char *useragent) { this->useragent_ = useragent; } + void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } + void set_body(std::string body) { this->body_ = body; } + void set_headers(std::list
headers) { this->headers_ = headers; } + void send(); + + protected: + HTTPClient client_{}; + const char *url_; + const char *method_; + const char *useragent_{nullptr}; + uint16_t timeout_{5000}; + std::string body_; + std::list
headers_; +#ifdef ARDUINO_ARCH_ESP8266 + BearSSL::WiFiClientSecure *wifi_client_; +#endif +}; + +template class HttpRequestSendAction : public Action { + public: + HttpRequestSendAction(HttpRequestComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(const char *, url) + TEMPLATABLE_VALUE(const char *, method) + TEMPLATABLE_VALUE(std::string, body) + TEMPLATABLE_VALUE(const char *, useragent) + TEMPLATABLE_VALUE(uint16_t, timeout) + + void add_header(const char *key, TemplatableValue value) { this->headers_.insert({key, value}); } + + void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + + void set_json(std::function json_func) { this->json_func_ = json_func; } + + void play(Ts... x) override { + this->parent_->set_url(this->url_.value(x...)); + this->parent_->set_method(this->method_.value(x...)); + if (this->body_.has_value()) { + this->parent_->set_body(this->body_.value(x...)); + } + if (!this->json_.empty()) { + auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); + this->parent_->set_body(json::build_json(f)); + } + if (this->json_func_ != nullptr) { + auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); + this->parent_->set_body(json::build_json(f)); + } + if (this->useragent_.has_value()) { + this->parent_->set_useragent(this->useragent_.value(x...)); + } + if (this->timeout_.has_value()) { + this->parent_->set_timeout(this->timeout_.value(x...)); + } + if (!this->headers_.empty()) { + std::list
headers; + for (const auto &item : this->headers_) { + auto val = item.second; + Header header; + header.name = item.first; + header.value = val.value(x...); + headers.push_back(header); + } + this->parent_->set_headers(headers); + } + this->parent_->send(); + } + + protected: + void encode_json_(Ts... x, JsonObject &root) { + for (const auto &item : this->json_) { + auto val = item.second; + root[item.first] = val.value(x...); + } + } + void encode_json_func_(Ts... x, JsonObject &root) { this->json_func_(x..., root); } + HttpRequestComponent *parent_; + std::map> headers_{}; + std::map> json_{}; + std::function json_func_{nullptr}; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 2af143984c..5867666576 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -20,7 +20,7 @@ ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0' ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0' ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ '/stage' -ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' +ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.1' ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3' ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' diff --git a/esphome/core_config.py b/esphome/core_config.py index f63d2e17e3..f7149db797 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -76,7 +76,7 @@ def validate_arduino_version(value): if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT: raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " "at this time. You can override this by manually using " - "espressif8266@") + "espressif8266@".format(value)) if value_ in PLATFORMIO_ESP8266_LUT: return PLATFORMIO_ESP8266_LUT[value_] return value @@ -84,7 +84,7 @@ def validate_arduino_version(value): if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT: raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " "at this time. You can override this by manually using " - "espressif32@") + "espressif32@".format(value)) if value_ in PLATFORMIO_ESP32_LUT: return PLATFORMIO_ESP32_LUT[value_] return value From 367ae906c3537065ca9aa5d5293e62a130db3f1f Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 11 Nov 2019 23:50:06 -0300 Subject: [PATCH 074/412] fix missing checks of is_playing condition (#844) --- esphome/components/dfplayer/dfplayer.cpp | 6 ++++ esphome/components/dfplayer/dfplayer.h | 45 +++++++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 5ce4998796..6fed433dac 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -8,8 +8,10 @@ static const char* TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { if (folder < 100 && file < 256) { + this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); } else if (folder <= 10 && file <= 1000) { + this->ack_set_is_playing_ = true; this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); } else { ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file); @@ -93,6 +95,10 @@ void DFPlayer::loop() { ESP_LOGI(TAG, "USB, TF Card available"); } break; + case 0x40: + ESP_LOGV(TAG, "Nack"); + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; case 0x41: ESP_LOGV(TAG, "Ack ok"); this->is_playing_ |= this->ack_set_is_playing_; diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 86efd62138..22ca11c3be 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -27,29 +27,56 @@ class DFPlayer : public uart::UARTDevice, public Component { public: void loop() override; - void next() { this->send_cmd_(0x01); } - void previous() { this->send_cmd_(0x02); } + void next() { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x01); + } + void previous() { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x02); + } void play_file(uint16_t file) { this->ack_set_is_playing_ = true; this->send_cmd_(0x03, file); } - void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); } + void play_file_loop(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x08, file); + } void play_folder(uint16_t folder, uint16_t file); - void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); } + void play_folder_loop(uint16_t folder) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x17, folder); + } void volume_up() { this->send_cmd_(0x04); } void volume_down() { this->send_cmd_(0x05); } void set_device(Device device) { this->send_cmd_(0x09, device); } void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } - void sleep() { this->send_cmd_(0x0A); } - void reset() { this->send_cmd_(0x0C); } - void start() { this->send_cmd_(0x0D); } + void sleep() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0A); + } + void reset() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0C); + } + void start() { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x0D); + } void pause() { this->ack_reset_is_playing_ = true; this->send_cmd_(0x0E); } - void stop() { this->send_cmd_(0x16); } - void random() { this->send_cmd_(0x18); } + void stop() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x16); + } + void random() { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x18); + } bool is_playing() { return is_playing_; } void dump_config() override; From 9580b13b9f7f62b505b19e4b7856d723bbdf3ac9 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 12 Nov 2019 11:28:23 -0300 Subject: [PATCH 075/412] fix esphome better error out (#843) * fix esphome better error out * lint * not in then --- esphome/__main__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 6d3705c87e..3ce116739b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -188,6 +188,10 @@ def upload_program(config, args, host): from esphome import espota2 + if CONF_OTA not in config: + raise EsphomeError("Cannot upload Over the Air as the config does not include the ota: " + "component") + ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] password = ota_conf[CONF_PASSWORD] From fad05d5a2e681bd6c2c51da827b9496e45c948ee Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 17:39:27 +0100 Subject: [PATCH 076/412] Add wifi output_power setting (#853) * Add wifi output_power setting See also: - https://github.com/esphome/feature-requests/issues/471#issuecomment-552350467 - https://github.com/esp8266/Arduino/issues/6366 - https://github.com/esp8266/Arduino/issues/6471 - https://github.com/xoseperez/espurna/blob/849f8cf920096fa4b804e70913dab0917ee18ad9/code/espurna/config/general.h#L593-L599 - https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/network/esp_wifi.html#_CPPv425esp_wifi_set_max_tx_power6int8_t * Lint --- esphome/components/wifi/__init__.py | 4 ++++ esphome/components/wifi/wifi_component.cpp | 6 ++++++ esphome/components/wifi/wifi_component.h | 3 +++ esphome/components/wifi/wifi_component_esp32.cpp | 4 ++++ esphome/components/wifi/wifi_component_esp8266.cpp | 5 +++++ esphome/config_validation.py | 1 + 6 files changed, 23 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 818d3c84e0..818fb70105 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -110,6 +110,7 @@ def validate(config): return config +CONF_OUTPUT_POWER = 'output_power' CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiComponent), cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), @@ -125,6 +126,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_OUTPUT_POWER): cv.All(cv.decibel, cv.float_range(min=10.0, max=20.5)), cv.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), }), validate) @@ -186,6 +188,8 @@ def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT])) + if CONF_OUTPUT_POWER in config: + cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) if CORE.is_esp8266: cg.add_library('ESP8266WiFi', None) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index cb664d3cc3..60b7fb8945 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -36,6 +36,9 @@ void WiFiComponent::setup() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); + if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { + ESP_LOGV(TAG, "Setting Power Save Option failed!"); + } if (!this->wifi_apply_power_save_()) { ESP_LOGV(TAG, "Setting Power Save Option failed!"); @@ -49,6 +52,9 @@ void WiFiComponent::setup() { } } else if (this->has_ap()) { this->setup_ap_config_(); + if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { + ESP_LOGV(TAG, "Setting Power Save Option failed!"); + } #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) captive_portal::global_captive_portal->start(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 04866ef8e2..d04e1c2ce0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -162,6 +162,7 @@ class WiFiComponent : public Component { bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); + void set_output_power(float output_power) { output_power_ = output_power; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -217,6 +218,7 @@ class WiFiComponent : public Component { bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); + bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); bool wifi_sta_ip_config_(optional manual_ip); IPAddress wifi_sta_ip_(); @@ -260,6 +262,7 @@ class WiFiComponent : public Component { std::vector scan_result_; bool scan_done_{false}; bool ap_setup_{false}; + optional output_power_; }; extern WiFiComponent *global_wifi_component; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 353a51040a..7fc459d858 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -53,6 +53,10 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } +bool WiFiComponent::wifi_apply_output_power_(float output_power) { + int8_t val = static_cast(output_power * 4); + return esp_wifi_set_max_tx_power(val) == ESP_OK; +} bool WiFiComponent::wifi_sta_pre_setup_() { if (!this->wifi_mode_(true, {})) return false; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ff0dd57ed4..ee67fd36df 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -390,6 +390,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { WiFiMockClass::_event_callback(event); } +bool WiFiComponent::wifi_apply_output_power_(float output_power) { + uint8_t val = static_cast(output_power * 4); + system_phy_set_max_tpw(val); + return true; +} bool WiFiComponent::wifi_sta_pre_setup_() { if (!this->wifi_mode_(true, {})) return false; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 956779f655..5c7255a874 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -616,6 +616,7 @@ angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) _temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") _temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") _temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") +decibel = float_with_unit("decibel", u"(dB|dBm|db|dbm)", optional_unit=True) if IS_PY2: # Override voluptuous invalid to unicode for py2 From fb055750df912a56e406cf151c4513195867f792 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 18:58:26 +0100 Subject: [PATCH 077/412] Add missing state attribute (#851) * Add api missing_state attribute Fixes https://github.com/esphome/issues/issues/828 Adds a new property for missing state, so that HA can now when a sensor does not have a state yet. * Update api.proto --- esphome/components/api/api.proto | 9 +++++ esphome/components/api/api_connection.cpp | 3 ++ esphome/components/api/api_pb2.cpp | 39 ++++++++++++++++++++++ esphome/components/api/api_pb2.h | 17 ++++++---- esphome/components/api/subscribe_state.cpp | 9 ----- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 175bd3858f..4e55744384 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -216,6 +216,9 @@ message BinarySensorStateResponse { fixed32 key = 1; bool state = 2; + // If the binary sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; } // ==================== COVER ==================== @@ -416,6 +419,9 @@ message SensorStateResponse { fixed32 key = 1; float state = 2; + // If the sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; } // ==================== SWITCH ==================== @@ -472,6 +478,9 @@ message TextSensorStateResponse { fixed32 key = 1; string state = 2; + // If the text sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; } // ==================== SUBSCRIBE LOGS ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e786fe61be..d4c4a52054 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -163,6 +163,7 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary BinarySensorStateResponse resp; resp.key = binary_sensor->get_object_id_hash(); resp.state = state; + resp.missing_state = !binary_sensor->has_state(); return this->send_binary_sensor_state_response(resp); } bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { @@ -362,6 +363,7 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) { SensorStateResponse resp{}; resp.key = sensor->get_object_id_hash(); resp.state = state; + resp.missing_state = !sensor->has_state(); return this->send_sensor_state_response(resp); } bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { @@ -419,6 +421,7 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, TextSensorStateResponse resp{}; resp.key = text_sensor->get_object_id_hash(); resp.state = std::move(state); + resp.missing_state = !text_sensor->has_state(); return this->send_text_sensor_state_response(resp); } bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 7f45d38f1b..0b6021c224 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -404,6 +404,10 @@ bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->state = value.as_bool(); return true; } + case 3: { + this->missing_state = value.as_bool(); + return true; + } default: return false; } @@ -421,6 +425,7 @@ bool BinarySensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); + buffer.encode_bool(3, this->missing_state); } void BinarySensorStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -433,6 +438,10 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append(" state: "); out.append(YESNO(this->state)); out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); out.append("}"); } bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -1451,6 +1460,16 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -1468,6 +1487,7 @@ bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); + buffer.encode_bool(3, this->missing_state); } void SensorStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -1481,6 +1501,10 @@ void SensorStateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->state); out.append(buffer); out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); out.append("}"); } bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -1700,6 +1724,16 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} bool TextSensorStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -1723,6 +1757,7 @@ bool TextSensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); + buffer.encode_bool(3, this->missing_state); } void TextSensorStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -1735,6 +1770,10 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append(" state: "); out.append("'").append(this->state).append("'"); out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); out.append("}"); } bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index eb6a15afd4..3fe64fcb61 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -188,8 +188,9 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { }; class BinarySensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool missing_state{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -380,13 +381,15 @@ class ListEntitiesSensorResponse : public ProtoMessage { }; class SensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - float state{0.0f}; // NOLINT + uint32_t key{0}; // NOLINT + float state{0.0f}; // NOLINT + bool missing_state{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesSwitchResponse : public ProtoMessage { public: @@ -442,14 +445,16 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { }; class TextSensorStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - std::string state{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string state{}; // NOLINT + bool missing_state{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class SubscribeLogsRequest : public ProtoMessage { public: diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 50d674bee2..2612a852d3 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -7,9 +7,6 @@ namespace api { #ifdef USE_BINARY_SENSOR bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - if (!binary_sensor->has_state()) - return true; - return this->client_->send_binary_sensor_state(binary_sensor, binary_sensor->state); } #endif @@ -24,9 +21,6 @@ bool InitialStateIterator::on_light(light::LightState *light) { return this->cli #endif #ifdef USE_SENSOR bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { - if (!sensor->has_state()) - return true; - return this->client_->send_sensor_state(sensor, sensor->state); } #endif @@ -37,9 +31,6 @@ bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { #endif #ifdef USE_TEXT_SENSOR bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - if (!text_sensor->has_state()) - return true; - return this->client_->send_text_sensor_state(text_sensor, text_sensor->state); } #endif From 907c14aa981cafd891a387982e921aa432f0b053 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 18:59:23 +0100 Subject: [PATCH 078/412] Fix neopixelbus missing method pins (#848) Fixes https://github.com/esphome/issues/issues/839 --- esphome/components/neopixelbus/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index e5106d4bd6..e405b3d7a8 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -74,7 +74,11 @@ def validate_method_pin(value): method_pins['BIT_BANG'] = list(range(0, 16)) elif CORE.is_esp32: method_pins['BIT_BANG'] = list(range(0, 32)) - pins_ = method_pins[method] + pins_ = method_pins.get(method) + if pins_ is None: + # all pins allowed for this method + return value + for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): if opt in value and value[opt] not in pins_: raise cv.Invalid("Method {} only supports pin(s) {}".format( From 51233e1931bc7f2a35e830ea2ce1c54332d5da1b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 19:01:36 +0100 Subject: [PATCH 079/412] Fix sensor force_update native API (#847) Fixes https://github.com/esphome/issues/issues/842 --- esphome/components/api/api_connection.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d4c4a52054..a329e81cee 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -377,6 +377,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.icon = sensor->get_icon(); msg.unit_of_measurement = sensor->get_unit_of_measurement(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); + msg.force_update = sensor->get_force_update(); return this->send_list_entities_sensor_response(msg); } #endif From 7d4f2792061938f54a0ebee5c434e46b9d5c2d25 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 19:03:59 +0100 Subject: [PATCH 080/412] Web server CORS headers (#840) * Add CORS header to web server * Refactor * Cleanup See also https://github.com/esphome/issues/issues/806 --- esphome/components/web_server/web_server.cpp | 22 +++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4fdbbbce7d..c6204533d4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -18,6 +18,8 @@ namespace web_server { static const char *TAG = "web_server"; void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &klass, const std::string &action) { + if (obj->is_internal()) + return; stream->print("print(klass.c_str()); stream->print("\" id=\""); @@ -135,41 +137,37 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("\">

")); stream->print(title.c_str()); stream->print(F("

States

")); + // All content is controlled and created by user - so allowing all origins is fine here. + stream->addHeader("Access-Control-Allow-Origin", "*"); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) - if (!obj->is_internal()) - write_row(stream, obj, "sensor", ""); + write_row(stream, obj, "sensor", ""); #endif #ifdef USE_SWITCH for (auto *obj : App.get_switches()) - if (!obj->is_internal()) - write_row(stream, obj, "switch", ""); + write_row(stream, obj, "switch", ""); #endif #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) - if (!obj->is_internal()) - write_row(stream, obj, "binary_sensor", ""); + write_row(stream, obj, "binary_sensor", ""); #endif #ifdef USE_FAN for (auto *obj : App.get_fans()) - if (!obj->is_internal()) - write_row(stream, obj, "fan", ""); + write_row(stream, obj, "fan", ""); #endif #ifdef USE_LIGHT for (auto *obj : App.get_lights()) - if (!obj->is_internal()) - write_row(stream, obj, "light", ""); + write_row(stream, obj, "light", ""); #endif #ifdef USE_TEXT_SENSOR for (auto *obj : App.get_text_sensors()) - if (!obj->is_internal()) - write_row(stream, obj, "text_sensor", ""); + write_row(stream, obj, "text_sensor", ""); #endif stream->print(F("
NameStateActions

See ESPHome Web API for " From 7b142525b474c25a553ac0df907b395fd510454b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 19:04:11 +0100 Subject: [PATCH 081/412] Check DHT sensor exists before publishing (#850) Fixes https://github.com/esphome/issues/issues/841 --- esphome/components/dht/dht.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 1e28246bee..23d8c1d3e2 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -47,8 +47,10 @@ void DHT::update() { if (error) { ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); - this->temperature_sensor_->publish_state(temperature); - this->humidity_sensor_->publish_state(humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); this->status_clear_warning(); } else { const char *str = ""; @@ -56,8 +58,10 @@ void DHT::update() { str = " and consider manually specifying the DHT model using the model option"; } ESP_LOGW(TAG, "Invalid readings! Please check your wiring (pull-up resistor, pin number)%s.", str); - this->temperature_sensor_->publish_state(NAN); - this->humidity_sensor_->publish_state(NAN); + 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(); } } From 4f1a28d46013e3d504b1379275a3c6bd28e43bb1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 19:04:39 +0100 Subject: [PATCH 082/412] Adjust some units (#852) * Adjust some units Fixes https://github.com/esphome/issues/issues/843 * Lint --- esphome/components/atm90e32/sensor.py | 7 ++++--- esphome/components/pzemac/sensor.py | 4 ++-- esphome/components/tx20/sensor.py | 4 ++-- esphome/const.py | 4 +++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 030ff90a77..a5d11bbb75 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -4,7 +4,7 @@ from esphome.components import sensor, spi from esphome.const import \ CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, \ ICON_FLASH, ICON_LIGHTBULB, ICON_CURRENT_AC, ICON_THERMOMETER, \ - UNIT_HZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, UNIT_CELSIUS + UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE CONF_PHASE_A = 'phase_a' CONF_PHASE_B = 'phase_b' @@ -33,7 +33,8 @@ ATM90E32_PHASE_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_EMPTY, ICON_LIGHTBULB, 2), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_VOLT_AMPS_REACTIVE, + ICON_LIGHTBULB, 2), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t, @@ -44,7 +45,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, - cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HZ, ICON_CURRENT_AC, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 35d8069767..54eb01e085 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, modbus from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ - ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ AUTO_LOAD = ['modbus'] @@ -15,7 +15,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), - cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), }).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index daa6677196..3547cdf50c 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -4,7 +4,7 @@ from esphome import pins from esphome.components import sensor from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \ CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \ - UNIT_EMPTY, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION + ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION, UNIT_DEGREES tx20_ns = cg.esphome_ns.namespace('tx20') Tx20Component = tx20_ns.class_('Tx20Component', cg.Component) @@ -14,7 +14,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema(UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1), cv.Optional(CONF_WIND_DIRECTION_DEGREES): - sensor.sensor_schema(UNIT_EMPTY, ICON_SIGN_DIRECTION, 1), + sensor.sensor_schema(UNIT_DEGREES, ICON_SIGN_DIRECTION, 1), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt), }).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/const.py b/esphome/const.py index 5867666576..829b4738ce 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -539,7 +539,7 @@ UNIT_DEGREES = u'°' UNIT_EMPTY = '' UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' -UNIT_HZ = 'hz' +UNIT_HERTZ = 'hz' UNIT_KELVIN = 'K' UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' @@ -557,6 +557,8 @@ UNIT_PULSES_PER_MINUTE = 'pulses/min' UNIT_SECOND = 's' UNIT_STEPS = 'steps' UNIT_VOLT = 'V' +UNIT_VOLT_AMPS = 'VA' +UNIT_VOLT_AMPS_REACTIVE = 'VAR' UNIT_WATT = 'W' DEVICE_CLASS_CONNECTIVITY = 'connectivity' From 8677d477773a428f6ae2e37a46132c30d4d9a361 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 21:44:20 +0100 Subject: [PATCH 083/412] Fix PZEM004T v2 (#846) Fixes https://github.com/esphome/issues/issues/817 --- esphome/components/pzem004t/pzem004t.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index cbdc14f0d0..e2d832b019 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -8,7 +8,7 @@ static const char *TAG = "pzem004t"; void PZEM004T::loop() { const uint32_t now = millis(); - if (now - this->last_read_ > 500 && this->available()) { + if (now - this->last_read_ > 500 && this->available() < 7) { while (this->available()) this->read(); this->last_read_ = now; @@ -78,7 +78,7 @@ void PZEM004T::loop() { this->last_read_ = now; } } -void PZEM004T::update() { this->write_state_(SET_ADDRESS); } +void PZEM004T::update() { this->write_state_(READ_VOLTAGE); } void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { if (state == DONE) { this->read_state_ = state; From 663f84f8b465e033dff152954e60c039cd9a8337 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 21:44:28 +0100 Subject: [PATCH 084/412] Mark python 3.5 support deprecated (#849) * Mark python 3.5 unsupported Fixes https://github.com/esphome/issues/issues/831 * Update .travis.yml * Update typing dep --- .travis.yml | 6 +++--- esphome/__main__.py | 6 +++++- setup.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa848877c6..c7f75a2cb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: python -python: '3.5' +python: '3.6' install: script/setup cache: directories: @@ -15,8 +15,8 @@ matrix: - script/ci-custom.py - flake8 esphome - pylint esphome - - python: "3.5" - env: TARGET=Test3.5 + - python: "3.6" + env: TARGET=Test3.6 script: - esphome tests/test1.yaml compile - esphome tests/test2.yaml compile diff --git a/esphome/__main__.py b/esphome/__main__.py index 3ce116739b..32be45f5d6 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -14,7 +14,7 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.py_compat import IS_PY2, safe_input +from esphome.py_compat import IS_PY2, safe_input, IS_PY3 from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files _LOGGER = logging.getLogger(__name__) @@ -518,6 +518,10 @@ def run_esphome(argv): _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " "or higher.") + elif IS_PY3 and sys.version_info < (3, 6, 0): + _LOGGER.warning("You're using ESPHome with python 3.5. Support for python 3.5 is " + "deprecated and will be removed in 1.15.0. Please reinstall ESPHome with " + "python 3.6 or higher.") if args.command in PRE_CONFIG_ACTIONS: try: diff --git a/setup.py b/setup.py index 53acef5a30..641bb3e431 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ REQUIRES = [ 'paho-mqtt==1.4.0', 'colorlog==4.0.2', 'tornado==5.1.1', - 'typing>=3.6.6;python_version<"3.5"', + 'typing>=3.6.6;python_version<"3.6"', 'protobuf==3.10.0', 'tzlocal==2.0.0', 'pytz==2019.3', From 39a520f552917582388d3a1766d514cc088355be Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 12 Nov 2019 13:24:13 -0800 Subject: [PATCH 085/412] add position reporting to the template cover (#821) * add position reporting to the template cover * remove duplicate import * use config flag instead Co-authored-by: Samuel Sieb --- esphome/components/template/cover/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 13370d749c..7a60096d85 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -16,11 +16,14 @@ RESTORE_MODES = { 'RESTORE_AND_CALL': TemplateCoverRestoreMode.COVER_RESTORE_AND_CALL, } +CONF_HAS_POSITION = 'has_position' + CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(TemplateCover), cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_HAS_POSITION, default=False): cv.boolean, cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), @@ -56,6 +59,7 @@ def to_code(config): cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) @automation.register_action('cover.template.publish', cover.CoverPublishAction, cv.Schema({ From a386bb476f56b8285a820be97bd5609aa2e36832 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 22:26:35 +0100 Subject: [PATCH 086/412] Fix output_power log strings, lint --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp32.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 60b7fb8945..e68ab1765b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -37,7 +37,7 @@ void WiFiComponent::setup() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed!"); } if (!this->wifi_apply_power_save_()) { @@ -53,7 +53,7 @@ void WiFiComponent::setup() { } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed!"); } #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 7fc459d858..862db7a9de 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -54,7 +54,7 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } bool WiFiComponent::wifi_apply_output_power_(float output_power) { - int8_t val = static_cast(output_power * 4); + int8_t val = static_cast(output_power * 4); return esp_wifi_set_max_tx_power(val) == ESP_OK; } bool WiFiComponent::wifi_sta_pre_setup_() { From 092bca0d6345555ecaf07e796ccef5537bd5d25a Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 13 Nov 2019 18:49:59 +1100 Subject: [PATCH 087/412] Atm90e32 pf fix (#841) * correct set_pf_sensor to set_power_factor_senor * remove junk files added in error * correct sensors.yaml reference to set_reactive_power * Fixes --- esphome/components/atm90e32/sensor.py | 4 ++-- tests/test1.yaml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index a5d11bbb75..520dfc82ef 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -74,10 +74,10 @@ def to_code(config): cg.add(var.set_power_sensor(i, sens)) if CONF_REACTIVE_POWER in conf: sens = yield sensor.new_sensor(conf[CONF_REACTIVE_POWER]) - cg.add(var.set_react_pow_sensor(i, sens)) + cg.add(var.set_reactive_power_sensor(i, sens)) if CONF_POWER_FACTOR in conf: sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) - cg.add(var.set_pf_sensor(i, sens)) + cg.add(var.set_power_factor_sensor(i, sens)) if CONF_FREQUENCY in config: sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 0905c51efa..e9f8765167 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -261,6 +261,10 @@ sensor: name: "EMON CT1 Current" power: name: "EMON Active Power CT1" + reactive_power: + name: "EMON Reactive Power CT1" + power_factor: + name: "EMON Power Factor CT1" gain_voltage: 47660 gain_ct: 12577 phase_b: From 694395ac91af447895f85000a4d8acddb2497b11 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 14 Nov 2019 12:42:50 +0100 Subject: [PATCH 088/412] Switch to 115200 baud upload if 460800 fails (#856) * Switch to 115200 baud upload if 460800 fails * Update __main__.py --- esphome/__main__.py | 27 +++++++++++++++++++-------- esphome/util.py | 2 ++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 32be45f5d6..ae2db3e35c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -165,16 +165,27 @@ def compile_program(args, config): def upload_using_esptool(config, port): path = CORE.firmware_bin - cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset', - '--baud', str(config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800)), - '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path] + first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800) - if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: - import esptool - # pylint: disable=protected-access - return run_external_command(esptool._main, *cmd) + def run_esptool(baud_rate): + cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset', + '--baud', str(baud_rate), + '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path] - return run_external_process(*cmd) + if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: + import esptool + # pylint: disable=protected-access + return run_external_command(esptool._main, *cmd) + + return run_external_process(*cmd) + + rc = run_esptool(first_baudrate) + if rc == 0 or first_baudrate == 115200: + return rc + # Try with 115200 baud rate, with some serial chips the faster baud rates do not work well + _LOGGER.info("Upload with baud rate %s failed. Trying again with baud rate 115200.", + first_baudrate) + return run_esptool(115200) def upload_program(config, args, host): diff --git a/esphome/util.py b/esphome/util.py index 098d5e52da..b8e65cd576 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -184,6 +184,7 @@ def run_external_command(func, *cmd, **kwargs): except Exception as err: # pylint: disable=broad-except _LOGGER.error(u"Running command failed: %s", err) _LOGGER.error(u"Please try running %s locally.", full_cmd) + return 1 finally: sys.argv = orig_argv sys.exit = orig_exit @@ -216,6 +217,7 @@ def run_external_process(*cmd, **kwargs): except Exception as err: # pylint: disable=broad-except _LOGGER.error(u"Running command failed: %s", err) _LOGGER.error(u"Please try running %s locally.", full_cmd) + return 1 finally: if capture_stdout: # pylint: disable=lost-exception From aca306d1202b2577f99fab9792296b07c9102b12 Mon Sep 17 00:00:00 2001 From: Brandon Davidson Date: Thu, 14 Nov 2019 04:36:55 -0800 Subject: [PATCH 089/412] Fix logger uart conflict check (#858) * Fix logger uart conflict check * Fix class for check func * Fix syntax Hope lint is OK with moving the end of the conditional outside the #IFDEF * Move end of conditional inside ifdef and remove extra whitespace * Simplify clang-format did not like the ifdefs and was reformatting in a way that killed clang-tidy. Simple solution is to use logger's hw_serial as source of truth Also simplifies the code - uart doesn't need to know what the logger uart settings mean --- esphome/components/logger/logger.h | 1 + esphome/components/uart/uart.cpp | 28 +++++++++++++++------------- esphome/components/uart/uart.h | 1 + 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9d252af515..039ad78c63 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -32,6 +32,7 @@ class Logger : public Component { /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } + HardwareSerial *get_hw_serial() const { return hw_serial_; } /// Get the UART used by the logger. UARTSelection get_uart() const; diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 3284d4cb67..1f40498606 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -46,12 +46,7 @@ void UARTComponent::dump_config() { } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); -#ifdef USE_LOGGER - if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { - ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " - "disable logging over the serial port by setting logger->baud_rate to 0."); - } -#endif + this->check_logger_conflict_(); } void UARTComponent::write_byte(uint8_t data) { @@ -156,13 +151,7 @@ void UARTComponent::dump_config() { } else { ESP_LOGCONFIG(TAG, " Using software serial"); } - -#ifdef USE_LOGGER - if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { - ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " - "disable logging over the serial port by setting logger->baud_rate to 0."); - } -#endif + this->check_logger_conflict_(); } void UARTComponent::write_byte(uint8_t data) { @@ -378,6 +367,19 @@ int UARTComponent::peek() { return data; } +void UARTComponent::check_logger_conflict_() { +#ifdef USE_LOGGER + if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) { + return; + } + + if (this->hw_serial_ == logger::global_logger->get_hw_serial()) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif +} + void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { if (this->parent_->baud_rate_ != baud_rate) { ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 0e92fed0dc..76e7496b80 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -76,6 +76,7 @@ class UARTComponent : public Component, public Stream { void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } protected: + void check_logger_conflict_(); bool check_read_timeout_(size_t len = 1); friend class UARTDevice; From 4f8f59f705e2d5ed8cdc62629cac76f437257b79 Mon Sep 17 00:00:00 2001 From: Brandon Davidson Date: Thu, 14 Nov 2019 04:43:44 -0800 Subject: [PATCH 090/412] Tuya: Fix init sequence and handle wifi test command (#820) * Handle WiFi test command Also rename commands to match Tuya protocol docs * Fix init sequence and product info check * Fix clang-format suggestions * Additional changes based on code review * Fix temp command buffer scope * Let the interval timer fire the first heatbeat * Fix init steps; add logging * Lint * Remove setup_priority override * Add delay to dump_config * Refactor dump sequence * Fix verbose logging * Fix lints * Don't bother suppressing duplicate config dumps * nolint Co-authored-by: Otto Winter --- esphome/components/tuya/tuya.cpp | 93 +++++++++++++++++++++----------- esphome/components/tuya/tuya.h | 24 ++++++--- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index cb796644c8..b21df81d1e 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -8,7 +8,6 @@ namespace tuya { static const char *TAG = "tuya"; void Tuya::setup() { - this->send_empty_command_(TuyaCommandType::MCU_CONF); this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); } @@ -22,8 +21,12 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); - if ((gpio_status_ != -1) || (gpio_reset_ != -1)) - ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + if (this->init_state_ != TuyaInitState::INIT_DONE) { + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT + this->init_state_); + ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); + return; + } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::BOOLEAN) ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); @@ -36,9 +39,11 @@ void Tuya::dump_config() { else ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); } - if (this->datapoints_.empty()) { - ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); + if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) { + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_, + this->gpio_reset_); } + ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); this->check_uart_settings(9600); } @@ -89,8 +94,8 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; - ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, - hexencode(message_data, length).c_str()); + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT + hexencode(message_data, length).c_str(), this->init_state_); this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -105,41 +110,58 @@ void Tuya::handle_char_(uint8_t c) { } void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { - uint8_t c; switch ((TuyaCommandType) command) { case TuyaCommandType::HEARTBEAT: ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); if (buffer[0] == 0) { ESP_LOGI(TAG, "MCU restarted"); - this->send_empty_command_(TuyaCommandType::QUERY_STATE); + this->init_state_ = TuyaInitState::INIT_HEARTBEAT; + } + if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) { + this->init_state_ = TuyaInitState::INIT_PRODUCT; + this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY); } break; - case TuyaCommandType::QUERY_PRODUCT: { - // check it is a valid string - bool valid = false; + case TuyaCommandType::PRODUCT_QUERY: { + // check it is a valid string made up of printable characters + bool valid = true; for (int i = 0; i < len; i++) { - if (buffer[i] == 0x00) { - valid = true; + if (!std::isprint(buffer[i])) { + valid = false; break; } } if (valid) { - ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast(buffer)); + this->product_ = std::string(reinterpret_cast(buffer), len); + } else { + this->product_ = R"({"p":"INVALID"})"; + } + if (this->init_state_ == TuyaInitState::INIT_PRODUCT) { + this->init_state_ = TuyaInitState::INIT_CONF; + this->send_empty_command_(TuyaCommandType::CONF_QUERY); } break; } - case TuyaCommandType::MCU_CONF: + case TuyaCommandType::CONF_QUERY: { if (len >= 2) { gpio_status_ = buffer[0]; gpio_reset_ = buffer[1]; } - // set wifi state LED to off or on depending on the MCU firmware - // but it shouldn't be blinking - c = 0x3; - this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); - this->send_empty_command_(TuyaCommandType::QUERY_STATE); + if (this->init_state_ == TuyaInitState::INIT_CONF) { + // If we were following the spec to the letter we would send + // state updates until connected to both WiFi and API/MQTT. + // Instead we just claim to be connected immediately and move on. + uint8_t c[] = {0x04}; + this->init_state_ = TuyaInitState::INIT_WIFI; + this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); + } break; + } case TuyaCommandType::WIFI_STATE: + if (this->init_state_ == TuyaInitState::INIT_WIFI) { + this->init_state_ = TuyaInitState::INIT_DATAPOINT; + this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + } break; case TuyaCommandType::WIFI_RESET: ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); @@ -147,14 +169,22 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::WIFI_SELECT: ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); break; - case TuyaCommandType::SET_DATAPOINT: + case TuyaCommandType::DATAPOINT_DELIVER: break; - case TuyaCommandType::STATE: { + case TuyaCommandType::DATAPOINT_REPORT: + if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) { + this->init_state_ = TuyaInitState::INIT_DONE; + this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); + } this->handle_datapoint_(buffer, len); break; - } - case TuyaCommandType::QUERY_STATE: + case TuyaCommandType::DATAPOINT_QUERY: break; + case TuyaCommandType::WIFI_TEST: { + uint8_t c[] = {0x00, 0x00}; + this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); + break; + } default: ESP_LOGE(TAG, "invalid command (%02x) received", command); } @@ -214,8 +244,6 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { } if (!found) { this->datapoints_.push_back(datapoint); - // New datapoint found, reprint dump_config after a delay. - this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); } // Run through listeners @@ -227,9 +255,12 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { uint8_t len_hi = len >> 8; uint8_t len_lo = len >> 0; - this->write_array({0x55, 0xAA, - 0x00, // version - (uint8_t) command, len_hi, len_lo}); + uint8_t version = 0; + + ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT + hexencode(buffer, len).c_str(), this->init_state_); + + this->write_array({0x55, 0xAA, version, (uint8_t) command, len_hi, len_lo}); if (len != 0) this->write_array(buffer, len); @@ -278,7 +309,7 @@ void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { buffer.push_back(data.size() >> 8); buffer.push_back(data.size() >> 0); buffer.insert(buffer.end(), data.begin(), data.end()); - this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); + this->send_command_(TuyaCommandType::DATAPOINT_DELIVER, buffer.data(), buffer.size()); } void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 6bc6d92da0..2fc9a16d44 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -34,19 +34,29 @@ struct TuyaDatapointListener { enum class TuyaCommandType : uint8_t { HEARTBEAT = 0x00, - QUERY_PRODUCT = 0x01, - MCU_CONF = 0x02, + PRODUCT_QUERY = 0x01, + CONF_QUERY = 0x02, WIFI_STATE = 0x03, WIFI_RESET = 0x04, WIFI_SELECT = 0x05, - SET_DATAPOINT = 0x06, - STATE = 0x07, - QUERY_STATE = 0x08, + DATAPOINT_DELIVER = 0x06, + DATAPOINT_REPORT = 0x07, + DATAPOINT_QUERY = 0x08, + WIFI_TEST = 0x0E, +}; + +enum class TuyaInitState : uint8_t { + INIT_HEARTBEAT = 0x00, + INIT_PRODUCT, + INIT_CONF, + INIT_WIFI, + INIT_DATAPOINT, + INIT_DONE, }; class Tuya : public Component, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::LATE; } void setup() override; void loop() override; void dump_config() override; @@ -62,8 +72,10 @@ class Tuya : public Component, public uart::UARTDevice { void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; int gpio_status_ = -1; int gpio_reset_ = -1; + std::string product_ = ""; std::vector listeners_; std::vector datapoints_; std::vector rx_message_; From 1814e4a46bf7ec7eb42294c44e8535b24ee06348 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 16 Nov 2019 12:34:11 -0300 Subject: [PATCH 091/412] Add climate dry fan (#845) * add climate dry fan * clang-format * updates, add swing mode, add back compat with old ha * revert client-config add swing * sort const.py * fix missing retur --- esphome/components/api/api.proto | 27 ++++ esphome/components/api/api_connection.cpp | 21 ++- esphome/components/api/api_pb2.cpp | 124 ++++++++++++++++ esphome/components/api/api_pb2.h | 69 ++++++--- esphome/components/api/api_pb2_service.cpp | 2 + esphome/components/api/api_pb2_service.h | 2 + esphome/components/climate/__init__.py | 38 ++++- esphome/components/climate/automation.h | 4 + esphome/components/climate/climate.cpp | 108 ++++++++++++++ esphome/components/climate/climate.h | 24 ++++ esphome/components/climate/climate_mode.cpp | 44 ++++++ esphome/components/climate/climate_mode.h | 46 ++++++ esphome/components/climate/climate_traits.cpp | 94 +++++++++++++ esphome/components/climate/climate_traits.h | 40 ++++++ esphome/components/climate_ir/climate_ir.cpp | 56 +++++++- esphome/components/climate_ir/climate_ir.h | 13 +- esphome/components/coolix/coolix.cpp | 133 +++++++++++------- esphome/components/coolix/coolix.h | 17 ++- esphome/components/mqtt/mqtt_climate.cpp | 10 ++ esphome/const.py | 2 + script/api_protobuf/api_protobuf.py | 35 ++++- 21 files changed, 824 insertions(+), 85 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4e55744384..2e01856a3b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -653,6 +653,25 @@ enum ClimateMode { CLIMATE_MODE_AUTO = 1; CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; + CLIMATE_MODE_FAN_ONLY = 4; + CLIMATE_MODE_DRY = 5; +} +enum ClimateFanMode { + CLIMATE_FAN_ON = 0; + CLIMATE_FAN_OFF = 1; + CLIMATE_FAN_AUTO = 2; + CLIMATE_FAN_LOW = 3; + CLIMATE_FAN_MEDIUM = 4; + CLIMATE_FAN_HIGH = 5; + CLIMATE_FAN_MIDDLE = 6; + CLIMATE_FAN_FOCUS = 7; + CLIMATE_FAN_DIFFUSE = 8; +} +enum ClimateSwingMode { + CLIMATE_SWING_OFF = 0; + CLIMATE_SWING_BOTH = 1; + CLIMATE_SWING_VERTICAL = 2; + CLIMATE_SWINT_HORIZONTAL = 3; } enum ClimateAction { CLIMATE_ACTION_OFF = 0; @@ -678,6 +697,8 @@ message ListEntitiesClimateResponse { float visual_temperature_step = 10; bool supports_away = 11; bool supports_action = 12; + repeated ClimateFanMode supported_fan_modes = 13; + repeated ClimateSwingMode supported_swing_modes = 14; } message ClimateStateResponse { option (id) = 47; @@ -693,6 +714,8 @@ message ClimateStateResponse { float target_temperature_high = 6; bool away = 7; ClimateAction action = 8; + ClimateFanMode fan_mode = 9; + ClimateSwingMode swing_mode = 10; } message ClimateCommandRequest { option (id) = 48; @@ -711,4 +734,8 @@ message ClimateCommandRequest { float target_temperature_high = 9; bool has_away = 10; bool away = 11; + bool has_fan_mode = 12; + ClimateFanMode fan_mode = 13; + bool has_swing_mode = 14; + ClimateSwingMode swing_mode = 15; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a329e81cee..8844aa1e1a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -458,6 +458,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { } if (traits.get_supports_away()) resp.away = climate->away; + if (traits.get_supports_fan_modes()) + resp.fan_mode = static_cast(climate->fan_mode); + if (traits.get_supports_swing_modes()) + resp.swing_mode = static_cast(climate->swing_mode); return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -470,7 +474,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT}) { + climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) { if (traits.supports_mode(mode)) msg.supported_modes.push_back(static_cast(mode)); } @@ -479,6 +483,17 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_temperature_step = traits.get_visual_temperature_step(); msg.supports_away = traits.get_supports_away(); msg.supports_action = traits.get_supports_action(); + for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { + if (traits.supports_fan_mode(fan_mode)) + msg.supported_fan_modes.push_back(static_cast(fan_mode)); + } + for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL}) { + if (traits.supports_swing_mode(swing_mode)) + msg.supported_swing_modes.push_back(static_cast(swing_mode)); + } return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { @@ -497,6 +512,10 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_high(msg.target_temperature_high); if (msg.has_away) call.set_away(msg.away); + if (msg.has_fan_mode) + call.set_fan_mode(static_cast(msg.fan_mode)); + if (msg.has_swing_mode) + call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0b6021c224..cca488decf 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/log.h" @@ -102,6 +104,48 @@ template<> const char *proto_enum_to_string(enums::ClimateMo return "CLIMATE_MODE_COOL"; case enums::CLIMATE_MODE_HEAT: return "CLIMATE_MODE_HEAT"; + case enums::CLIMATE_MODE_FAN_ONLY: + return "CLIMATE_MODE_FAN_ONLY"; + case enums::CLIMATE_MODE_DRY: + return "CLIMATE_MODE_DRY"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { + switch (value) { + case enums::CLIMATE_FAN_ON: + return "CLIMATE_FAN_ON"; + case enums::CLIMATE_FAN_OFF: + return "CLIMATE_FAN_OFF"; + case enums::CLIMATE_FAN_AUTO: + return "CLIMATE_FAN_AUTO"; + case enums::CLIMATE_FAN_LOW: + return "CLIMATE_FAN_LOW"; + case enums::CLIMATE_FAN_MEDIUM: + return "CLIMATE_FAN_MEDIUM"; + case enums::CLIMATE_FAN_HIGH: + return "CLIMATE_FAN_HIGH"; + case enums::CLIMATE_FAN_MIDDLE: + return "CLIMATE_FAN_MIDDLE"; + case enums::CLIMATE_FAN_FOCUS: + return "CLIMATE_FAN_FOCUS"; + case enums::CLIMATE_FAN_DIFFUSE: + return "CLIMATE_FAN_DIFFUSE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { + switch (value) { + case enums::CLIMATE_SWING_OFF: + return "CLIMATE_SWING_OFF"; + case enums::CLIMATE_SWING_BOTH: + return "CLIMATE_SWING_BOTH"; + case enums::CLIMATE_SWING_VERTICAL: + return "CLIMATE_SWING_VERTICAL"; + case enums::CLIMATE_SWINT_HORIZONTAL: + return "CLIMATE_SWINT_HORIZONTAL"; default: return "UNKNOWN"; } @@ -2458,6 +2502,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_action = value.as_bool(); return true; } + case 13: { + this->supported_fan_modes.push_back(value.as_enum()); + return true; + } + case 14: { + this->supported_swing_modes.push_back(value.as_enum()); + return true; + } default: return false; } @@ -2517,6 +2569,12 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(10, this->visual_temperature_step); buffer.encode_bool(11, this->supports_away); buffer.encode_bool(12, this->supports_action); + for (auto &it : this->supported_fan_modes) { + buffer.encode_enum(13, it, true); + } + for (auto &it : this->supported_swing_modes) { + buffer.encode_enum(14, it, true); + } } void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2574,6 +2632,18 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); + + for (const auto &it : this->supported_fan_modes) { + out.append(" supported_fan_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_swing_modes) { + out.append(" supported_swing_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } out.append("}"); } bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2590,6 +2660,14 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->action = value.as_enum(); return true; } + case 9: { + this->fan_mode = value.as_enum(); + return true; + } + case 10: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2629,6 +2707,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); buffer.encode_enum(8, this->action); + buffer.encode_enum(9, this->fan_mode); + buffer.encode_enum(10, this->swing_mode); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2669,6 +2749,14 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2701,6 +2789,22 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->away = value.as_bool(); return true; } + case 12: { + this->has_fan_mode = value.as_bool(); + return true; + } + case 13: { + this->fan_mode = value.as_enum(); + return true; + } + case 14: { + this->has_swing_mode = value.as_bool(); + return true; + } + case 15: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2739,6 +2843,10 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(9, this->target_temperature_high); buffer.encode_bool(10, this->has_away); buffer.encode_bool(11, this->away); + buffer.encode_bool(12, this->has_fan_mode); + buffer.encode_enum(13, this->fan_mode); + buffer.encode_bool(14, this->has_swing_mode); + buffer.encode_enum(15, this->swing_mode); } void ClimateCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -2790,6 +2898,22 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" away: "); out.append(YESNO(this->away)); out.append("\n"); + + out.append(" has_fan_mode: "); + out.append(YESNO(this->has_fan_mode)); + out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" has_swing_mode: "); + out.append(YESNO(this->has_swing_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3fe64fcb61..fc855a889a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "proto.h" @@ -50,6 +52,25 @@ enum ClimateMode : uint32_t { CLIMATE_MODE_AUTO = 1, CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, + CLIMATE_MODE_FAN_ONLY = 4, + CLIMATE_MODE_DRY = 5, +}; +enum ClimateFanMode : uint32_t { + CLIMATE_FAN_ON = 0, + CLIMATE_FAN_OFF = 1, + CLIMATE_FAN_AUTO = 2, + CLIMATE_FAN_LOW = 3, + CLIMATE_FAN_MEDIUM = 4, + CLIMATE_FAN_HIGH = 5, + CLIMATE_FAN_MIDDLE = 6, + CLIMATE_FAN_FOCUS = 7, + CLIMATE_FAN_DIFFUSE = 8, +}; +enum ClimateSwingMode : uint32_t { + CLIMATE_SWING_OFF = 0, + CLIMATE_SWING_BOTH = 1, + CLIMATE_SWING_VERTICAL = 2, + CLIMATE_SWINT_HORIZONTAL = 3, }; enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, @@ -643,18 +664,20 @@ class CameraImageRequest : public ProtoMessage { }; class ListEntitiesClimateResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool supports_current_temperature{false}; // NOLINT - bool supports_two_point_target_temperature{false}; // NOLINT - std::vector supported_modes{}; // NOLINT - float visual_min_temperature{0.0f}; // NOLINT - float visual_max_temperature{0.0f}; // NOLINT - float visual_temperature_step{0.0f}; // NOLINT - bool supports_away{false}; // NOLINT - bool supports_action{false}; // NOLINT + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool supports_current_temperature{false}; // NOLINT + bool supports_two_point_target_temperature{false}; // NOLINT + std::vector supported_modes{}; // NOLINT + float visual_min_temperature{0.0f}; // NOLINT + float visual_max_temperature{0.0f}; // NOLINT + float visual_temperature_step{0.0f}; // NOLINT + bool supports_away{false}; // NOLINT + bool supports_action{false}; // NOLINT + std::vector supported_fan_modes{}; // NOLINT + std::vector supported_swing_modes{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -665,14 +688,16 @@ class ListEntitiesClimateResponse : public ProtoMessage { }; class ClimateStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - enums::ClimateMode mode{}; // NOLINT - float current_temperature{0.0f}; // NOLINT - float target_temperature{0.0f}; // NOLINT - float target_temperature_low{0.0f}; // NOLINT - float target_temperature_high{0.0f}; // NOLINT - bool away{false}; // NOLINT - enums::ClimateAction action{}; // NOLINT + uint32_t key{0}; // NOLINT + enums::ClimateMode mode{}; // NOLINT + float current_temperature{0.0f}; // NOLINT + float target_temperature{0.0f}; // NOLINT + float target_temperature_low{0.0f}; // NOLINT + float target_temperature_high{0.0f}; // NOLINT + bool away{false}; // NOLINT + enums::ClimateAction action{}; // NOLINT + enums::ClimateFanMode fan_mode{}; // NOLINT + enums::ClimateSwingMode swing_mode{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -693,6 +718,10 @@ class ClimateCommandRequest : public ProtoMessage { float target_temperature_high{0.0f}; // NOLINT bool has_away{false}; // NOLINT bool away{false}; // NOLINT + bool has_fan_mode{false}; // NOLINT + enums::ClimateFanMode fan_mode{}; // NOLINT + bool has_swing_mode{false}; // NOLINT + enums::ClimateSwingMode swing_mode{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 13e123c10f..ea6b647c72 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2_service.h" #include "esphome/core/log.h" diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 16662811fe..afbe39e314 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "api_pb2.h" diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8c9db58694..843b888218 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -5,7 +5,7 @@ from esphome.components import mqtt from esphome.const import CONF_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, \ CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \ CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \ - CONF_MQTT_ID, CONF_NAME + CONF_MQTT_ID, CONF_NAME, CONF_FAN_MODE, CONF_SWING_MODE from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -22,9 +22,35 @@ CLIMATE_MODES = { 'AUTO': ClimateMode.CLIMATE_MODE_AUTO, 'COOL': ClimateMode.CLIMATE_MODE_COOL, 'HEAT': ClimateMode.CLIMATE_MODE_HEAT, + 'DRY': ClimateMode.CLIMATE_MODE_DRY, + 'FAN_ONLY': ClimateMode.CLIMATE_MODE_FAN_ONLY, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) + +ClimateFanMode = climate_ns.enum('ClimateFanMode') +CLIMATE_FAN_MODES = { + 'ON': ClimateFanMode.CLIMATE_FAN_ON, + 'OFF': ClimateFanMode.CLIMATE_FAN_OFF, + 'AUTO': ClimateFanMode.CLIMATE_FAN_AUTO, + 'LOW': ClimateFanMode.CLIMATE_FAN_LOW, + 'MEDIUM': ClimateFanMode.CLIMATE_FAN_MEDIUM, + 'HIGH': ClimateFanMode.CLIMATE_FAN_HIGH, + 'MIDDLE': ClimateFanMode.CLIMATE_FAN_MIDDLE, + 'FOCUS': ClimateFanMode.CLIMATE_FAN_FOCUS, + 'DIFFUSE': ClimateFanMode.CLIMATE_FAN_DIFFUSE, } -validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) +validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) + +ClimateSwingMode = climate_ns.enum('ClimateSwingMode') +CLIMATE_SWING_MODES = { + 'OFF': ClimateSwingMode.CLIMATE_SWING_OFF, + 'BOTH': ClimateSwingMode.CLIMATE_SWING_BOTH, + 'VERTICAL': ClimateSwingMode.CLIMATE_SWING_VERTICAL, + 'HORIZONTAL': ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_('ControlAction', automation.Action) @@ -74,6 +100,8 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema({ cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), + cv.Optional(CONF_FAN_MODE): cv.templatable(validate_climate_fan_mode), + cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), }) @@ -96,6 +124,12 @@ def climate_control_to_code(config, action_id, template_arg, args): if CONF_AWAY in config: template_ = yield cg.templatable(config[CONF_AWAY], args, bool) cg.add(var.set_away(template_)) + if CONF_FAN_MODE in config: + template_ = yield cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) + cg.add(var.set_fan_mode(template_)) + if CONF_SWING_MODE in config: + template_ = yield cg.templatable(config[CONF_SWING_MODE], args, ClimateSwingMode) + cg.add(var.set_swing_mode(template_)) yield var diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 845773a0ab..0cd52b1036 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -15,6 +15,8 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(bool, away) + TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) void play(Ts... x) override { auto call = this->climate_->make_call(); @@ -23,6 +25,8 @@ template class ControlAction : public Action { call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); call.set_away(this->away_.optional_value(x...)); + call.set_fan_mode(this->fan_mode_.optional_value(x...)); + call.set_swing_mode(this->swing_mode_.optional_value(x...)); call.perform(); } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7c7da6bb0c..443290ed6d 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -13,6 +13,14 @@ void ClimateCall::perform() { const char *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", mode_s); } + if (this->fan_mode_.has_value()) { + const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); + ESP_LOGD(TAG, " Fan: %s", fan_mode_s); + } + if (this->swing_mode_.has_value()) { + const char *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); + ESP_LOGD(TAG, " Swing: %s", swing_mode_s); + } if (this->target_temperature_.has_value()) { ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); } @@ -36,6 +44,20 @@ void ClimateCall::validate_() { this->mode_.reset(); } } + if (this->fan_mode_.has_value()) { + auto fan_mode = *this->fan_mode_; + if (!traits.supports_fan_mode(fan_mode)) { + ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", climate_fan_mode_to_string(fan_mode)); + this->fan_mode_.reset(); + } + } + if (this->swing_mode_.has_value()) { + auto swing_mode = *this->swing_mode_; + if (!traits.supports_swing_mode(swing_mode)) { + ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", climate_swing_mode_to_string(swing_mode)); + this->swing_mode_.reset(); + } + } if (this->target_temperature_.has_value()) { auto target = *this->target_temperature_; if (traits.get_supports_two_point_target_temperature()) { @@ -91,11 +113,63 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { this->set_mode(CLIMATE_MODE_COOL); } else if (str_equals_case_insensitive(mode, "HEAT")) { this->set_mode(CLIMATE_MODE_HEAT); + } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) { + this->set_mode(CLIMATE_MODE_FAN_ONLY); + } else if (str_equals_case_insensitive(mode, "DRY")) { + this->set_mode(CLIMATE_MODE_DRY); } else { ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); } return *this; } +ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { + if (str_equals_case_insensitive(fan_mode, "ON")) { + this->set_fan_mode(CLIMATE_FAN_ON); + } else if (str_equals_case_insensitive(fan_mode, "OFF")) { + this->set_fan_mode(CLIMATE_FAN_OFF); + } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { + this->set_fan_mode(CLIMATE_FAN_AUTO); + } else if (str_equals_case_insensitive(fan_mode, "LOW")) { + this->set_fan_mode(CLIMATE_FAN_LOW); + } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) { + this->set_fan_mode(CLIMATE_FAN_MEDIUM); + } else if (str_equals_case_insensitive(fan_mode, "HIGH")) { + this->set_fan_mode(CLIMATE_FAN_HIGH); + } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) { + this->set_fan_mode(CLIMATE_FAN_MIDDLE); + } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) { + this->set_fan_mode(CLIMATE_FAN_FOCUS); + } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { + this->set_fan_mode(CLIMATE_FAN_DIFFUSE); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + } + return *this; +} + +ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + if (str_equals_case_insensitive(swing_mode, "OFF")) { + this->set_swing_mode(CLIMATE_SWING_OFF); + } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { + this->set_swing_mode(CLIMATE_SWING_BOTH); + } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) { + this->set_swing_mode(CLIMATE_SWING_VERTICAL); + } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) { + this->set_swing_mode(CLIMATE_SWING_HORIZONTAL); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + } + return *this; +} + ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { this->target_temperature_ = target_temperature; return *this; @@ -113,6 +187,8 @@ const optional &ClimateCall::get_target_temperature() const { return this const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional &ClimateCall::get_away() const { return this->away_; } +const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } +const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } ClimateCall &ClimateCall::set_away(bool away) { this->away_ = away; return *this; @@ -137,6 +213,14 @@ ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; } +ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); @@ -165,6 +249,12 @@ void Climate::save_state_() { if (traits.get_supports_away()) { state.away = this->away; } + if (traits.get_supports_fan_modes()) { + state.fan_mode = this->fan_mode; + } + if (traits.get_supports_swing_modes()) { + state.swing_mode = this->swing_mode; + } this->rtc_.save(&state); } @@ -176,6 +266,12 @@ void Climate::publish_state() { if (traits.get_supports_action()) { ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); } + if (traits.get_supports_fan_modes()) { + ESP_LOGD(TAG, " Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode)); + } + if (traits.get_supports_swing_modes()) { + ESP_LOGD(TAG, " Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } @@ -236,6 +332,12 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (traits.get_supports_away()) { call.set_away(this->away); } + if (traits.get_supports_fan_modes()) { + call.set_fan_mode(this->fan_mode); + } + if (traits.get_supports_swing_modes()) { + call.set_swing_mode(this->swing_mode); + } return call; } void ClimateDeviceRestoreState::apply(Climate *climate) { @@ -250,6 +352,12 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (traits.get_supports_away()) { climate->away = this->away; } + if (traits.get_supports_fan_modes()) { + climate->fan_mode = this->fan_mode; + } + if (traits.get_supports_swing_modes()) { + climate->swing_mode = this->swing_mode; + } climate->publish_state(); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 4dd872bbed..786afe097a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,18 @@ class ClimateCall { ClimateCall &set_target_temperature_high(optional target_temperature_high); ClimateCall &set_away(bool away); ClimateCall &set_away(optional away); + /// Set the fan mode of the climate device. + ClimateCall &set_fan_mode(ClimateFanMode fan_mode); + /// Set the fan mode of the climate device. + ClimateCall &set_fan_mode(optional fan_mode); + /// Set the fan mode of the climate device based on a string. + ClimateCall &set_fan_mode(const std::string &fan_mode); + /// Set the swing mode of the climate device. + ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); + /// Set the swing mode of the climate device. + ClimateCall &set_swing_mode(optional swing_mode); + /// Set the swing mode of the climate device based on a string. + ClimateCall &set_swing_mode(const std::string &swing_mode); void perform(); @@ -72,6 +84,8 @@ class ClimateCall { const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; const optional &get_away() const; + const optional &get_fan_mode() const; + const optional &get_swing_mode() const; protected: void validate_(); @@ -82,12 +96,16 @@ class ClimateCall { optional target_temperature_low_; optional target_temperature_high_; optional away_; + optional fan_mode_; + optional swing_mode_; }; /// Struct used to save the state of the climate device in restore memory. struct ClimateDeviceRestoreState { ClimateMode mode; bool away; + ClimateFanMode fan_mode; + ClimateSwingMode swing_mode; union { float target_temperature; struct { @@ -149,6 +167,12 @@ class Climate : public Nameable { */ bool away{false}; + /// The active fan mode of the climate device. + ClimateFanMode fan_mode; + + /// The active swing mode of the climate device. + ClimateSwingMode swing_mode; + /** Add a callback for the climate device state, each time the state of the climate device is updated * (using publish_state), this callback will be called. * diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 34aa564fb0..aa06ce87f0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -13,6 +13,10 @@ const char *climate_mode_to_string(ClimateMode mode) { return "COOL"; case CLIMATE_MODE_HEAT: return "HEAT"; + case CLIMATE_MODE_FAN_ONLY: + return "FAN_ONLY"; + case CLIMATE_MODE_DRY: + return "DRY"; default: return "UNKNOWN"; } @@ -30,5 +34,45 @@ const char *climate_action_to_string(ClimateAction action) { } } +const char *climate_fan_mode_to_string(ClimateFanMode fan_mode) { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return "ON"; + case climate::CLIMATE_FAN_OFF: + return "OFF"; + case climate::CLIMATE_FAN_AUTO: + return "AUTO"; + case climate::CLIMATE_FAN_LOW: + return "LOW"; + case climate::CLIMATE_FAN_MEDIUM: + return "MEDIUM"; + case climate::CLIMATE_FAN_HIGH: + return "HIGH"; + case climate::CLIMATE_FAN_MIDDLE: + return "MIDDLE"; + case climate::CLIMATE_FAN_FOCUS: + return "FOCUS"; + case climate::CLIMATE_FAN_DIFFUSE: + return "DIFFUSE"; + default: + return "UNKNOWN"; + } +} + +const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return "OFF"; + case climate::CLIMATE_SWING_BOTH: + return "BOTH"; + case climate::CLIMATE_SWING_VERTICAL: + return "VERTICAL"; + case climate::CLIMATE_SWING_HORIZONTAL: + return "HORIZONTAL"; + default: + return "UNKNOWN"; + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index e5786286d8..83ef715402 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -15,6 +15,10 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_COOL = 2, /// The climate device is manually set to heat mode (not in auto mode!) CLIMATE_MODE_HEAT = 3, + /// The climate device is manually set to fan only mode + CLIMATE_MODE_FAN_ONLY = 4, + /// The climate device is manually set to dry mode + CLIMATE_MODE_DRY = 5, }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -27,9 +31,51 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_HEATING = 3, }; +/// Enum for all modes a climate fan can be in +enum ClimateFanMode : uint8_t { + /// The fan mode is set to On + CLIMATE_FAN_ON = 0, + /// The fan mode is set to Off + CLIMATE_FAN_OFF = 1, + /// The fan mode is set to Auto + CLIMATE_FAN_AUTO = 2, + /// The fan mode is set to Low + CLIMATE_FAN_LOW = 3, + /// The fan mode is set to Medium + CLIMATE_FAN_MEDIUM = 4, + /// The fan mode is set to High + CLIMATE_FAN_HIGH = 5, + /// The fan mode is set to Middle + CLIMATE_FAN_MIDDLE = 6, + /// The fan mode is set to Focus + CLIMATE_FAN_FOCUS = 7, + /// The fan mode is set to Diffuse + CLIMATE_FAN_DIFFUSE = 8, +}; + +/// Enum for all modes a climate swing can be in +enum ClimateSwingMode : uint8_t { + /// The sing mode is set to Off + CLIMATE_SWING_OFF = 0, + /// The fan mode is set to Both + CLIMATE_SWING_BOTH = 1, + /// The fan mode is set to Vertical + CLIMATE_SWING_VERTICAL = 2, + /// The fan mode is set to Horizontal + CLIMATE_SWING_HORIZONTAL = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); + +/// Convert the given ClimateAction to a human-readable string. const char *climate_action_to_string(ClimateAction action); +/// Convert the given ClimateFanMode to a human-readable string. +const char *climate_fan_mode_to_string(ClimateFanMode mode); + +/// Convert the given ClimateSwingMode to a human-readable string. +const char *climate_swing_mode_to_string(ClimateSwingMode mode); + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index a1db2bc696..6e941bddf0 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -14,6 +14,10 @@ bool ClimateTraits::supports_mode(ClimateMode mode) const { return this->supports_cool_mode_; case CLIMATE_MODE_HEAT: return this->supports_heat_mode_; + case CLIMATE_MODE_FAN_ONLY: + return this->supports_fan_only_mode_; + case CLIMATE_MODE_DRY: + return this->supports_dry_mode_; default: return false; } @@ -29,6 +33,10 @@ void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } +void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { + supports_fan_only_mode_ = supports_fan_only_mode; +} +void ClimateTraits::set_supports_dry_mode(bool supports_dry_mode) { supports_dry_mode_ = supports_dry_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } @@ -55,5 +63,91 @@ void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual bool ClimateTraits::get_supports_away() const { return supports_away_; } bool ClimateTraits::get_supports_action() const { return supports_action_; } +void ClimateTraits::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ClimateTraits::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ClimateTraits::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ClimateTraits::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ClimateTraits::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ClimateTraits::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ClimateTraits::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ClimateTraits::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ClimateTraits::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +bool ClimateTraits::supports_fan_mode(ClimateFanMode fan_mode) const { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return this->supports_fan_mode_on_; + case climate::CLIMATE_FAN_OFF: + return this->supports_fan_mode_off_; + case climate::CLIMATE_FAN_AUTO: + return this->supports_fan_mode_auto_; + case climate::CLIMATE_FAN_LOW: + return this->supports_fan_mode_low_; + case climate::CLIMATE_FAN_MEDIUM: + return this->supports_fan_mode_medium_; + case climate::CLIMATE_FAN_HIGH: + return this->supports_fan_mode_high_; + case climate::CLIMATE_FAN_MIDDLE: + return this->supports_fan_mode_middle_; + case climate::CLIMATE_FAN_FOCUS: + return this->supports_fan_mode_focus_; + case climate::CLIMATE_FAN_DIFFUSE: + return this->supports_fan_mode_diffuse_; + default: + return false; + } +} +bool ClimateTraits::get_supports_fan_modes() const { + return this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || + this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_; +} +void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ClimateTraits::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ClimateTraits::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ClimateTraits::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +bool ClimateTraits::supports_swing_mode(ClimateSwingMode swing_mode) const { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return this->supports_swing_mode_off_; + case climate::CLIMATE_SWING_BOTH: + return this->supports_swing_mode_both_; + case climate::CLIMATE_SWING_VERTICAL: + return this->supports_swing_mode_vertical_; + case climate::CLIMATE_SWING_HORIZONTAL: + return this->supports_swing_mode_horizontal_; + default: + return false; + } +} +bool ClimateTraits::get_supports_swing_modes() const { + return this->supports_swing_mode_off_ || this->supports_swing_mode_both_ || supports_swing_mode_vertical_ || + supports_swing_mode_horizontal_; +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2d6f44eea6..347a7bc1f2 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -21,10 +21,16 @@ namespace climate { * - auto mode (automatic control) * - cool mode (lowers current temperature) * - heat mode (increases current temperature) + * - dry mode (removes humidity from air) + * - fan mode (only turns on fan) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. * - supports action - if the climate device supports reporting the active * current action of the device with the action property. + * - supports fan modes - optionally, if it has a fan which can be configured in different ways: + * - on, off, auto, high, medium, low, middle, focus, diffuse + * - supports swing modes - optionally, if it has a swing which can be configured in different ways: + * - off, both, vertical, horizontal * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,11 +47,30 @@ class ClimateTraits { void set_supports_auto_mode(bool supports_auto_mode); void set_supports_cool_mode(bool supports_cool_mode); void set_supports_heat_mode(bool supports_heat_mode); + void set_supports_fan_only_mode(bool supports_fan_only_mode); + void set_supports_dry_mode(bool supports_dry_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; void set_supports_action(bool supports_action); bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + bool supports_fan_mode(ClimateFanMode fan_mode) const; + bool get_supports_fan_modes() const; + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + bool supports_swing_mode(ClimateSwingMode swing_mode) const; + bool get_supports_swing_modes() const; float get_visual_min_temperature() const; void set_visual_min_temperature(float visual_min_temperature); @@ -61,8 +86,23 @@ class ClimateTraits { bool supports_auto_mode_{false}; bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; + bool supports_fan_only_mode_{false}; + bool supports_dry_mode_{false}; bool supports_away_{false}; bool supports_action_{false}; + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + bool supports_fan_mode_auto_{false}; + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_vertical_{false}; + bool supports_swing_mode_horizontal_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 4b9a1c0baa..8f06ff2214 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -12,11 +12,60 @@ climate::ClimateTraits ClimateIR::traits() { traits.set_supports_auto_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_two_point_target_temperature(false); traits.set_supports_away(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); + for (auto fan_mode : this->fan_modes_) { + switch (fan_mode) { + case climate::CLIMATE_FAN_AUTO: + traits.set_supports_fan_mode_auto(true); + break; + case climate::CLIMATE_FAN_DIFFUSE: + traits.set_supports_fan_mode_diffuse(true); + break; + case climate::CLIMATE_FAN_FOCUS: + traits.set_supports_fan_mode_focus(true); + break; + case climate::CLIMATE_FAN_HIGH: + traits.set_supports_fan_mode_high(true); + break; + case climate::CLIMATE_FAN_LOW: + traits.set_supports_fan_mode_low(true); + break; + case climate::CLIMATE_FAN_MEDIUM: + traits.set_supports_fan_mode_medium(true); + break; + case climate::CLIMATE_FAN_MIDDLE: + traits.set_supports_fan_mode_middle(true); + break; + case climate::CLIMATE_FAN_OFF: + traits.set_supports_fan_mode_off(true); + break; + case climate::CLIMATE_FAN_ON: + traits.set_supports_fan_mode_on(true); + break; + } + } + for (auto swing_mode : this->swing_modes_) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + traits.set_supports_swing_mode_off(true); + break; + case climate::CLIMATE_SWING_BOTH: + traits.set_supports_swing_mode_both(true); + break; + case climate::CLIMATE_SWING_VERTICAL: + traits.set_supports_swing_mode_vertical(true); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + traits.set_supports_swing_mode_horizontal(true); + break; + } + } return traits; } @@ -40,6 +89,8 @@ void ClimateIR::setup() { // initialize target temperature to some value so that it's not NAN this->target_temperature = roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; } // Never send nan to HA if (isnan(this->target_temperature)) @@ -51,7 +102,10 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->mode = *call.get_mode(); if (call.get_target_temperature().has_value()) this->target_temperature = *call.get_target_temperature(); - + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index b4c036f3d6..6dc5b43279 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -18,10 +18,17 @@ namespace climate_ir { */ class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { public: - ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, + bool supports_dry = false, bool supports_fan_only = false, + std::vector fan_modes = {}, + std::vector swing_modes = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; + this->supports_dry_ = supports_dry; + this->supports_fan_only_ = supports_fan_only; + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; } void setup() override; @@ -46,6 +53,10 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_cool_{true}; bool supports_heat_{true}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + std::vector fan_modes_ = {}; + std::vector swing_modes_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c08571c2e9..441f43b424 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -12,15 +12,13 @@ const uint32_t COOLIX_LED = 0xB5F5A5; const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; -const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; const uint8_t COOLIX_COOL = 0b0000; const uint8_t COOLIX_DRY_FAN = 0b0100; const uint8_t COOLIX_AUTO = 0b1000; const uint8_t COOLIX_HEAT = 0b1100; const uint32_t COOLIX_MODE_MASK = 0b1100; const uint32_t COOLIX_FAN_MASK = 0xF000; -const uint32_t COOLIX_FAN_DRY = 0x1000; +const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; const uint32_t COOLIX_FAN_AUTO = 0xB000; const uint32_t COOLIX_FAN_MIN = 0x9000; const uint32_t COOLIX_FAN_MED = 0x5000; @@ -28,23 +26,23 @@ const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110; // Part of Fan Mode. +const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. const uint32_t COOLIX_TEMP_MASK = 0b11110000; const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { - 0b0000, // 17C - 0b0001, // 18c - 0b0011, // 19C - 0b0010, // 20C - 0b0110, // 21C - 0b0111, // 22C - 0b0101, // 23C - 0b0100, // 24C - 0b1100, // 25C - 0b1101, // 26C - 0b1001, // 27C - 0b1000, // 28C - 0b1010, // 29C - 0b1011 // 30C + 0b00000000, // 17C + 0b00010000, // 18c + 0b00110000, // 19C + 0b00100000, // 20C + 0b01100000, // 21C + 0b01110000, // 22C + 0b01010000, // 23C + 0b01000000, // 24C + 0b11000000, // 25C + 0b11010000, // 26C + 0b10010000, // 27C + 0b10000000, // 28C + 0b10100000, // 29C + 0b10110000 // 30C }; // Constants @@ -59,29 +57,60 @@ static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; void CoolixClimate::transmit_state() { - uint32_t remote_state; + uint32_t remote_state = 0xB20F00; - switch (this->mode) { - case climate::CLIMATE_MODE_COOL: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL; - break; - case climate::CLIMATE_MODE_HEAT: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT; - break; - case climate::CLIMATE_MODE_AUTO: - remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; - break; - case climate::CLIMATE_MODE_OFF: - default: - remote_state = COOLIX_OFF; - break; + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state = COOLIX_SWING; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COOLIX_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= COOLIX_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COOLIX_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + remote_state |= COOLIX_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state = COOLIX_OFF; + break; + } + if (this->mode != climate::CLIMATE_MODE_OFF) { + if (this->mode != climate::CLIMATE_MODE_FAN_ONLY) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); + remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; + } else { + remote_state |= COOLIX_FAN_TEMP_CODE; + } + if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + remote_state |= COOLIX_FAN_MODE_AUTO_DRY; + } else { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= COOLIX_FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= COOLIX_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= COOLIX_FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= COOLIX_FAN_AUTO; + break; + } + } + } } - if (this->mode != climate::CLIMATE_MODE_OFF) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); - remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp. - remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4; - } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); auto transmit = this->transmitter_->transmit(); @@ -161,35 +190,35 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { if (remote_state == COOLIX_OFF) { this->mode = climate::CLIMATE_MODE_OFF; + } else if (remote_state == COOLIX_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) this->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) this->mode = climate::CLIMATE_MODE_AUTO; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { - // climate::CLIMATE_MODE_DRY; - if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY) - ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO"); + if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) + this->mode = climate::CLIMATE_MODE_DRY; else - ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO"); - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_FAN_ONLY; } else this->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - // When climate::CLIMATE_MODE_DRY is implemented replace following line with this: - // if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY) - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO) - ESP_LOGV(TAG, "Not supported FAN speed AUTO"); + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO || + this->mode == climate::CLIMATE_MODE_DRY) + this->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - ESP_LOGV(TAG, "Not supported FAN speed MIN"); + this->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - ESP_LOGV(TAG, "Not supported FAN speed MED"); + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - ESP_LOGV(TAG, "Not supported FAN speed MAX"); + this->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature - uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4; + uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) this->target_temperature = i + COOLIX_TEMP_MIN; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index ed03a2fd1e..caf93f7621 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -11,13 +11,28 @@ const uint8_t COOLIX_TEMP_MAX = 30; // Celsius class CoolixClimate : public climate_ir::ClimateIR { public: - CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} + CoolixClimate() + : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } protected: /// Transmit via IR the state of this climate controller. void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; }; } // namespace coolix diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 48b470cfb2..3f097d9c07 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -30,6 +30,10 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC modes.add("cool"); if (traits.supports_mode(CLIMATE_MODE_HEAT)) modes.add("heat"); + if (traits.supports_mode(CLIMATE_MODE_FAN_ONLY)) + modes.add("fan_only"); + if (traits.supports_mode(CLIMATE_MODE_DRY)) + modes.add("dry"); if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic @@ -155,6 +159,12 @@ bool MQTTClimateComponent::publish_state_() { case CLIMATE_MODE_HEAT: mode_s = "heat"; break; + case CLIMATE_MODE_FAN_ONLY: + mode_s = "fan_only"; + break; + case CLIMATE_MODE_DRY: + mode_s = "dry"; + break; } bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) diff --git a/esphome/const.py b/esphome/const.py index 829b4738ce..0655da8f5e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -153,6 +153,7 @@ CONF_EXPIRE_AFTER = 'expire_after' CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' +CONF_FAN_MODE = 'fan_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -422,6 +423,7 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_MODE = 'swing_mode' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TAG = 'tag' diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index a8f81c9daf..8373e0bd66 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1,6 +1,19 @@ """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. + +you need to install protobuf-compiler: +running protc --version should return +libprotoc 3.6.1 + +then run this script with python3 and the files + + esphome/components/api/api_pb2_service.h + esphome/components/api/api_pb2_service.cpp + esphome/components/api/api_pb2.h + esphome/components/api/api_pb2.cpp + +will be generated, they still need to be formatted """ import re @@ -10,13 +23,17 @@ from subprocess import call # Generate with # protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto + import api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor -cwd = Path(__file__).parent +file_header = '// This file was automatically generated with a tool.\n' +file_header += '// See scripts/api_protobuf/api_protobuf.py\n' + +cwd = Path(__file__).resolve().parent root = cwd.parent.parent / 'esphome' / 'components' / 'api' -prot = cwd / 'api.protoc' -call(['protoc', '-o', prot, '-I', root, 'api.proto']) +prot = root / 'api.protoc' +call(['protoc', '-o', str(prot), '-I', str(root), 'api.proto']) content = prot.read_bytes() d = descriptor.FileDescriptorSet.FromString(content) @@ -617,7 +634,8 @@ def build_message_type(desc): file = d.file[0] -content = '''\ +content = file_header +content += '''\ #pragma once #include "proto.h" @@ -627,7 +645,8 @@ namespace api { ''' -cpp = '''\ +cpp = file_header +cpp += '''\ #include "api_pb2.h" #include "esphome/core/log.h" @@ -739,7 +758,8 @@ def build_service_message_type(mt): return hout, cout -hpp = '''\ +hpp = file_header +hpp += '''\ #pragma once #include "api_pb2.h" @@ -750,7 +770,8 @@ namespace api { ''' -cpp = '''\ +cpp = file_header +cpp += '''\ #include "api_pb2_service.h" #include "esphome/core/log.h" From 5becaebdda0903558f0d3227c78c9bc7391e9d81 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 17 Nov 2019 23:25:04 +0100 Subject: [PATCH 092/412] Improve WiFi disconnect messages (#857) * Improve WiFi disconnect messages * Fix * Update wifi_component_esp32.cpp --- esphome/components/wifi/wifi_component_esp32.cpp | 8 ++++++-- esphome/components/wifi/wifi_component_esp8266.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 862db7a9de..e345ab1671 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -329,8 +329,12 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason=%s", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + if (it.reason == WIFI_REASON_NO_AP_FOUND) { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + } else { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + } break; } case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ee67fd36df..88bcb03450 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -330,8 +330,12 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=%s reason='%s'", buf, format_mac_addr(it.bssid).c_str(), - get_disconnect_reason_str(it.reason)); + if (it.reason == REASON_NO_AP_FOUND) { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + } else { + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + } break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { From b55544b860c6b2bf9fc66b74d119d6c4a216b1eb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 17 Nov 2019 23:25:20 +0100 Subject: [PATCH 093/412] Fix MQTT python 3 stringify IPAddress Type (#864) Fixes https://github.com/esphome/issues/issues/850 --- esphome/mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index e89a6d9578..541f1983f7 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -67,7 +67,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) tls_version=tls_version, ciphers=None) try: - client.connect(config[CONF_MQTT][CONF_BROKER], config[CONF_MQTT][CONF_PORT]) + client.connect(str(config[CONF_MQTT][CONF_BROKER]), config[CONF_MQTT][CONF_PORT]) except socket.error as err: raise EsphomeError("Cannot connect to MQTT broker: {}".format(err)) @@ -127,7 +127,7 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): # From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py def get_fingerprint(config): - addr = config[CONF_MQTT][CONF_BROKER], config[CONF_MQTT][CONF_PORT] + addr = str(config[CONF_MQTT][CONF_BROKER]), config[CONF_MQTT][CONF_PORT] _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) try: cert_pem = ssl.get_server_certificate(addr) From cfd42ea16298f234ae68f082d8b8025d5bcdc633 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 17 Nov 2019 23:28:30 +0100 Subject: [PATCH 094/412] Revert ESP32 BLE Tracker defaults (#863) Fixes https://github.com/esphome/issues/issues/824 Fixes https://github.com/esphome/issues/issues/851 --- esphome/components/esp32_ble_tracker/__init__.py | 2 +- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 3d48eafde4..ccc15eb451 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -82,7 +82,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ cv.Optional(CONF_DURATION, default='5min'): cv.positive_time_period_seconds, cv.Optional(CONF_INTERVAL, default='320ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_WINDOW, default='200ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_WINDOW, default='30ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_ACTIVE, default=True): cv.boolean, }), validate_scan_parameters), diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 4b67277f16..a53d61f878 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -442,8 +442,8 @@ const optional &ESPBTDevice::get_service_data_uuid() const { return t void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); ESP_LOGCONFIG(TAG, " Scan Duration: %u s", this->scan_duration_); - ESP_LOGCONFIG(TAG, " Scan Interval: %u ms", this->scan_interval_); - ESP_LOGCONFIG(TAG, " Scan Window: %u ms", this->scan_window_); + ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); + ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); } void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { From 072cd5b83ec51f81a68fc6f7969b9e48524caa8b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 17 Nov 2019 23:28:43 +0100 Subject: [PATCH 095/412] Change ESP8266 default wifi output power (#862) See also https://github.com/esphome/issues/issues/455 --- esphome/components/wifi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 818fb70105..37ea20c6e4 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -126,7 +126,8 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional(CONF_OUTPUT_POWER): cv.All(cv.decibel, cv.float_range(min=10.0, max=20.5)), + cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( + cv.decibel, cv.float_range(min=10.0, max=20.5)), cv.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), }), validate) From ad76709d00e757907e3241f43e1cf9626de8032b Mon Sep 17 00:00:00 2001 From: warpzone <11829895+warpzone@users.noreply.github.com> Date: Thu, 21 Nov 2019 00:54:25 +0900 Subject: [PATCH 096/412] =?UTF-8?q?fix=20the=20problem=20of=20missing=20pa?= =?UTF-8?q?rt=20of=20advertising=20packet=20when=20activ=E2=80=A6=20(#868)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix the problem of missing part of advertising packet when active scan is enabled. * fix for ci-suggest-changes --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index a53d61f878..53c6de62cc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -325,13 +325,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e } ESP_LOGVV(TAG, "Adv data: %s", - hexencode_string(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); + hexencode_string( + std::string(reinterpret_cast(param.ble_adv), param.adv_data_len + param.scan_rsp_len)) + .c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { size_t offset = 0; const uint8_t *payload = param.ble_adv; - uint8_t len = param.adv_data_len; + uint8_t len = param.adv_data_len + param.scan_rsp_len; while (offset + 2 < len) { const uint8_t field_length = payload[offset++]; // First byte is length of adv record From b7b23ffdb2c6e45a2c8b426f9fb7ceed6e537fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Airy=20Andr=C3=A9?= Date: Wed, 20 Nov 2019 17:00:32 +0100 Subject: [PATCH 097/412] Decode DHT11 decimal part (#861) * Decode DHT11 decimal part * Fix comment * Fix comment * Handle negative temp for some DHT11 - code from the DHT12 component * Don't use the fractional part if the checksum is the 2 bytes one --- esphome/components/dht/dht.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 23d8c1d3e2..28a365c49f 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -165,10 +165,24 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro } if (this->model_ == DHT_MODEL_DHT11) { - *humidity = data[0]; - if (*humidity > 100) - *humidity = NAN; - *temperature = data[2]; + if (checksum_a == data[4]) { + // Data format: 8bit integral RH data + 8bit decimal RH data + 8bit integral T data + 8bit decimal T data + 8bit + // check sum - some models always have 0 in the decimal part + const uint16_t raw_temperature = uint16_t(data[2]) * 10 + (data[3] & 0x7F); + *temperature = raw_temperature / 10.0f; + if ((data[3] & 0x80) != 0) { + // negative + *temperature *= -1; + } + + const uint16_t raw_humidity = uint16_t(data[0]) * 10 + data[1]; + *humidity = raw_humidity / 10.0f; + } else { + // For compatibily with DHT11 models which might only use 2 bytes checksums, only use the data from these two + // bytes + *temperature = data[2]; + *humidity = data[0]; + } } else { uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); From 6a0268b8523ffcfe53833062e8508175ea4a1ad2 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Wed, 20 Nov 2019 11:47:34 -0500 Subject: [PATCH 098/412] fix chip_temperature for atm90e32 component (#865) * Added more data to atm90e32 component * ignore * correction * Delete 6chan_energy_meter.yaml * PR request changes * repository test branch * Update setup.py * Update const.py * delete test yaml * fix chip_temperature_sensor This was throwing an error if chip_temperature was used. It needed to be changed from temp to temperature. * default * Update test1.yaml --- esphome/components/atm90e32/sensor.py | 2 +- tests/test1.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 520dfc82ef..490fdb4719 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -83,6 +83,6 @@ def to_code(config): cg.add(var.set_freq_sensor(sens)) if CONF_CHIP_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) - cg.add(var.set_chip_temp_sensor(sens)) + cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) diff --git a/tests/test1.yaml b/tests/test1.yaml index e9f8765167..103a450379 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -272,6 +272,10 @@ sensor: name: "EMON CT2 Current" power: name: "EMON Active Power CT2" + reactive_power: + name: "EMON Reactive Power CT2" + power_factor: + name: "EMON Power Factor CT2" gain_voltage: 47660 gain_ct: 12577 phase_c: @@ -279,10 +283,16 @@ sensor: name: "EMON CT3 Current" power: name: "EMON Active Power CT3" + reactive_power: + name: "EMON Reactive Power CT3" + power_factor: + name: "EMON Power Factor CT3" gain_voltage: 47660 gain_ct: 12577 frequency: name: "EMON Line Frequency" + chip_temperature: + name: "EMON Chip Temp A" line_frequency: 50Hz gain_pga: 2X - platform: bh1750 From be36eef939554478d5a48556827a5eed810de98d Mon Sep 17 00:00:00 2001 From: KristopherMackowiak <33198635+KristopherMackowiak@users.noreply.github.com> Date: Thu, 21 Nov 2019 21:57:27 +0100 Subject: [PATCH 099/412] fix: template cover add position action (#877) * Update const.py * Update __init__.py * Update template_cover.cpp * Update __init__.py * Update test3.yaml * formatting code * formatting code 2 * removed position lambda * Update test3.yaml * Update __init__.py --- esphome/components/template/cover/__init__.py | 12 +++++++++--- esphome/components/template/cover/template_cover.cpp | 4 ++-- esphome/const.py | 1 + tests/test3.yaml | 4 ++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 7a60096d85..607d9d0064 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -4,7 +4,8 @@ from esphome import automation from esphome.components import cover from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \ CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \ - CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA + CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA, \ + CONF_POSITION_ACTION from .. import template_ns TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component) @@ -29,6 +30,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True), }).extend(cv.COMPONENT_SCHEMA) @@ -55,11 +57,15 @@ def to_code(config): tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [], return_type=cg.optional.template(float)) cg.add(var.set_tilt_lambda(tilt_template_)) - + if CONF_POSITION_ACTION in config: + yield automation.build_automation(var.get_position_trigger(), [(float, 'pos')], + config[CONF_POSITION_ACTION]) + cg.add(var.set_has_position(True)) + else: + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) - cg.add(var.set_has_position(config[CONF_HAS_POSITION])) @automation.register_action('cover.template.publish', cover.CoverPublishAction, cv.Schema({ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 381e6dd6cd..887f282007 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -86,10 +86,10 @@ void TemplateCover::control(const CoverCall &call) { } else if (pos == COVER_CLOSED) { this->close_trigger_->trigger(); this->prev_command_trigger_ = this->close_trigger_; + } else { + this->position_trigger_->trigger(pos); } - this->position_trigger_->trigger(pos); - if (this->optimistic_) { this->position = pos; } diff --git a/esphome/const.py b/esphome/const.py index 0655da8f5e..b604120318 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -331,6 +331,7 @@ CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' CONF_PORT = 'port' CONF_POSITION = 'position' +CONF_POSITION_ACTION = 'position_action' CONF_POWER = 'power' CONF_POWER_FACTOR = 'power_factor' CONF_POWER_ON_VALUE = 'power_on_value' diff --git a/tests/test3.yaml b/tests/test3.yaml index 6d70f60764..bbfcc72880 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -578,6 +578,10 @@ cover: - output.set_level: id: out level: !lambda "return tilt;" + position_action: + - output.set_level: + id: out + level: !lambda "return pos;" output: From fe89dcdc0892248f2353481e6e44a8cc346e91fe Mon Sep 17 00:00:00 2001 From: Daniel Kucera Date: Tue, 26 Nov 2019 16:56:04 +0000 Subject: [PATCH 100/412] added idle action for climate (#859) * added idle * more clear state description * Also add drying/fan Putting it in this PR because it will be put in the same aioesphomeapi release anyway. * Update bang_bang for idle action Co-authored-by: root Co-authored-by: Otto Winter --- esphome/components/api/api.proto | 3 ++ esphome/components/api/api_pb2.cpp | 6 +++ esphome/components/api/api_pb2.h | 3 ++ .../bang_bang/bang_bang_climate.cpp | 38 ++++++++++++------- esphome/components/climate/climate_mode.cpp | 6 +++ esphome/components/climate/climate_mode.h | 6 +++ 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 2e01856a3b..4bb7d1b555 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -678,6 +678,9 @@ enum ClimateAction { // values same as mode for readability CLIMATE_ACTION_COOLING = 2; CLIMATE_ACTION_HEATING = 3; + CLIMATE_ACTION_IDLE = 4; + CLIMATE_ACTION_DRYING = 5; + CLIMATE_ACTION_FAN = 6; } message ListEntitiesClimateResponse { option (id) = 46; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index cca488decf..6b98f95f53 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -158,6 +158,12 @@ template<> const char *proto_enum_to_string(enums::Climate return "CLIMATE_ACTION_COOLING"; case enums::CLIMATE_ACTION_HEATING: return "CLIMATE_ACTION_HEATING"; + case enums::CLIMATE_ACTION_IDLE: + return "CLIMATE_ACTION_IDLE"; + case enums::CLIMATE_ACTION_DRYING: + return "CLIMATE_ACTION_DRYING"; + case enums::CLIMATE_ACTION_FAN: + return "CLIMATE_ACTION_FAN"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index fc855a889a..8be89f0365 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -76,6 +76,9 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, CLIMATE_ACTION_COOLING = 2, CLIMATE_ACTION_HEATING = 3, + CLIMATE_ACTION_IDLE = 4, + CLIMATE_ACTION_DRYING = 5, + CLIMATE_ACTION_FAN = 6, }; } // namespace enums diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 978abae52a..cf527988fe 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -51,12 +51,15 @@ climate::ClimateTraits BangBangClimate::traits() { } void BangBangClimate::compute_state_() { if (this->mode != climate::CLIMATE_MODE_AUTO) { - // in non-auto mode + // in non-auto mode, switch directly to appropriate action + // - HEAT mode -> HEATING action + // - COOL mode -> COOLING action + // - OFF mode -> OFF action (not IDLE!) this->switch_to_action_(static_cast(this->mode)); return; } if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { - // if any control values are nan, go to OFF (idle) mode + // if any control parameters are nan, go to OFF action (not IDLE!) this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } @@ -69,18 +72,18 @@ void BangBangClimate::compute_state_() { if (this->supports_heat_) target_action = climate::CLIMATE_ACTION_HEATING; else - target_action = climate::CLIMATE_ACTION_OFF; + target_action = climate::CLIMATE_ACTION_IDLE; } else if (too_hot) { // too hot -> enable cooling if possible, else idle if (this->supports_cool_) target_action = climate::CLIMATE_ACTION_COOLING; else - target_action = climate::CLIMATE_ACTION_OFF; + target_action = climate::CLIMATE_ACTION_IDLE; } else { // neither too hot nor too cold -> in range if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle mode - target_action = climate::CLIMATE_ACTION_OFF; + // if supports both ends, go to idle action + target_action = climate::CLIMATE_ACTION_IDLE; } else { // else use current mode and don't change (hysteresis) target_action = this->action; @@ -94,6 +97,16 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { // already in target mode return; + if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { + // switching from OFF to IDLE or vice-versa + // these only have visual difference. OFF means user manually disabled, + // IDLE means it's in auto mode but value is in target range. + this->action = action; + this->publish_state(); + return; + } + if (this->prev_trigger_ != nullptr) { this->prev_trigger_->stop(); this->prev_trigger_ = nullptr; @@ -101,6 +114,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { Trigger<> *trig; switch (action) { case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: trig = this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: @@ -112,13 +126,11 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { default: trig = nullptr; } - if (trig != nullptr) { - // trig should never be null, but still check so that we don't crash - trig->trigger(); - this->action = action; - this->prev_trigger_ = trig; - this->publish_state(); - } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_trigger_ = trig; + this->publish_state(); } void BangBangClimate::change_away_(bool away) { if (!away) { diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index aa06ce87f0..ddcc4af4d9 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -29,6 +29,12 @@ const char *climate_action_to_string(ClimateAction action) { return "COOLING"; case CLIMATE_ACTION_HEATING: return "HEATING"; + case CLIMATE_ACTION_IDLE: + return "IDLE"; + case CLIMATE_ACTION_DRYING: + return "DRYING"; + case CLIMATE_ACTION_FAN: + return "FAN"; default: return "UNKNOWN"; } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 83ef715402..8037ea2196 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -29,6 +29,12 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_COOLING = 2, /// The climate device is actively heating (usually in heat or auto mode) CLIMATE_ACTION_HEATING = 3, + /// The climate device is idle (monitoring climate but no action needed) + CLIMATE_ACTION_IDLE = 4, + /// The climate device is drying (either mode DRY or AUTO) + CLIMATE_ACTION_DRYING = 5, + /// The climate device is in fan only mode (either mode FAN_ONLY or AUTO) + CLIMATE_ACTION_FAN = 6, }; /// Enum for all modes a climate fan can be in From 36ffef083bf3db2d0bfe76f304d88397956c003a Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Tue, 26 Nov 2019 10:31:33 -0700 Subject: [PATCH 101/412] Fix MAX31865 edge case. (#882) In a heavy EMI environment, reading the current config from the MAX31865 can fail, such as switching from a 4-wire sensor to a 3-wire sensor. This causes the temperature value to be off wildly, but still technically valid, so it doesn't get reported as a sensor failure. Since we know what configuration we want, rather than send it to the MAX31865 on setup and ask for it over and over (propagating any error as we write it back), instead store the base configuration and work from that to change modes. This not only avoids propagating any error, it also saves a lot of unnecessary reads from the MAX31865. --- esphome/components/max31865/max31865.cpp | 26 +++++++++++++++--------- esphome/components/max31865/max31865.h | 4 +++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 500b5b2883..a3c537a2c2 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -37,7 +37,7 @@ void MAX31865Sensor::update() { } // Run fault detection - write_register_(CONFIGURATION_REG, 0b11101110, 0b10000110); + this->write_config_(0b11101110, 0b10000110); const uint32_t start_time = micros(); uint8_t config; uint32_t fault_detect_time; @@ -55,7 +55,7 @@ void MAX31865Sensor::update() { ESP_LOGV(TAG, "Fault detection completed in %uμs.", fault_detect_time); // Start 1-shot conversion - this->write_register_(CONFIGURATION_REG, 0b11100000, 0b10100000); + this->write_config_(0b11100000, 0b10100000); // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz auto f = std::bind(&MAX31865Sensor::read_data_, this); @@ -66,13 +66,15 @@ void MAX31865Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up MAX31865Sensor '%s'...", this->name_.c_str()); this->spi_setup(); - // Build configuration - uint8_t config = 0b00000010; - config |= (filter_ & 1) << 0; + // Build base configuration + base_config_ = 0b00000000; + base_config_ |= (filter_ & 1) << 0; if (rtd_wires_ == 3) { - config |= 1 << 4; + base_config_ |= 1 << 4; } - this->write_register_(CONFIGURATION_REG, 0b11111111, config); + + // Clear any existing faults & set base config + this->write_config_(0b00000010, 0b00000010); } void MAX31865Sensor::dump_config() { @@ -90,7 +92,7 @@ float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; void MAX31865Sensor::read_data_() { // Read temperature, disable V_BIAS (save power) const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); - this->write_register_(CONFIGURATION_REG, 0b11000000, 0b00000000); + this->write_config_(0b11000000, 0b00000000); // Check faults const uint8_t faults = this->read_register_(FAULT_STATUS_REG); @@ -137,12 +139,16 @@ void MAX31865Sensor::read_data_() { this->publish_state(temperature); } -void MAX31865Sensor::write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) { - uint8_t value = this->read_register_(reg); +void MAX31865Sensor::write_config_(uint8_t mask, uint8_t bits, uint8_t start_position) { + uint8_t value = base_config_; value &= (~mask); value |= (bits << start_position); + this->write_register_(CONFIGURATION_REG, value); +} + +void MAX31865Sensor::write_register_(uint8_t reg, uint8_t value) { this->enable(); this->write_byte(reg |= SPI_WRITE_M); this->write_byte(value); diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index e63be8e6c5..ee2f155f74 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -43,10 +43,12 @@ class MAX31865Sensor : public sensor::Sensor, float rtd_nominal_resistance_; MAX31865ConfigFilter filter_; uint8_t rtd_wires_; + uint8_t base_config_; bool has_fault_ = false; bool has_warn_ = false; void read_data_(); - void write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position = 0); + void write_config_(uint8_t mask, uint8_t bits, uint8_t start_position = 0); + void write_register_(uint8_t reg, uint8_t value); const uint8_t read_register_(uint8_t reg); const uint16_t read_register_16_(uint8_t reg); float calc_temperature_(const float& rtd_ratio); From fa1adfd93467c4fbec5018f2d8d3030e94d5e6bf Mon Sep 17 00:00:00 2001 From: Tim P Date: Wed, 27 Nov 2019 04:43:11 +1100 Subject: [PATCH 102/412] Add QMC5883L Sensor + Improvements to HMC5883L (#671) * Add QMC5883L and Updated HMC5883L * add tests * changed to oversampling * fix pylint * fix private method * typo fix * fix protected method * Clean up code and PR recomendations * fix tests * remote file * fix qmc oversampling unit * Remove hmc5883l config logging Either the units are converted to the user values like 1x, 8x oversampling or not printed at all. Printing the machine-value of these is only confusing users. * Changes for validate_enum Move stuff that can be done beforehand out of the bound function, use text_type for py2/3 compatability. * Remove unused constant * Remove duplicate tests * Repeat remove config print * remove changes to test2 since bin is to large * Add comment to HMC5583L Co-authored-by: Timothy Purchas Co-authored-by: Otto Winter --- esphome/components/hmc5883l/hmc5883l.cpp | 10 +- esphome/components/hmc5883l/hmc5883l.h | 21 ++++ esphome/components/hmc5883l/sensor.py | 59 +++++++++-- esphome/components/qmc5883l/__init__.py | 0 esphome/components/qmc5883l/qmc5883l.cpp | 124 +++++++++++++++++++++++ esphome/components/qmc5883l/qmc5883l.h | 60 +++++++++++ esphome/components/qmc5883l/sensor.py | 105 +++++++++++++++++++ tests/test1.yaml | 14 +++ 8 files changed, 378 insertions(+), 15 deletions(-) create mode 100644 esphome/components/qmc5883l/__init__.py create mode 100644 esphome/components/qmc5883l/qmc5883l.cpp create mode 100644 esphome/components/qmc5883l/qmc5883l.h create mode 100644 esphome/components/qmc5883l/sensor.py diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index f68b65e654..9094548bb3 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -38,12 +38,9 @@ void HMC5883LComponent::setup() { } uint8_t config_a = 0; - // 0b0xx00000 << 5 Sample Averaging - 0b00=1 sample, 0b11=8 samples - config_a |= 0b01100000; - // 0b000xxx00 << 2 Data Output Rate - 0b100=15Hz - config_a |= 0b00010000; - // 0b000000xx << 0 Measurement Mode - 0b00=high impedance on load - config_a |= 0b00000000; + config_a |= this->oversampling_ << 5; + config_a |= this->datarate_ << 2; + config_a |= 0b0 << 0; // Measurement Mode: Normal(high impedance on load) if (!this->write_byte(HMC5883L_REGISTER_CONFIG_A, config_a)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); @@ -61,7 +58,6 @@ void HMC5883LComponent::setup() { uint8_t mode = 0; // Continuous Measurement Mode mode |= 0b00; - if (!this->write_byte(HMC5883L_REGISTER_MODE, mode)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 3946f1fb10..41d41baa22 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -7,6 +7,23 @@ namespace esphome { namespace hmc5883l { +enum HMC5883LOversampling { + HMC5883L_OVERSAMPLING_1 = 0b000, + HMC5883L_OVERSAMPLING_2 = 0b001, + HMC5883L_OVERSAMPLING_4 = 0b010, + HMC5883L_OVERSAMPLING_8 = 0b011, +}; + +enum HMC5883LDatarate { + HMC5883L_DATARATE_0_75_HZ = 0b000, + HMC5883L_DATARATE_1_5_HZ = 0b001, + HMC5883L_DATARATE_3_0_HZ = 0b010, + HMC5883L_DATARATE_7_5_HZ = 0b011, + HMC5883L_DATARATE_15_0_HZ = 0b100, + HMC5883L_DATARATE_30_0_HZ = 0b101, + HMC5883L_DATARATE_75_0_HZ = 0b110, +}; + enum HMC5883LRange { HMC5883L_RANGE_88_UT = 0b000, HMC5883L_RANGE_130_UT = 0b001, @@ -25,6 +42,8 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } + void set_datarate(HMC5883LDatarate datarate) { datarate_ = datarate; } void set_range(HMC5883LRange range) { range_ = range; } void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } @@ -32,6 +51,8 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; } protected: + HMC5883LOversampling oversampling_{HMC5883L_OVERSAMPLING_1}; + HMC5883LDatarate datarate_{HMC5883L_DATARATE_15_0_HZ}; HMC5883LRange range_{HMC5883L_RANGE_130_UT}; sensor::Sensor *x_sensor_; sensor::Sensor *y_sensor_; diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index afb8ffac7d..6e2366cadc 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -2,8 +2,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, \ - UNIT_DEGREES, ICON_SCREEN_ROTATION +from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, + UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL) +from esphome.py_compat import text_type DEPENDENCIES = ['i2c'] @@ -16,6 +18,25 @@ CONF_HEADING = 'heading' HMC5883LComponent = hmc5883l_ns.class_('HMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) +HMC5883LOversampling = hmc5883l_ns.enum('HMC5883LOversampling') +HMC5883LOversamplings = { + 1: HMC5883LOversampling.HMC5883L_OVERSAMPLING_1, + 2: HMC5883LOversampling.HMC5883L_OVERSAMPLING_2, + 4: HMC5883LOversampling.HMC5883L_OVERSAMPLING_4, + 8: HMC5883LOversampling.HMC5883L_OVERSAMPLING_8, +} + +HMC5883LDatarate = hmc5883l_ns.enum('HMC5883LDatarate') +HMC5883LDatarates = { + 0.75: HMC5883LDatarate.HMC5883L_DATARATE_0_75_HZ, + 1.5: HMC5883LDatarate.HMC5883L_DATARATE_1_5_HZ, + 3.0: HMC5883LDatarate.HMC5883L_DATARATE_3_0_HZ, + 7.5: HMC5883LDatarate.HMC5883L_DATARATE_7_5_HZ, + 15: HMC5883LDatarate.HMC5883L_DATARATE_15_0_HZ, + 30: HMC5883LDatarate.HMC5883L_DATARATE_30_0_HZ, + 75: HMC5883LDatarate.HMC5883L_DATARATE_75_0_HZ, +} + HMC5883LRange = hmc5883l_ns.enum('HMC5883LRange') HMC5883L_RANGES = { 88: HMC5883LRange.HMC5883L_RANGE_88_UT, @@ -29,11 +50,21 @@ HMC5883L_RANGES = { } -def validate_range(value): - value = cv.string(value) - if value.endswith(u'µT') or value.endswith('uT'): - value = value[:-2] - return cv.enum(HMC5883L_RANGES, int=True)(value) +def validate_enum(enum_values, units=None, int=True): + _units = [] + if units is not None: + _units = units if isinstance(units, list) else [units] + _units = [text_type(x) for x in _units] + enum_bound = cv.enum(enum_values, int=int) + + def validate_enum_bound(value): + value = cv.string(value) + for unit in _units: + if value.endswith(unit): + value = value[:-len(unit)] + break + return enum_bound(value) + return validate_enum_bound field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) @@ -42,19 +73,31 @@ heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(HMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_OVERSAMPLING, default='1x'): validate_enum(HMC5883LOversamplings, units="x"), + cv.Optional(CONF_RANGE, default=u'130µT'): validate_enum(HMC5883L_RANGES, units=["uT", u"µT"]), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, - cv.Optional(CONF_RANGE, default='130uT'): validate_range, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x1E)) +def auto_data_rate(config): + interval_sec = config[CONF_UPDATE_INTERVAL].seconds + interval_hz = 1.0/interval_sec + for datarate in sorted(HMC5883LDatarates.keys()): + if float(datarate) >= interval_hz: + return HMC5883LDatarates[datarate] + return HMC5883LDatarates[75] + + def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) + cg.add(var.set_datarate(auto_data_rate(config))) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) diff --git a/esphome/components/qmc5883l/__init__.py b/esphome/components/qmc5883l/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp new file mode 100644 index 0000000000..f809f9dfb3 --- /dev/null +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -0,0 +1,124 @@ +#include "qmc5883l.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qmc5883l { + +static const char *TAG = "qmc5883l"; +static const uint8_t QMC5883L_ADDRESS = 0x0D; + +static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; +static const uint8_t QMC5883L_REGISTER_DATA_X_MSB = 0x01; +static const uint8_t QMC5883L_REGISTER_DATA_Y_LSB = 0x02; +static const uint8_t QMC5883L_REGISTER_DATA_Y_MSB = 0x03; +static const uint8_t QMC5883L_REGISTER_DATA_Z_LSB = 0x04; +static const uint8_t QMC5883L_REGISTER_DATA_Z_MSB = 0x05; +static const uint8_t QMC5883L_REGISTER_STATUS = 0x06; +static const uint8_t QMC5883L_REGISTER_TEMPERATURE_LSB = 0x07; +static const uint8_t QMC5883L_REGISTER_TEMPERATURE_MSB = 0x08; +static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09; +static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A; +static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B; + +void QMC5883LComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up QMC5883L..."); + // Soft Reset + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + delay(10); + + uint8_t control_1 = 0; + control_1 |= 0b01 << 0; // MODE (Mode) -> 0b00=standby, 0b01=continuous + control_1 |= this->datarate_ << 2; + control_1 |= this->range_ << 4; + control_1 |= this->oversampling_ << 6; + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_1, control_1)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint8_t control_2 = 0; + control_2 |= 0b0 << 7; // SOFT_RST (Soft Reset) -> 0b00=disabled, 0b01=enabled + control_2 |= 0b0 << 6; // ROL_PNT (Pointer Roll Over) -> 0b00=disabled, 0b01=enabled + control_2 |= 0b0 << 0; // INT_ENB (Interrupt) -> 0b00=disabled, 0b01=enabled + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, control_2)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint8_t period = 0x01; // recommended value + if (!this->write_byte(QMC5883L_REGISTER_PERIOD, period)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} +void QMC5883LComponent::dump_config() { + ESP_LOGCONFIG(TAG, "QMC5883L:"); + LOG_I2C_DEVICE(this); + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with QMC5883L failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "X Axis", this->x_sensor_); + LOG_SENSOR(" ", "Y Axis", this->y_sensor_); + LOG_SENSOR(" ", "Z Axis", this->z_sensor_); + LOG_SENSOR(" ", "Heading", this->heading_sensor_); +} +float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } +void QMC5883LComponent::update() { + uint8_t status = false; + this->read_byte(QMC5883L_REGISTER_STATUS, &status); + + uint16_t raw_x, raw_y, raw_z; + if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) { + this->status_set_warning(); + return; + } + + float mg_per_bit; + switch (this->range_) { + case QMC5883L_RANGE_200_UT: + mg_per_bit = 0.0833f; + break; + case QMC5883L_RANGE_800_UT: + mg_per_bit = 0.333f; + break; + default: + mg_per_bit = NAN; + } + + // in µT + const float x = int16_t(raw_x) * mg_per_bit * 0.1f; + const float y = int16_t(raw_y) * mg_per_bit * 0.1f; + const float z = int16_t(raw_z) * mg_per_bit * 0.1f; + + float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; + ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° status=%u", x, y, z, heading, status); + + if (this->x_sensor_ != nullptr) + this->x_sensor_->publish_state(x); + if (this->y_sensor_ != nullptr) + this->y_sensor_->publish_state(y); + if (this->z_sensor_ != nullptr) + this->z_sensor_->publish_state(z); + if (this->heading_sensor_ != nullptr) + this->heading_sensor_->publish_state(heading); +} + +bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { + bool success = this->read_byte_16(a_register, data); + *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte oder, LSB first; + return success; +} + +} // namespace qmc5883l +} // namespace esphome diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h new file mode 100644 index 0000000000..01697ecbd0 --- /dev/null +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace qmc5883l { + +enum QMC5883LDatarate { + QMC5883L_DATARATE_10_HZ = 0b00, + QMC5883L_DATARATE_50_HZ = 0b01, + QMC5883L_DATARATE_100_HZ = 0b10, + QMC5883L_DATARATE_200_HZ = 0b11, +}; + +enum QMC5883LRange { + QMC5883L_RANGE_200_UT = 0b00, + QMC5883L_RANGE_800_UT = 0b01, +}; + +enum QMC5883LOversampling { + QMC5883L_SAMPLING_512 = 0b00, + QMC5883L_SAMPLING_256 = 0b01, + QMC5883L_SAMPLING_128 = 0b10, + QMC5883L_SAMPLING_64 = 0b11, +}; + +class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } + void set_range(QMC5883LRange range) { range_ = range; } + void set_oversampling(QMC5883LOversampling oversampling) { oversampling_ = oversampling; } + void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } + void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } + void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; } + void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; } + + protected: + QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ}; + QMC5883LRange range_{QMC5883L_RANGE_200_UT}; + QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512}; + sensor::Sensor *x_sensor_; + sensor::Sensor *y_sensor_; + sensor::Sensor *z_sensor_; + sensor::Sensor *heading_sensor_; + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + } error_code_; + bool read_byte_16_(uint8_t a_register, uint16_t *data); +}; + +} // namespace qmc5883l +} // namespace esphome diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py new file mode 100644 index 0000000000..8ccd542753 --- /dev/null +++ b/esphome/components/qmc5883l/sensor.py @@ -0,0 +1,105 @@ +# coding=utf-8 +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, + UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL) +from esphome.py_compat import text_type + +DEPENDENCIES = ['i2c'] + +qmc5883l_ns = cg.esphome_ns.namespace('qmc5883l') + +CONF_FIELD_STRENGTH_X = 'field_strength_x' +CONF_FIELD_STRENGTH_Y = 'field_strength_y' +CONF_FIELD_STRENGTH_Z = 'field_strength_z' +CONF_HEADING = 'heading' + +QMC5883LComponent = qmc5883l_ns.class_( + 'QMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) + +QMC5883LDatarate = qmc5883l_ns.enum('QMC5883LDatarate') +QMC5883LDatarates = { + 10: QMC5883LDatarate.QMC5883L_DATARATE_10_HZ, + 50: QMC5883LDatarate.QMC5883L_DATARATE_50_HZ, + 100: QMC5883LDatarate.QMC5883L_DATARATE_100_HZ, + 200: QMC5883LDatarate.QMC5883L_DATARATE_200_HZ, +} + +QMC5883LRange = qmc5883l_ns.enum('QMC5883LRange') +QMC5883L_RANGES = { + 200: QMC5883LRange.QMC5883L_RANGE_200_UT, + 800: QMC5883LRange.QMC5883L_RANGE_800_UT, +} + +QMC5883LOversampling = qmc5883l_ns.enum('QMC5883LOversampling') +QMC5883LOversamplings = { + 512: QMC5883LOversampling.QMC5883L_SAMPLING_512, + 256: QMC5883LOversampling.QMC5883L_SAMPLING_256, + 128: QMC5883LOversampling.QMC5883L_SAMPLING_128, + 64: QMC5883LOversampling.QMC5883L_SAMPLING_64, +} + + +def validate_enum(enum_values, units=None, int=True): + _units = [] + if units is not None: + _units = units if isinstance(units, list) else [units] + _units = [text_type(x) for x in _units] + enum_bound = cv.enum(enum_values, int=int) + + def validate_enum_bound(value): + value = cv.string(value) + for unit in _units: + if value.endswith(unit): + value = value[:-len(unit)] + break + return enum_bound(value) + return validate_enum_bound + + +field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) +heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(QMC5883LComponent), + cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_RANGE, default=u'200µT'): validate_enum(QMC5883L_RANGES, units=["uT", u"µT"]), + cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum(QMC5883LOversamplings, units="x"), + cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, + cv.Optional(CONF_HEADING): heading_schema, +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x0D)) + + +def auto_data_rate(config): + interval_sec = config[CONF_UPDATE_INTERVAL].seconds + interval_hz = 1.0/interval_sec + for datarate in sorted(QMC5883LDatarates.keys()): + if float(datarate) >= interval_hz: + return QMC5883LDatarates[datarate] + return QMC5883LDatarates[200] + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) + cg.add(var.set_datarate(auto_data_rate(config))) + cg.add(var.set_range(config[CONF_RANGE])) + if CONF_FIELD_STRENGTH_X in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) + cg.add(var.set_x_sensor(sens)) + if CONF_FIELD_STRENGTH_Y in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) + cg.add(var.set_y_sensor(sens)) + if CONF_FIELD_STRENGTH_Z in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) + cg.add(var.set_z_sensor(sens)) + if CONF_HEADING in config: + sens = yield sensor.new_sensor(config[CONF_HEADING]) + cg.add(var.set_heading_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 103a450379..3bde797b4a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -417,6 +417,20 @@ sensor: heading: name: "HMC5883L Heading" range: 130uT + oversampling: 8x + update_interval: 15s + - platform: qmc5883l + address: 0x0D + field_strength_x: + name: "QMC5883L Field Strength X" + field_strength_y: + name: "QMC5883L Field Strength Y" + field_strength_z: + name: "QMC5883L Field Strength Z" + heading: + name: "QMC5883L Heading" + range: 800uT + oversampling: 256x update_interval: 15s - platform: hx711 name: "HX711 Value" From 6433da13a33f5427142b1e2a512301db94626e36 Mon Sep 17 00:00:00 2001 From: Andrej Komelj Date: Tue, 3 Dec 2019 14:18:05 +0100 Subject: [PATCH 103/412] Add B/W support for Waveshare 2.90in (B) screen (#889) * Add B/W support for Waveshare 2.90in (B) screen * Fix for() loop source code formatting --- .../components/waveshare_epaper/display.py | 2 + .../waveshare_epaper/waveshare_epaper.cpp | 79 +++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 20 +++++ 3 files changed, 101 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a8ffbcc538..343059d0c1 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -12,6 +12,7 @@ WaveshareEPaper = waveshare_epaper_ns.class_('WaveshareEPaper', cg.PollingCompon display.DisplayBuffer) WaveshareEPaperTypeA = waveshare_epaper_ns.class_('WaveshareEPaperTypeA', WaveshareEPaper) WaveshareEPaper2P7In = waveshare_epaper_ns.class_('WaveshareEPaper2P7In', WaveshareEPaper) +WaveshareEPaper2P9InB = waveshare_epaper_ns.class_('WaveshareEPaper2P9InB', WaveshareEPaper) WaveshareEPaper4P2In = waveshare_epaper_ns.class_('WaveshareEPaper4P2In', WaveshareEPaper) WaveshareEPaper7P5In = waveshare_epaper_ns.class_('WaveshareEPaper7P5In', WaveshareEPaper) @@ -24,6 +25,7 @@ MODELS = { '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), '2.70in': ('b', WaveshareEPaper2P7In), + '2.90in-b': ('b', WaveshareEPaper2P9InB), '4.20in': ('b', WaveshareEPaper4P2In), '7.50in': ('b', WaveshareEPaper7P5In), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index c2f7acde40..ff29df4444 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -423,6 +423,85 @@ void WaveshareEPaper2P7In::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 2.90in Type B (LUT from OTP) +// Datasheet: +// - https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf +// - https://github.com/soonuse/epd-library-arduino/blob/master/2.9inch_e-paper_b/epd2in9b/epd2in9b.cpp +// ======================================================== + +void WaveshareEPaper2P9InB::initialize() { + // from https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf, page 37 + // EPD hardware init start + this->reset_(); + + // COMMAND BOOSTER SOFT START + this->command(0x06); + this->data(0x17); + this->data(0x17); + this->data(0x17); + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + // 128x296 resolution: 10 + // LUT from OTP: 0 + // B/W mode (doesn't work): 1 + // scan-up: 1 + // shift-right: 1 + // booster ON: 1 + // no soft reset: 1 + this->data(0x9F); + + // COMMAND RESOLUTION SETTING + // set to 128x296 by COMMAND PANNEL SETTING + + // COMMAND VCOM AND DATA INTERVAL SETTING + // use defaults for white border and ESPHome image polarity + + // EPD hardware init end +} +void HOT WaveshareEPaper2P9InB::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + delay(2); + this->start_data_(); + for (int i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0x00); + this->end_data_(); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(2); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper2P9InB::get_width_internal() { return 128; } +int WaveshareEPaper2P9InB::get_height_internal() { return 296; } +void WaveshareEPaper2P9InB::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in (B)"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + static const uint8_t LUT_VCOM_DC_4_2[] = { 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index eff6b895a9..46fe465c5b 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -126,6 +126,26 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InB : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; From 0f406c38ebe0c5c2e00bba412082c8a589101265 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 3 Dec 2019 11:50:06 -0300 Subject: [PATCH 104/412] fix climate_ir on receive optional (#897) * fix climate on receive optional * add climate tests --- esphome/components/climate_ir/climate_ir.h | 3 +++ tests/test1.yaml | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 6dc5b43279..7a69b19786 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -51,6 +51,9 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: /// Transmit via IR the state of this climate controller. virtual void transmit_state() = 0; + // Dummy implement on_receive so implementation is optional for inheritors + bool on_receive(remote_base::RemoteReceiveData data) override { return false; }; + bool supports_cool_{true}; bool supports_heat_{true}; bool supports_dry_{false}; diff --git a/tests/test1.yaml b/tests/test1.yaml index 3bde797b4a..601789a829 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -667,7 +667,7 @@ sensor: name: "Windspeed" wind_direction_degrees: name: "Winddirection Degrees" - pin: + pin: number: GPIO04 mode: INPUT - platform: zyaura @@ -1082,7 +1082,7 @@ light: name: "Test For Custom Lambda Effect" lambda: |- it[0] = current_color; - + - automation: name: Custom Effect sequence: @@ -1154,6 +1154,11 @@ climate: sensor: my_sensor - platform: coolix name: Coolix Climate + - platform: fujitsu_general + name: Fujitsu General Climate + - platform: yashima + name: Yashima Climate + switch: - platform: gpio @@ -1351,7 +1356,7 @@ interval: static uint16_t btn_left_state = id(btn_left)->get_value(); ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); - + btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; id(btn_left)->set_threshold(btn_left_state * 0.9); From 7f895abc24547aefe8f8b979fd57650bebc15fd0 Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:34:10 +0100 Subject: [PATCH 105/412] Add support for Sensirion SPS30 Particulate Matter sensors (#891) * Add support for Sensirion SPS30 Particulate Matter sensors * Remove blocking of the main thread on initialization; Improve wording on the debug messages; Add robustness in re-initialization of reconnected or replaced sensors; * Fix code formatting; Co-authored-by: Nad --- esphome/components/sps30/__init__.py | 0 esphome/components/sps30/sensor.py | 82 ++++++++ esphome/components/sps30/sps30.cpp | 268 +++++++++++++++++++++++++++ esphome/components/sps30/sps30.h | 62 +++++++ esphome/const.py | 11 ++ tests/test1.yaml | 30 +++ 6 files changed, 453 insertions(+) create mode 100644 esphome/components/sps30/__init__.py create mode 100644 esphome/components/sps30/sensor.py create mode 100644 esphome/components/sps30/sps30.cpp create mode 100644 esphome/components/sps30/sps30.h diff --git a/esphome/components/sps30/__init__.py b/esphome/components/sps30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py new file mode 100644 index 0000000000..c45758be4e --- /dev/null +++ b/esphome/components/sps30/sensor.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_PM_1_0, CONF_PM_2_5, CONF_PM_4_0, CONF_PM_10_0, \ + CONF_PMC_0_5, CONF_PMC_1_0, CONF_PMC_2_5, CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, \ + UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, UNIT_MICROMETER, \ + ICON_CHEMICAL_WEAPON, ICON_COUNTER, ICON_RULER + +DEPENDENCIES = ['i2c'] + +sps30_ns = cg.esphome_ns.namespace('sps30') +SPS30Component = sps30_ns.class_('SPS30Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SPS30Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_4_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PM_SIZE): sensor.sensor_schema(UNIT_MICROMETER, + ICON_RULER, 0), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x69)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_PM_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_4_0]) + cg.add(var.set_pm_4_0_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + + if CONF_PMC_0_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_0_5]) + cg.add(var.set_pmc_0_5_sensor(sens)) + + if CONF_PMC_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_1_0]) + cg.add(var.set_pmc_1_0_sensor(sens)) + + if CONF_PMC_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_2_5]) + cg.add(var.set_pmc_2_5_sensor(sens)) + + if CONF_PMC_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_4_0]) + cg.add(var.set_pmc_4_0_sensor(sens)) + + if CONF_PMC_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_10_0]) + cg.add(var.set_pmc_10_0_sensor(sens)) + + if CONF_PM_SIZE in config: + sens = yield sensor.new_sensor(config[CONF_PM_SIZE]) + cg.add(var.set_pm_size_sensor(sens)) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp new file mode 100644 index 0000000000..181bf44189 --- /dev/null +++ b/esphome/components/sps30/sps30.cpp @@ -0,0 +1,268 @@ +#include "sps30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sps30 { + +static const char *TAG = "sps30"; + +static const uint16_t SPS30_CMD_GET_ARTICLE_CODE = 0xD025; +static const uint16_t SPS30_CMD_GET_SERIAL_NUMBER = 0xD033; +static const uint16_t SPS30_CMD_GET_FIRMWARE_VERSION = 0xD100; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG = 0x0300; +static const uint16_t SPS30_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SPS30_CMD_READ_MEASUREMENT = 0x0300; +static const uint16_t SPS30_CMD_STOP_MEASUREMENTS = 0x0104; +static const uint16_t SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS = 0x8004; +static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607; +static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304; +static const size_t SERIAL_NUMBER_LENGTH = 8; +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); + /// 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; + } + + uint16_t raw_firmware_version[4]; + if (!this->read_data_(raw_firmware_version, 4)) { + this->error_code_ = FIRMWARE_VERSION_READ_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, " Firmware version v%0d.%02d", (raw_firmware_version[0] >> 8), + uint16_t(raw_firmware_version[0] & 0xFF)); + /// 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)) { + this->error_code_ = SERIAL_NUMBER_READ_FAILED; + this->mark_failed(); + return; + } + + for (size_t i = 0; i < 8; ++i) { + this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); + this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); + } + ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + this->start_continuous_measurement_(); + }); +} + +void SPS30Component::dump_config() { + ESP_LOGCONFIG(TAG, "sps30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor serial number"); + break; + case SERIAL_NUMBER_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor serial number"); + break; + case FIRMWARE_VERSION_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor firmware version"); + break; + case FIRMWARE_VERSION_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Serial Number: '%s'", this->serial_number_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM4", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); +} + +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)) { + ESP_LOGD(TAG, "Sensor has soft-reset successfully. Waiting for reconnection in 500ms..."); + this->set_timeout(500, [this]() { + this->start_continuous_measurement_(); + /// Sensor restarted and reading attempt made next cycle + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + ESP_LOGD(TAG, "Sensor reconnected successfully. Resuming continuous measurement!"); + }); + } else { + ESP_LOGD(TAG, "Sensor soft-reset failed. Is the sensor offline?"); + } + return; + } + /// Check if measurement is ready before reading the value + if (!this->write_command_(SPS30_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) { + 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 + /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. + if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { + ESP_LOGD(TAG, "Sensor exceeded max allowed attempts. Sensor communication will be reinitialized."); + this->status_set_warning(); + } + return; + } + + if (!this->write_command_(SPS30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement status!"); + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[20]; + if (!this->read_data_(raw_data, 20)) { + ESP_LOGW(TAG, "Error reading measurement data!"); + this->status_set_warning(); + return; + } + + union uint32_float_t { + uint32_t uint32; + float value; + }; + + /// Reading and converting Mass concentration + uint32_float_t pm_1_0{.uint32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])))}; + uint32_float_t pm_2_5{.uint32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])))}; + uint32_float_t pm_4_0{.uint32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])))}; + uint32_float_t pm_10_0{.uint32 = (((uint32_t(raw_data[6])) << 16) | (uint32_t(raw_data[7])))}; + + /// Reading and converting Number concentration + uint32_float_t pmc_0_5{.uint32 = (((uint32_t(raw_data[8])) << 16) | (uint32_t(raw_data[9])))}; + uint32_float_t pmc_1_0{.uint32 = (((uint32_t(raw_data[10])) << 16) | (uint32_t(raw_data[11])))}; + uint32_float_t pmc_2_5{.uint32 = (((uint32_t(raw_data[12])) << 16) | (uint32_t(raw_data[13])))}; + uint32_float_t pmc_4_0{.uint32 = (((uint32_t(raw_data[14])) << 16) | (uint32_t(raw_data[15])))}; + uint32_float_t pmc_10_0{.uint32 = (((uint32_t(raw_data[16])) << 16) | (uint32_t(raw_data[17])))}; + + /// Reading and converting Typical size + uint32_float_t pm_size{.uint32 = (((uint32_t(raw_data[18])) << 16) | (uint32_t(raw_data[19])))}; + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0.value); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5.value); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0.value); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0.value); + + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(pmc_0_5.value); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(pmc_1_0.value); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(pmc_2_5.value); + if (this->pmc_4_0_sensor_ != nullptr) + this->pmc_4_0_sensor_->publish_state(pmc_4_0.value); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(pmc_10_0.value); + + if (this->pm_size_sensor_ != nullptr) + this->pm_size_sensor_->publish_state(pm_size.value); + + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + }); +} + +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)) { + 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; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + 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); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h new file mode 100644 index 0000000000..2f977252a5 --- /dev/null +++ b/esphome/components/sps30/sps30.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.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 { + 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; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_4_0_sensor(sensor::Sensor *pmc_4_0) { pmc_4_0_sensor_ = pmc_4_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } + + void setup() override; + void update() override; + void dump_config() override; + 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 + bool start_continuous_measurement_(); + uint8_t skipped_data_read_cycles_ = 0; + + enum ErrorCode { + COMMUNICATION_FAILED, + FIRMWARE_VERSION_REQUEST_FAILED, + FIRMWARE_VERSION_READ_FAILED, + SERIAL_NUMBER_REQUEST_FAILED, + SERIAL_NUMBER_READ_FAILED, + MEASUREMENT_INIT_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_4_0_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_4_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; + sensor::Sensor *pm_size_sensor_{nullptr}; +}; + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index b604120318..e225862e70 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -329,6 +329,13 @@ CONF_PLATFORMIO_OPTIONS = 'platformio_options' CONF_PM_1_0 = 'pm_1_0' CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' +CONF_PM_4_0 = 'pm_4_0' +CONF_PM_SIZE = 'pm_size' +CONF_PMC_0_5 = 'pmc_0_5' +CONF_PMC_1_0 = 'pmc_1_0' +CONF_PMC_10_0 = 'pmc_10_0' +CONF_PMC_2_5 = 'pmc_2_5' +CONF_PMC_4_0 = 'pmc_4_0' CONF_PORT = 'port' CONF_POSITION = 'position' CONF_POSITION_ACTION = 'position_action' @@ -503,6 +510,7 @@ ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' @@ -519,6 +527,7 @@ ICON_PULSE = 'mdi:pulse' ICON_RADIATOR = 'mdi:radiator' ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' +ICON_RULER = 'mdi:ruler' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGN_DIRECTION = 'mdi:sign-direction' @@ -535,6 +544,7 @@ ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' UNIT_CELSIUS = u'°C' +UNIT_COUNTS_PER_CUBIC_METER = u'#/m³' UNIT_DECIBEL = 'dB' UNIT_DECIBEL_MILLIWATT = 'dBm' UNIT_DEGREE_PER_SECOND = u'°/s' @@ -550,6 +560,7 @@ UNIT_LUX = 'lx' UNIT_METER = 'm' UNIT_METER_PER_SECOND_SQUARED = u'm/s²' UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_MICROMETER = 'µm' UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' UNIT_MICROTESLA = u'µT' UNIT_OHM = u'Ω' diff --git a/tests/test1.yaml b/tests/test1.yaml index 601789a829..f4d6e5dcfe 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -593,6 +593,36 @@ sensor: accuracy_decimals: 1 address: 0x58 update_interval: 5s + - platform: sps30 + pm_1_0: + name: "Workshop PM <1µm Weight concentration" + id: "workshop_PM_1_0" + pm_2_5: + name: "Workshop PM <2.5µm Weight concentration" + id: "workshop_PM_2_5" + pm_4_0: + name: "Workshop PM <4µm Weight concentration" + id: "workshop_PM_4_0" + pm_10_0: + name: "Workshop PM <10µm Weight concentration" + id: "workshop_PM_10_0" + pmc_0_5: + name: "Workshop PM <0.5µm Number concentration" + id: "workshop_PMC_0_5" + pmc_1_0: + name: "Workshop PM <1µm Number concentration" + id: "workshop_PMC_1_0" + pmc_2_5: + name: "Workshop PM <2.5µm Number concentration" + id: "workshop_PMC_2_5" + pmc_4_0: + name: "Workshop PM <4µm Number concentration" + id: "workshop_PMC_4_0" + pmc_10_0: + name: "Workshop PM <10µm Number concentration" + id: "workshop_PMC_10_0" + address: 0x69 + update_interval: 10s - platform: shtcx temperature: name: "Living Room Temperature 10" From 31d964c16ae61fd5716794b8682262627acb79d0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 4 Dec 2019 13:11:53 +0100 Subject: [PATCH 106/412] Add TM1561 support (#893) * Add TM1561 support * Fixed after clang-tidy * Fixed after clang-tidy * Fixed after clang-tidy, updated lib_deps * Fixed after clang-tidy, updated formatting * Added actions, removed from display domain * Protected methods naming * float casting * float casting --- .gitignore | 3 ++ esphome/components/tm1651/__init__.py | 61 +++++++++++++++++++++++ esphome/components/tm1651/tm1651.cpp | 69 +++++++++++++++++++++++++++ esphome/components/tm1651/tm1651.h | 56 ++++++++++++++++++++++ platformio.ini | 1 + tests/test1.yaml | 5 ++ tests/test3.yaml | 20 ++++++++ 7 files changed, 215 insertions(+) create mode 100644 esphome/components/tm1651/__init__.py create mode 100644 esphome/components/tm1651/tm1651.cpp create mode 100644 esphome/components/tm1651/tm1651.h diff --git a/.gitignore b/.gitignore index b004947390..fa4670769b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ *.sublime-project *.sublime-workspace +# Intellij Idea +.idea + # Hide some OS X stuff .DS_Store .AppleDouble diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py new file mode 100644 index 0000000000..1c49287878 --- /dev/null +++ b/esphome/components/tm1651/__init__.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_LEVEL, CONF_BRIGHTNESS + +tm1651_ns = cg.esphome_ns.namespace('tm1651') +TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) +SetLevelAction = tm1651_ns.class_('SetLevelAction', automation.Action) +SetBrightnessAction = tm1651_ns.class_('SetBrightnessAction', automation.Action) +validate_level = cv.All(cv.int_range(min=0, max=100)) + +TM1651_BRIGHTNESS_OPTIONS = { + 1: TM1651Display.TM1651_BRIGHTNESS_LOW, + 2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM, + 3: TM1651Display.TM1651_BRIGHTNESS_HIGH +} + +CONF_DIO_PIN = 'dio_pin' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TM1651Display), + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + clk_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk_pin)) + dio_pin = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio_pin)) + + # https://platformio.org/lib/show/6865/TM1651 + cg.add_library('6865', '1.0.0') + + +@automation.register_action('tm1651.set_level', SetLevelAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL): cv.templatable(validate_level), +}, key=CONF_LEVEL)) +def tm1651_set_level_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.uint8) + cg.add(var.set_level(template_)) + yield var + + +@automation.register_action('tm1651.set_brightness', SetBrightnessAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_level), +}, key=CONF_BRIGHTNESS)) +def tm1651_set_brightness_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) + yield var diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp new file mode 100644 index 0000000000..594ebe9db9 --- /dev/null +++ b/esphome/components/tm1651/tm1651.cpp @@ -0,0 +1,69 @@ +#include "tm1651.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tm1651 { + +static const char *TAG = "tm1651.display"; +static const uint8_t TM1651_MAX_LEVEL = 7; +static const uint8_t MAX_INPUT_LEVEL = 100; + +static const uint8_t TM1651_BRIGHTNESS_LOW = 0; +static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2; +static const uint8_t TM1651_BRIGHTNESS_HIGH = 7; + +void TM1651Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1651..."); + + uint8_t clk = clk_pin_->get_pin(); + uint8_t dio = dio_pin_->get_pin(); + + battery_display_ = new TM1651(clk, dio); + battery_display_->init(); + battery_display_->clearDisplay(); +} + +void TM1651Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1651 Battery Display"); + LOG_PIN(" CLK: ", clk_pin_); + LOG_PIN(" DIO: ", dio_pin_); +} + +void TM1651Display::set_level(uint8_t new_level) { + this->level_ = calculate_level_(new_level); + this->repaint_(); +} + +void TM1651Display::set_brightness(uint8_t new_brightness) { + this->brightness_ = calculate_brightness_(new_brightness); + this->repaint_(); +} + +void TM1651Display::repaint_() { + battery_display_->set(this->brightness_); + battery_display_->displayLevel(this->level_); +} + +uint8_t TM1651Display::calculate_level_(uint8_t new_level) { + if (new_level == 0) { + return 0; + } + + float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL / (float) new_level); + return (uint8_t) roundf(calculated_level); +} + +uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { + if (new_brightness <= 1) { + return TM1651_BRIGHTNESS_LOW; + } else if (new_brightness == 2) { + return TM1651_BRIGHTNESS_MEDIUM; + } else if (new_brightness >= 3) { + return TM1651_BRIGHTNESS_HIGH; + } + + return TM1651_BRIGHTNESS_LOW; +} + +} // namespace tm1651 +} // namespace esphome diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h new file mode 100644 index 0000000000..d75c2adb62 --- /dev/null +++ b/esphome/components/tm1651/tm1651.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/core/automation.h" + +#include + +namespace esphome { +namespace tm1651 { + +class TM1651Display : public Component { + public: + void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + + void setup() override; + void dump_config() override; + + void set_level(uint8_t); + void set_brightness(uint8_t); + + protected: + TM1651 *battery_display_; + GPIOPin *clk_pin_; + GPIOPin *dio_pin_; + + uint8_t brightness_; + uint8_t level_; + + void repaint_(); + + uint8_t calculate_level_(uint8_t); + uint8_t calculate_brightness_(uint8_t); +}; + +template class SetLevelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level) + void play(Ts... x) override { + auto level = this->level_.value(x...); + this->parent_->set_level(level); + } +}; + +template class SetBrightnessAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, brightness) + void play(Ts... x) override { + auto brightness = this->brightness_.value(x...); + this->parent_->set_brightness(brightness); + } +}; + +} // namespace tm1651 +} // namespace esphome diff --git a/platformio.ini b/platformio.ini index 4e1ef1d294..408e5af1ce 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,6 +18,7 @@ lib_deps = NeoPixelBus-esphome@2.5.2 ESPAsyncTCP-esphome@1.2.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) + 6865@1.0.0 ; TM1651 Battery Display build_flags = -Wno-reorder -DUSE_WEB_SERVER diff --git a/tests/test1.yaml b/tests/test1.yaml index f4d6e5dcfe..192d7a2fc9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1454,6 +1454,11 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +tm1651: + id: tm1651_battery + clk_pin: GPIO23 + dio_pin: GPIO23 + remote_receiver: pin: GPIO32 dump: all diff --git a/tests/test3.yaml b/tests/test3.yaml index bbfcc72880..2703621752 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -138,6 +138,21 @@ api: then: - dfplayer.random + - service: battery_level + variables: + level: int + then: + - tm1651.set_level: + id: tm1651_battery + level: !lambda 'return level;' + - service: battery_brightness + variables: + brightness: int + then: + - tm1651.set_brightness: + id: tm1651_battery + brightness: !lambda 'return brightness;' + wifi: ssid: 'MySSID' password: 'password1' @@ -651,3 +666,8 @@ dfplayer: dfplayer.is_playing then: logger.log: 'Playback finished event' + +tm1651: + id: tm1651_battery + clk_pin: D6 + dio_pin: D5 From b7dff4bbab367e44279671fd04d72e140d9817d4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 15:58:40 +0100 Subject: [PATCH 107/412] Add magic value REPLACEME (#881) * Add magic value REPLACEME * Lint --- esphome/config.py | 30 +++++++++++++++-- esphome/config_validation.py | 9 ++--- esphome/helpers.py | 48 ++++++++++++++++++++++++++ esphome/yaml_util.py | 65 ++++++++++-------------------------- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index 5906e2fc95..53449c3e85 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -18,12 +18,12 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2, decode_text +from esphome.py_compat import text_type, IS_PY2, decode_text, string_types from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa from esphome.core import ConfigType # noqa -from esphome.yaml_util import is_secret, ESPHomeDataBase +from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid _LOGGER = logging.getLogger(__name__) @@ -380,6 +380,24 @@ def do_id_pass(result): # type: (Config) -> None result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) +def recursive_check_replaceme(value): + import esphome.config_validation as cv + + if isinstance(value, list): + return cv.Schema([recursive_check_replaceme])(value) + if isinstance(value, dict): + return cv.Schema({cv.valid: recursive_check_replaceme})(value) + if isinstance(value, ESPForceValue): + pass + if isinstance(value, string_types) and value == 'REPLACEME': + raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. " + "Please make sure you have replaced all fields from the sample " + "configuration.\n" + "If you want to use the literal REPLACEME string, " + "please use \"!force REPLACEME\"") + return value + + def validate_config(config): result = Config() @@ -393,6 +411,12 @@ def validate_config(config): result.add_error(err) return result + # 1.1. Check for REPLACEME special value + try: + recursive_check_replaceme(config) + except vol.Invalid as err: + result.add_error(err) + if 'esphomeyaml' in config: _LOGGER.warning("The esphomeyaml section has been renamed to esphome in 1.11.0. " "Please replace 'esphomeyaml:' in your configuration with 'esphome:'.") @@ -588,7 +612,7 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): validation_error = text_type(validation_error) - m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) + m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 5c7255a874..88fb55e841 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -19,7 +19,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes -from esphome.helpers import list_starts_with +from esphome.helpers import list_starts_with, add_class_to_obj from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text from esphome.voluptuous_schema import _Schema @@ -964,11 +964,8 @@ def enum(mapping, **kwargs): one_of_validator = one_of(*mapping, **kwargs) def validator(value): - from esphome.yaml_util import make_data_base - - value = make_data_base(one_of_validator(value)) - cls = value.__class__ - value.__class__ = cls.__class__(cls.__name__ + "Enum", (cls, core.EnumValue), {}) + value = one_of_validator(value) + value = add_class_to_obj(value, core.EnumValue) value.enum_value = mapping[value] return value diff --git a/esphome/helpers.py b/esphome/helpers.py index 48607dbff5..e91b13a735 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -261,3 +261,51 @@ def file_compare(path1, path2): if not blob1: # Reached end return True + + +# A dict of types that need to be converted to heaptypes before a class can be added +# to the object +_TYPE_OVERLOADS = { + int: type('int', (int,), dict()), + float: type('float', (float,), dict()), + str: type('str', (str,), dict()), + dict: type('dict', (str,), dict()), + list: type('list', (list,), dict()), +} + +if IS_PY2: + _TYPE_OVERLOADS[long] = type('long', (long,), dict()) + _TYPE_OVERLOADS[unicode] = type('unicode', (unicode,), dict()) + +# cache created classes here +_CLASS_LOOKUP = {} + + +def add_class_to_obj(value, cls): + """Add a class to a python type. + + This function modifies value so that it has cls as a basetype. + The value itself may be modified by this action! You must use the return + value of this function however, since some types need to be copied first (heaptypes). + """ + if isinstance(value, cls): + # If already is instance, do not add + return value + + try: + orig_cls = value.__class__ + key = (orig_cls, cls) + new_cls = _CLASS_LOOKUP.get(key) + if new_cls is None: + new_cls = orig_cls.__class__(orig_cls.__name__, (orig_cls, cls), {}) + _CLASS_LOOKUP[key] = new_cls + value.__class__ = new_cls + return value + except TypeError: + # Non heap type, look in overloads dict + for type_, func in _TYPE_OVERLOADS.items(): + # Use type() here, we only need to trigger if it's the exact type, + # as otherwise we don't need to overload the class + if type(value) is type_: # pylint: disable=unidiomatic-typecheck + return add_class_to_obj(func(value), cls) + raise diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index fb04d7d5b0..d80334cedf 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -2,6 +2,7 @@ from __future__ import print_function import fnmatch import functools +import inspect import logging import math import os @@ -13,6 +14,7 @@ import yaml.constructor from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange +from esphome.helpers import add_class_to_obj from esphome.py_compat import text_type, IS_PY2 from esphome.util import OrderedDict, filter_yaml_files @@ -26,14 +28,6 @@ _SECRET_CACHE = {} _SECRET_VALUES = {} -class NodeListClass(list): - pass - - -class NodeStrClass(text_type): - pass - - class ESPHomeDataBase(object): @property def esp_range(self): @@ -44,56 +38,25 @@ class ESPHomeDataBase(object): self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) -class ESPInt(int, ESPHomeDataBase): +class ESPForceValue(object): pass -class ESPFloat(float, ESPHomeDataBase): - pass - - -class ESPStr(str, ESPHomeDataBase): - pass - - -class ESPDict(OrderedDict, ESPHomeDataBase): - pass - - -class ESPList(list, ESPHomeDataBase): - pass - - -class ESPLambda(Lambda, ESPHomeDataBase): - pass - - -ESP_TYPES = { - int: ESPInt, - float: ESPFloat, - str: ESPStr, - dict: ESPDict, - list: ESPList, - Lambda: ESPLambda, -} -if IS_PY2: - class ESPUnicode(unicode, ESPHomeDataBase): - pass - - ESP_TYPES[unicode] = ESPUnicode - - def make_data_base(value): - for typ, cons in ESP_TYPES.items(): - if isinstance(value, typ): - return cons(value) - return value + return add_class_to_obj(value, ESPHomeDataBase) def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): res = fn(loader, node) + # newer PyYAML versions use generators, resolve them + if inspect.isgenerator(res): + generator = res + res = next(generator) + # Let generator finish + for _ in generator: + pass res = make_data_base(res) if isinstance(res, ESPHomeDataBase): res.from_node(node) @@ -296,6 +259,11 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors def construct_lambda(self, node): return Lambda(text_type(node.value)) + @_add_data_ref + def construct_force(self, node): + obj = self.construct_scalar(node) + return add_class_to_obj(obj, ESPForceValue) + ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) @@ -314,6 +282,7 @@ ESPHomeLoader.add_constructor('!include_dir_named', ESPHomeLoader.construct_incl ESPHomeLoader.add_constructor('!include_dir_merge_named', ESPHomeLoader.construct_include_dir_merge_named) ESPHomeLoader.add_constructor('!lambda', ESPHomeLoader.construct_lambda) +ESPHomeLoader.add_constructor('!force', ESPHomeLoader.construct_force) def load_yaml(fname): From 73f80a8ea1a18b8d2fad0116d0fd5379d746ac06 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 15:58:58 +0100 Subject: [PATCH 108/412] Fix MQTT logs Int or String expected Python 3 (#898) Fixes https://github.com/esphome/issues/issues/850 --- esphome/mqtt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 541f1983f7..77eb941363 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -67,7 +67,9 @@ def initialize(config, subscriptions, on_message, username, password, client_id) tls_version=tls_version, ciphers=None) try: - client.connect(str(config[CONF_MQTT][CONF_BROKER]), config[CONF_MQTT][CONF_PORT]) + host = str(config[CONF_MQTT][CONF_BROKER]) + port = int(config[CONF_MQTT][CONF_PORT]) + client.connect(host, port) except socket.error as err: raise EsphomeError("Cannot connect to MQTT broker: {}".format(err)) @@ -127,7 +129,7 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): # From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), config[CONF_MQTT][CONF_PORT] + addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) try: cert_pem = ssl.get_server_certificate(addr) From d26cd85306ee925b5ae1021247633d2d075939a7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 15:59:08 +0100 Subject: [PATCH 109/412] web_server call setup_controller (#899) --- esphome/components/web_server/web_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c6204533d4..fcd83297e2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -64,6 +64,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); + this->setup_controller(); this->base_->init(); this->events_.onConnect([this](AsyncEventSourceClient *client) { From e86f2e993f94954a6c96d55cb09a0004a46f32d0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 15:59:27 +0100 Subject: [PATCH 110/412] Pulse counter validate not both disabled (#902) --- esphome/components/pulse_counter/sensor.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index e73bc36036..61d3f3d5b5 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -38,16 +38,25 @@ def validate_pulse_counter_pin(value): return value +def validate_count_mode(value): + rising_edge = value[CONF_RISING_EDGE] + falling_edge = value[CONF_FALLING_EDGE] + if rising_edge == 'DISABLE' and falling_edge == 'DISABLE': + raise cv.Invalid("Can't set both count modes to DISABLE! This means no counting occurs at " + "all!") + return value + + CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).extend({ cv.GenerateID(): cv.declare_id(PulseCounterSensor), cv.Required(CONF_PIN): validate_pulse_counter_pin, cv.Optional(CONF_COUNT_MODE, default={ CONF_RISING_EDGE: 'INCREMENT', CONF_FALLING_EDGE: 'DISABLE', - }): cv.Schema({ + }): cv.All(cv.Schema({ cv.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA, cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, - }), + }), validate_count_mode), cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, }).extend(cv.polling_component_schema('60s')) From e9e92afc9e6857af24bd55dbce0de452c76aca07 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 16:03:37 +0100 Subject: [PATCH 111/412] Optimize application loop speed (#860) * Optimize application loop speed * Also check call_loop * Remove duplicate code * Fixes --- esphome/components/light/light_state.cpp | 2 -- esphome/core/application.cpp | 10 +++++++++- esphome/core/application.h | 3 +++ esphome/core/component.cpp | 10 ++++++++++ esphome/core/component.h | 2 ++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index e96d64ad1f..0ffb603818 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -416,8 +416,6 @@ LightColorValues LightCall::validate_() { if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); if (this->red_.has_value()) v.set_red(*this->red_); if (this->green_.has_value()) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 2600ace218..4ecb247ec3 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -57,13 +57,14 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); this->schedule_dump_config(); + this->calculate_looping_components_(); } void Application::loop() { uint32_t new_app_state = 0; const uint32_t start = millis(); this->scheduler.call(); - for (Component *component : this->components_) { + for (Component *component : this->looping_components_) { component->call(); new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; @@ -146,6 +147,13 @@ void Application::safe_reboot() { } } +void Application::calculate_looping_components_() { + for (auto *obj : this->components_) { + if (obj->has_overridden_loop()) + this->looping_components_.push_back(obj); + } +} + Application App; } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 2014b082e9..3c293e6c8f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -209,7 +209,10 @@ class Application { void register_component_(Component *comp); + void calculate_looping_components_(); + std::vector components_{}; + std::vector looping_components_{}; #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0547fcbdd5..26662b0061 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -138,6 +138,16 @@ float Component::get_actual_setup_priority() const { return this->setup_priority_override_; } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } +bool Component::has_overridden_loop() const { +#ifdef CLANG_TIDY + bool loop_overridden = true; + bool call_loop_overridden = true; +#else + bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); + bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); +#endif + return loop_overridden || call_loop_overridden; +} PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} diff --git a/esphome/core/component.h b/esphome/core/component.h index c8e05cc252..e3f9a51f25 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -126,6 +126,8 @@ class Component { void status_momentary_error(const std::string &name, uint32_t length = 5000); + bool has_overridden_loop() const; + protected: virtual void call_loop(); virtual void call_setup(); From 064589934cd3df5d582563f6120f4fc9b8b9c717 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 16:04:46 +0100 Subject: [PATCH 112/412] Ignore ESP32 Camera unknown framesizes (#901) Fixes https://github.com/esphome/issues/issues/866 --- esphome/components/esp32_camera/esp32_camera.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 2b153f5667..1d9faf7ea2 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -85,6 +85,8 @@ void ESP32Camera::dump_config() { case FRAMESIZE_UXGA: ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); break; + default: + break; } if (this->is_failed()) { From 0698be39956f55f02c83bd7b606f0e9f6a0b0a05 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 16:47:34 +0100 Subject: [PATCH 113/412] Better/stricter pin validation (#903) * Better/stricter pin validation * Update tests --- esphome/pins.py | 40 ++++++++++++++++++++++++++++++---------- tests/test1.yaml | 12 ++++++------ tests/test2.yaml | 4 ++-- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/esphome/pins.py b/esphome/pins.py index 42c8548da4..9a2cf90984 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -289,30 +289,48 @@ def _translate_pin(value): return _lookup_pin(value) +_ESP_SDIO_PINS = { + 6: 'Flash Clock', + 7: 'Flash Data 0', + 8: 'Flash Data 1', + 11: 'Flash Command', +} + + def validate_gpio_pin(value): value = _translate_pin(value) if CORE.is_esp32: if value < 0 or value > 39: raise cv.Invalid(u"ESP32: Invalid pin number: {}".format(value)) - if 6 <= value <= 11: - _LOGGER.warning(u"ESP32: Pin %s (6-11) might already be used by the " - u"flash interface. Be warned.", value) + if value in _ESP_SDIO_PINS: + raise cv.Invalid("This pin cannot be used on ESP32s and is already used by " + "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) + if 9 <= value <= 10: + _LOGGER.warning(u"ESP32: Pin %s (9-10) might already be used by the " + u"flash interface in QUAD IO flash mode.", value) if value in (20, 24, 28, 29, 30, 31): - _LOGGER.warning(u"ESP32: Pin %s (20, 24, 28-31) can usually not be used. " - u"Be warned.", value) + # These pins are not exposed in GPIO mux (reason unknown) + # but they're missing from IO_MUX list in datasheet + raise cv.Invalid("The pin GPIO{} is not usable on ESP32s.".format(value)) return value if CORE.is_esp8266: - if 6 <= value <= 11: - _LOGGER.warning(u"ESP8266: Pin %s (6-11) might already be used by the " - u"flash interface. Be warned.", value) if value < 0 or value > 17: raise cv.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) + if value in _ESP_SDIO_PINS: + raise cv.Invalid("This pin cannot be used on ESP8266s and is already used by " + "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) + if 9 <= value <= 10: + _LOGGER.warning(u"ESP8266: Pin %s (9-10) might already be used by the " + u"flash interface in QUAD IO flash mode.", value) return value raise NotImplementedError def input_pin(value): - return validate_gpio_pin(value) + value = validate_gpio_pin(value) + if CORE.is_esp8266 and value == 17: + raise cv.Invalid("GPIO17 (TOUT) is an analog-only pin on the ESP8266.") + return value def input_pullup_pin(value): @@ -335,6 +353,8 @@ def output_pin(value): u"input pin.".format(value)) return value if CORE.is_esp8266: + if value == 17: + raise cv.Invalid("GPIO17 (TOUT) is an analog-only pin on the ESP8266.") return value raise NotImplementedError @@ -348,7 +368,7 @@ def analog_pin(value): if CORE.is_esp8266: if value == 17: # A0 return value - raise cv.Invalid(u"ESP8266: Only pin A0 (17) supports ADC.") + raise cv.Invalid(u"ESP8266: Only pin A0 (GPIO17) supports ADC.") raise NotImplementedError diff --git a/tests/test1.yaml b/tests/test1.yaml index 192d7a2fc9..b7f3d04b40 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -435,7 +435,7 @@ sensor: - platform: hx711 name: "HX711 Value" dout_pin: GPIO23 - clk_pin: GPIO24 + clk_pin: GPIO25 gain: 128 update_interval: 15s - platform: ina219 @@ -542,7 +542,7 @@ sensor: name: "Rotary Encoder" id: rotary_encoder1 pin_a: GPIO23 - pin_b: GPIO24 + pin_b: GPIO25 pin_reset: GPIO25 filters: - or: @@ -655,7 +655,7 @@ sensor: integration_time: 402ms gain: 16x - platform: ultrasonic - trigger_pin: GPIO24 + trigger_pin: GPIO25 echo_pin: number: GPIO23 inverted: true @@ -1396,11 +1396,11 @@ display: dimensions: 18x4 data_pins: - GPIO19 - - GPIO20 - GPIO21 - GPIO22 + - GPIO23 enable_pin: GPIO23 - rs_pin: GPIO24 + rs_pin: GPIO25 lambda: |- it.print("Hello World!"); - platform: lcd_pcf8574 @@ -1528,7 +1528,7 @@ stepper: - platform: a4988 id: my_stepper step_pin: GPIO23 - dir_pin: GPIO24 + dir_pin: GPIO25 sleep_pin: GPIO25 max_speed: 250 steps/s acceleration: 100 steps/s^2 diff --git a/tests/test2.yaml b/tests/test2.yaml index 935f9edb84..bcc777b83b 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -10,7 +10,7 @@ substitutions: ethernet: type: LAN8720 mdc_pin: GPIO23 - mdio_pin: GPIO24 + mdio_pin: GPIO25 clk_mode: GPIO0_IN phy_addr: 0 power_pin: GPIO25 @@ -286,7 +286,7 @@ stepper: - platform: uln2003 id: my_stepper pin_a: GPIO23 - pin_b: GPIO24 + pin_b: GPIO27 pin_c: GPIO25 pin_d: GPIO26 sleep_when_done: no From f68a3a9334da5288b39035542b95e0c194198a7e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 16:48:08 +0100 Subject: [PATCH 114/412] Disable default wait_time for rc_switch (#900) See also https://github.com/esphome/esphome/commit/82625a30808f4c914bb9e6a2b3e7ef229a451c99#commitcomment-36092052 --- esphome/components/remote_base/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a62304c87d..023f0f253e 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -505,7 +505,7 @@ RC_SWITCH_TYPE_D_SCHEMA = cv.Schema({ RC_SWITCH_TRANSMITTER = cv.Schema({ cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='10ms'): + cv.Optional(CONF_WAIT_TIME, default='0us'): cv.templatable(cv.positive_time_period_microseconds), }), }) From 33c08812ccc34515f5fb163bbd937ecdce772971 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 17:12:26 +0100 Subject: [PATCH 115/412] Update ESP32 BLE ADV parse to match BLE spec (#904) * Update ESP32 BLE ADV parse to match BLE spec * Update xiaomi * Update ruuvi * Format * Update esp32_ble_tracker.cpp * Fix log * Format * Update xiaomi_ble.cpp --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 130 ++++++++++++------ .../esp32_ble_tracker/esp32_ble_tracker.h | 56 +++++--- esphome/components/ruuvi_ble/ruuvi_ble.cpp | 35 +++-- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 31 +++-- esphome/core/helpers.h | 1 + 5 files changed, 158 insertions(+), 95 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 53c6de62cc..ab6bfa681c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -203,10 +203,6 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode_string(const std::string &raw_data) { - return hexencode(reinterpret_cast(raw_data.c_str()), raw_data.size()); -} - ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -267,13 +263,13 @@ std::string ESPBTUUID::to_string() { } ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } -optional ESPBLEiBeacon::from_manufacturer_data(const std::string &data) { - if (data.size() != 25) - return {}; - if (data[0] != 0x4C || data[1] != 0x00) +optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { + if (!data.uuid.contains(0x4C, 0x00)) return {}; - return ESPBLEiBeacon(reinterpret_cast(data.data())); + if (data.data.size() != 23) + return {}; + return ESPBLEiBeacon(data.data.data()); } void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -305,8 +301,8 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " RSSI: %d", this->rssi_); ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str()); - if (this->tx_power_.has_value()) { - ESP_LOGVV(TAG, " TX Power: %d", *this->tx_power_); + for (auto &it : this->tx_powers_) { + ESP_LOGVV(TAG, " TX Power: %d", it); } if (this->appearance_.has_value()) { ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_); @@ -314,20 +310,19 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e if (this->ad_flag_.has_value()) { ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); } - for (auto uuid : this->service_uuids_) { + for (auto &uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); - - if (this->service_data_uuid_.has_value()) { - ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); + for (auto &data : this->manufacturer_datas_) { + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + } + for (auto &data : this->service_datas_) { + ESP_LOGVV(TAG, " Service data:"); + ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); + ESP_LOGVV(TAG, " Data: %s", hexencode(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", - hexencode_string( - std::string(reinterpret_cast(param.ble_adv), param.adv_data_len + param.scan_rsp_len)) - .c_str()); + ESP_LOGVV(TAG, "Adv data: %s", hexencode(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -346,25 +341,52 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p const uint8_t record_length = field_length - 1; offset += record_length; + // See also Generic Access Profile Assigned Numbers: + // https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN + // RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11) + // See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/ + // (called CSS here) + switch (record_type) { case ESP_BLE_AD_TYPE_NAME_CMPL: { + // CSS 1.2 LOCAL NAME + // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the + // device." CSS 1: Optional in this context; shall not appear more than once in a block. this->name_ = std::string(reinterpret_cast(record), record_length); break; } case ESP_BLE_AD_TYPE_TX_PWR: { - this->tx_power_ = *payload; + // CSS 1.5 TX POWER LEVEL + // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." + // CSS 1: Optional in this context (may appear more than once in a block). + this->tx_powers_.push_back(*payload); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { + // CSS 1.12 APPEARANCE + // "The Appearance data type defines the external appearance of the device." + // See also https://www.bluetooth.com/specifications/gatt/characteristics/ + // CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both + // the AD and SRD of the same extended advertising interval. this->appearance_ = *reinterpret_cast(record); break; } case ESP_BLE_AD_TYPE_FLAG: { + // CSS 1.3 FLAGS + // "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the + // Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be + // omitted." + // CSS 1: Optional in this context; shall not appear more than once in a block. this->ad_flag_ = *record; break; } + // CSS 1.1 SERVICE UUID + // The Service UUID data type is used to include a list of Service or Service Class UUIDs. + // There are six data types defined for the three sizes of Service UUIDs that may be returned: + // CSS 1: Optional in this context (may appear more than once in a block). case ESP_BLE_AD_TYPE_16SRV_CMPL: case ESP_BLE_AD_TYPE_16SRV_PART: { + // • 16-bit Bluetooth Service UUIDs for (uint8_t i = 0; i < record_length / 2; i++) { this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast(record + 2 * i))); } @@ -372,6 +394,7 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p } case ESP_BLE_AD_TYPE_32SRV_CMPL: case ESP_BLE_AD_TYPE_32SRV_PART: { + // • 32-bit Bluetooth Service UUIDs for (uint8_t i = 0; i < record_length / 4; i++) { this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast(record + 4 * i))); } @@ -379,41 +402,70 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p } case ESP_BLE_AD_TYPE_128SRV_CMPL: case ESP_BLE_AD_TYPE_128SRV_PART: { + // • Global 128-bit Service UUIDs this->service_uuids_.push_back(ESPBTUUID::from_raw(record)); break; } case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: { - this->manufacturer_data_ = std::string(reinterpret_cast(record), record_length); + // CSS 1.4 MANUFACTURER SPECIFIC DATA + // "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall + // contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data + // shall be defined by the manufacturer specified by the company identifier." + // CSS 1: Optional in this context (may appear more than once in a block). + if (record_length < 2) { + ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE"); + break; + } + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->manufacturer_datas_.push_back(data); break; } + + // CSS 1.11 SERVICE DATA + // "The Service Data data type consists of a service UUID with the data associated with that service." + // CSS 1: Optional in this context (may appear more than once in a block). case ESP_BLE_AD_TYPE_SERVICE_DATA: { + // «Service Data - 16 bit UUID» + // Size: 2 or more octets + // The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data if (record_length < 2) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_uint16(*reinterpret_cast(record)); - if (record_length > 2) - this->service_data_ = std::string(reinterpret_cast(record + 2), record_length - 2UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast(record)); + data.data.assign(record + 2UL, record + record_length); + this->service_datas_.push_back(data); break; } case ESP_BLE_AD_TYPE_32SERVICE_DATA: { + // «Service Data - 32 bit UUID» + // Size: 4 or more octets + // The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data if (record_length < 4) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_uint32(*reinterpret_cast(record)); - if (record_length > 4) - this->service_data_ = std::string(reinterpret_cast(record + 4), record_length - 4UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast(record)); + data.data.assign(record + 4UL, record + record_length); + this->service_datas_.push_back(data); break; } case ESP_BLE_AD_TYPE_128SERVICE_DATA: { + // «Service Data - 128 bit UUID» + // Size: 16 or more octets + // The first 16 octets contain the 128 bit Service UUID followed by additional service data if (record_length < 16) { ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA"); break; } - this->service_data_uuid_ = ESPBTUUID::from_raw(record); - if (record_length > 16) - this->service_data_ = std::string(reinterpret_cast(record + 16), record_length - 16UL); + ServiceData data{}; + data.uuid = ESPBTUUID::from_raw(record); + data.data.assign(record + 16UL, record + record_length); + this->service_datas_.push_back(data); break; } default: { @@ -430,16 +482,6 @@ std::string ESPBTDevice::address_str() const { return mac; } uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } -esp_ble_addr_type_t ESPBTDevice::get_address_type() const { return this->address_type_; } -int ESPBTDevice::get_rssi() const { return this->rssi_; } -const std::string &ESPBTDevice::get_name() const { return this->name_; } -const optional &ESPBTDevice::get_tx_power() const { return this->tx_power_; } -const optional &ESPBTDevice::get_appearance() const { return this->appearance_; } -const optional &ESPBTDevice::get_ad_flag() const { return this->ad_flag_; } -const std::vector &ESPBTDevice::get_service_uuids() const { return this->service_uuids_; } -const std::string &ESPBTDevice::get_manufacturer_data() const { return this->manufacturer_data_; } -const std::string &ESPBTDevice::get_service_data() const { return this->service_data_; } -const optional &ESPBTDevice::get_service_data_uuid() const { return this->service_data_uuid_; } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); @@ -480,8 +522,8 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { ESP_LOGD(TAG, " Address Type: %s", address_type_s); if (!device.get_name().empty()) ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); - if (device.get_tx_power().has_value()) { - ESP_LOGD(TAG, " TX Power: %d", *device.get_tx_power()); + for (auto &tx_power : device.get_tx_powers()) { + ESP_LOGD(TAG, " TX Power: %d", tx_power); } } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 280c3fc45f..74bb7e5d10 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -33,11 +33,18 @@ class ESPBTUUID { esp_bt_uuid_t uuid_; }; +using adv_data_t = std::vector; + +struct ServiceData { + ESPBTUUID uuid; + adv_data_t data; +}; + class ESPBLEiBeacon { public: ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } ESPBLEiBeacon(const uint8_t *data); - static optional from_manufacturer_data(const std::string &data); + static optional from_manufacturer_data(const ServiceData &data); uint16_t get_major() { return reverse_bits_16(this->beacon_data_.major); } uint16_t get_minor() { return reverse_bits_16(this->beacon_data_.minor); } @@ -46,7 +53,6 @@ class ESPBLEiBeacon { protected: struct { - uint16_t manufacturer_id; uint8_t sub_type; uint8_t proximity_uuid[16]; uint16_t major; @@ -63,18 +69,33 @@ class ESPBTDevice { uint64_t address_uint64() const; - esp_ble_addr_type_t get_address_type() const; - int get_rssi() const; - const std::string &get_name() const; - const optional &get_tx_power() const; - const optional &get_appearance() const; - const optional &get_ad_flag() const; - const std::vector &get_service_uuids() const; - const std::string &get_manufacturer_data() const; - const std::string &get_service_data() const; - const optional &get_service_data_uuid() const; - const optional get_ibeacon() const { - return ESPBLEiBeacon::from_manufacturer_data(this->manufacturer_data_); + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } + int get_rssi() const { return rssi_; } + const std::string &get_name() const { return this->name_; } + + ESPDEPRECATED("Use get_tx_powers() instead") + optional get_tx_power() const { + if (this->tx_powers_.empty()) + return {}; + return this->tx_powers_[0]; + } + const std::vector &get_tx_powers() const { return tx_powers_; } + + const optional &get_appearance() const { return appearance_; } + const optional &get_ad_flag() const { return ad_flag_; } + const std::vector &get_service_uuids() const { return service_uuids_; } + + const std::vector &get_manufacturer_datas() const { return manufacturer_datas_; } + + const std::vector &get_service_datas() const { return service_datas_; } + + optional get_ibeacon() const { + for (auto &it : this->manufacturer_datas_) { + auto res = ESPBLEiBeacon::from_manufacturer_data(it); + if (res.has_value()) + return *res; + } + return {}; } protected: @@ -86,13 +107,12 @@ class ESPBTDevice { esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC}; int rssi_{0}; std::string name_{}; - optional tx_power_{}; + std::vector tx_powers_{}; optional appearance_{}; optional ad_flag_{}; std::vector service_uuids_; - std::string manufacturer_data_{}; - std::string service_data_{}; - optional service_data_uuid_{}; + std::vector manufacturer_datas_{}; + std::vector service_datas_{}; }; class ESP32BLETracker; diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 28a689fee4..7e13140e55 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -8,10 +8,12 @@ namespace ruuvi_ble { static const char *TAG = "ruuvi_ble"; -bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t *data, RuuviParseResult &result) { +bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviParseResult &result) { + const uint8_t data_type = adv_data[0]; + const auto *data = &adv_data[1]; switch (data_type) { case 0x03: { // RAWv1 - if (data_length != 16) + if (adv_data.size() != 14) return false; const uint8_t temp_sign = (data[1] >> 7) & 1; @@ -32,13 +34,13 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t result.acceleration_y = acceleration_y; result.acceleration_z = acceleration_z; result.acceleration = - sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); result.battery_voltage = battery_voltage; return true; } case 0x05: { // RAWv2 - if (data_length != 26) + if (adv_data.size() != 24) return false; const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; @@ -63,8 +65,8 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN ? NAN - : sqrt(acceleration_x * acceleration_x + acceleration_y * acceleration_y + - acceleration_z * acceleration_z); + : sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + + acceleration_z * acceleration_z); result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; result.movement_counter = movement_counter; @@ -77,21 +79,16 @@ bool parse_ruuvi_data_byte(uint8_t data_type, uint8_t data_length, const uint8_t } } optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device) { - const auto *raw = reinterpret_cast(device.get_manufacturer_data().data()); + bool success = false; + RuuviParseResult result{}; + for (auto &it : device.get_manufacturer_datas()) { + bool is_ruuvi = it.uuid.contains(0x99, 0x04); + if (!is_ruuvi) + continue; - bool is_ruuvi = raw[0] == 0x99 && raw[1] == 0x04; - - if (!is_ruuvi) { - return {}; + if (parse_ruuvi_data_byte(it.data, result)) + success = true; } - - const uint8_t data_length = device.get_manufacturer_data().size(); - const uint8_t format = raw[2]; - const uint8_t *data = &raw[3]; - - RuuviParseResult result; - - bool success = parse_ruuvi_data_byte(format, data_length, data, result); if (!success) return {}; return result; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index e6884e5ea6..18eaffed06 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -63,22 +63,17 @@ bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data return false; } } -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { - if (!device.get_service_data_uuid().has_value()) { - // ESP_LOGVV(TAG, "Xiaomi no service data"); - return {}; - } - - if (!device.get_service_data_uuid()->contains(0x95, 0xFE)) { +bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) { + if (!service_data.uuid.contains(0x95, 0xFE)) { // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); - return {}; + return false; } - const auto *raw = reinterpret_cast(device.get_service_data().data()); + const auto raw = service_data.data; - if (device.get_service_data().size() < 14) { + if (raw.size() < 14) { // ESP_LOGVV(TAG, "Xiaomi service data too short!"); - return {}; + return false; } bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; @@ -88,10 +83,9 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); - return {}; + return false; } - XiaomiParseResult result; result.type = XiaomiParseResult::TYPE_HHCCJCY01; if (is_lywsdcgq) { result.type = XiaomiParseResult::TYPE_LYWSDCGQ; @@ -111,7 +105,7 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d const uint8_t *raw_data = &raw[raw_offset]; uint8_t data_offset = 0; - uint8_t data_length = device.get_service_data().size() - raw_offset; + uint8_t data_length = raw.size() - raw_offset; bool success = false; while (true) { @@ -136,6 +130,15 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d data_offset += 3 + datapoint_length; } + return success; +} +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { + XiaomiParseResult result; + bool success = false; + for (auto &service_data : device.get_service_datas()) { + if (parse_xiaomi_service_data(result, service_data)) + success = true; + } if (!success) return {}; return result; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 88f0d587e5..91b78b73a0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -158,6 +158,7 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch // Encode raw data to a human-readable string (for debugging) std::string hexencode(const uint8_t *data, uint32_t len); +template std::string hexencode(const T &data) { return hexencode(data.data(), data.size()); } // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template struct seq {}; // NOLINT From bba6d6897d140b45fc7eafff26d0330c1df7af79 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 17:13:34 +0100 Subject: [PATCH 116/412] Update dependencies (#906) PyYAML 5.1.2 -> 5.2 https://github.com/yaml/pyyaml/blob/master/CHANGES flake8 3.6.0 -> 3.7.9 https://github.com/PyCQA/flake8/tree/master/docs/source/release-notes paho-mqtt 1.4.0 -> 1.5.0 https://github.com/eclipse/paho.mqtt.python/blob/master/ChangeLog.txt platformio 4.0.3 -> 4.1.0 https://github.com/platformio/platformio-core/releases protobuf 3.10.0 -> 3.11.1 https://github.com/protocolbuffers/protobuf/releases pylint 2.3.0 -> 2.4.4 http://pylint.pycqa.org/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-4 --- esphome/components/time/__init__.py | 12 ++++++------ esphome/core.py | 10 +++++----- esphome/cpp_helpers.py | 2 +- esphome/storage_json.py | 4 ++-- esphome/vscode.py | 8 ++++++-- esphome/writer.py | 2 +- pylintrc | 1 + requirements.txt | 8 ++++---- requirements_test.txt | 12 ++++++------ setup.cfg | 2 +- setup.py | 8 ++++---- 11 files changed, 37 insertions(+), 32 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index ca1ac375ba..58739772b0 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -301,17 +301,17 @@ def setup_time_core_(time_var, config): for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) - seconds = conf.get(CONF_SECONDS, [x for x in range(0, 61)]) + seconds = conf.get(CONF_SECONDS, list(range(0, 61))) cg.add(trigger.add_seconds(seconds)) - minutes = conf.get(CONF_MINUTES, [x for x in range(0, 60)]) + minutes = conf.get(CONF_MINUTES, list(range(0, 60))) cg.add(trigger.add_minutes(minutes)) - hours = conf.get(CONF_HOURS, [x for x in range(0, 24)]) + hours = conf.get(CONF_HOURS, list(range(0, 24))) cg.add(trigger.add_hours(hours)) - days_of_month = conf.get(CONF_DAYS_OF_MONTH, [x for x in range(1, 32)]) + days_of_month = conf.get(CONF_DAYS_OF_MONTH, list(range(1, 32))) cg.add(trigger.add_days_of_month(days_of_month)) - months = conf.get(CONF_MONTHS, [x for x in range(1, 13)]) + months = conf.get(CONF_MONTHS, list(range(1, 13))) cg.add(trigger.add_months(months)) - days_of_week = conf.get(CONF_DAYS_OF_WEEK, [x for x in range(1, 8)]) + days_of_week = conf.get(CONF_DAYS_OF_WEEK, list(range(1, 8))) cg.add(trigger.add_days_of_week(days_of_week)) yield cg.register_component(trigger, conf) diff --git a/esphome/core.py b/esphome/core.py index 82ce196cab..9df30384bc 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -8,7 +8,7 @@ import os import re # pylint: disable=unused-import, wrong-import-order -from typing import Any, Dict, List # noqa +from typing import Any, Dict, List, Optional, Set # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI @@ -271,7 +271,7 @@ class ID(object): else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type = type # type: Optional[MockObjClass] + self.type = type # type: Optional['MockObjClass'] def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -489,11 +489,11 @@ class EsphomeCore(object): # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables = {} # type: Dict[str, MockObj] + self.variables = {} # type: Dict[str, 'MockObj'] # A list of statements that go in the main setup() block - self.main_statements = [] # type: List[Statement] + self.main_statements = [] # type: List['Statement'] # A list of statements to insert in the global block (includes and global variables) - self.global_statements = [] # type: List[Statement] + self.global_statements = [] # type: List['Statement'] # A set of platformio libraries to add to the project self.libraries = [] # type: List[Library] # A set of build flags to set in the platformio project diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index fd79feec1c..39ac8e7118 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -59,7 +59,7 @@ def register_parented(var, value): def extract_registry_entry_config(registry, full_config): - # type: (Registry, ConfigType) -> RegistryEntry + # type: ('Registry', 'ConfigType') -> 'RegistryEntry' key, config = next((k, v) for k, v in full_config.items() if k in registry) return registry[key], config diff --git a/esphome/storage_json.py b/esphome/storage_json.py index e1b4070d3b..f3e53ce168 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -10,8 +10,8 @@ from esphome.core import CORE from esphome.helpers import write_file_if_changed # pylint: disable=unused-import, wrong-import-order -from esphome.core import CoreType # noqa -from typing import Any, Dict, Optional # noqa +from esphome.core import CoreType +from typing import Any, Optional, List from esphome.py_compat import text_type diff --git a/esphome/vscode.py b/esphome/vscode.py index 151bfb5281..6b35d4bd7a 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -3,10 +3,14 @@ from __future__ import print_function import json import os -from esphome.config import load_config, _format_vol_invalid -from esphome.core import CORE +from esphome.config import load_config, _format_vol_invalid, Config +from esphome.core import CORE, DocumentRange from esphome.py_compat import text_type, safe_input +# pylint: disable=unused-import, wrong-import-order +import voluptuous as vol +from typing import Optional + def _get_invalid_range(res, invalid): # type: (Config, vol.Invalid) -> Optional[DocumentRange] diff --git a/esphome/writer.py b/esphome/writer.py index 1961af5a15..b036413a66 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -291,7 +291,7 @@ def copy_src_tree(): source_files.update(component.source_files) # Convert to list and sort - source_files_l = [it for it in source_files.items()] + source_files_l = list(source_files.items()) source_files_l.sort() # Build #include list for esphome.h diff --git a/pylintrc b/pylintrc index b011e2750c..89cc73656f 100644 --- a/pylintrc +++ b/pylintrc @@ -24,6 +24,7 @@ disable= useless-object-inheritance, stop-iteration-return, no-self-use, + import-outside-toplevel, additional-builtins= diff --git a/requirements.txt b/requirements.txt index 14ce062000..b8af2a605b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ voluptuous==0.11.7 -PyYAML==5.1.2 -paho-mqtt==1.4.0 +PyYAML==5.2 +paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 typing>=3.6.6;python_version<"3.5" -protobuf==3.10.0 +protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 pyserial==3.4 ifaddr==0.1.6 -platformio==4.0.3 +platformio==4.1.0 esptool==2.7 diff --git a/requirements_test.txt b/requirements_test.txt index 26f14434d8..ff02badf80 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,19 +1,19 @@ voluptuous==0.11.7 -PyYAML==5.1.2 -paho-mqtt==1.4.0 +PyYAML==5.2 +paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 typing>=3.6.6;python_version<"3.5" -protobuf==3.10.0 +protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 pyserial==3.4 ifaddr==0.1.6 -platformio==4.0.3 +platformio==4.1.0 esptool==2.7 pylint==1.9.4 ; python_version<"3" -pylint==2.3.0 ; python_version>"3" -flake8==3.6.0 +pylint==2.4.4 ; python_version>"3" +flake8==3.7.9 pillow pexpect diff --git a/setup.cfg b/setup.cfg index e3521f48b7..bef998fb37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ Topic :: Home Automation [flake8] max-line-length = 120 -builtins = unicode, long, raw_input +builtins = unicode, long, raw_input, basestring exclude = api_pb2.py [bdist_wheel] diff --git a/setup.py b/setup.py index 641bb3e431..1d4fb3a4d0 100755 --- a/setup.py +++ b/setup.py @@ -24,12 +24,12 @@ DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) REQUIRES = [ 'voluptuous==0.11.7', - 'PyYAML==5.1.2', - 'paho-mqtt==1.4.0', + 'PyYAML==5.2', + 'paho-mqtt==1.5.0', 'colorlog==4.0.2', 'tornado==5.1.1', 'typing>=3.6.6;python_version<"3.6"', - 'protobuf==3.10.0', + 'protobuf==3.11.1', 'tzlocal==2.0.0', 'pytz==2019.3', 'pyserial==3.4', @@ -41,7 +41,7 @@ REQUIRES = [ # This means they have to be in your $PATH. if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: REQUIRES.extend([ - 'platformio==4.0.3', + 'platformio==4.1.0', 'esptool==2.7', ]) From 7a6df38515fb4bdd48db1b25c8004d1d5d755872 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 19:30:10 +0100 Subject: [PATCH 117/412] Add ESP8266 core v2.6.2 (#905) * Add ESP8266 core v2.6.2 * Upstream ESP8266 Wifi fixes * Replace disable_interrupt with InterruptLock C++ class * Update code to use InterruptLock * Lint * Update dht.cpp * Improve InterruptLock docs, mark as ICACHE_RAM_ATTR * Fixes --- .../components/dallas/dallas_component.cpp | 68 +++++---- esphome/components/dallas/esp_one_wire.cpp | 26 ++-- esphome/components/dht/dht.cpp | 137 ++++++++++-------- esphome/components/hx711/hx711.cpp | 31 ++-- .../remote_transmitter_esp8266.cpp | 24 +-- esphome/components/uart/uart.cpp | 39 ++--- .../wifi/wifi_component_esp8266.cpp | 59 +++++++- esphome/core/helpers.cpp | 28 ++-- esphome/core/helpers.h | 34 ++++- esphome/core/preferences.cpp | 22 +-- esphome/core_config.py | 2 + esphome/platformio_api.py | 1 + 12 files changed, 291 insertions(+), 180 deletions(-) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 6eeddb1b56..aa839e7331 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -32,9 +32,11 @@ void DallasComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); yield(); - disable_interrupts(); - std::vector raw_sensors = this->one_wire_->search_vec(); - enable_interrupts(); + std::vector raw_sensors; + { + InterruptLock lock; + raw_sensors = this->one_wire_->search_vec(); + } for (auto &address : raw_sensors) { std::string s = uint64_to_string(address); @@ -108,16 +110,17 @@ DallasTemperatureSensor *DallasComponent::get_sensor_by_index(uint8_t index, uin void DallasComponent::update() { this->status_clear_warning(); - disable_interrupts(); bool result; - if (!this->one_wire_->reset()) { - result = false; - } else { - result = true; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + { + InterruptLock lock; + if (!this->one_wire_->reset()) { + result = false; + } else { + result = true; + this->one_wire_->skip(); + this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + } } - enable_interrupts(); if (!result) { ESP_LOGE(TAG, "Requesting conversion failed"); @@ -127,9 +130,11 @@ void DallasComponent::update() { for (auto *sensor : this->sensors_) { this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { - disable_interrupts(); - bool res = sensor->read_scratch_pad(); - enable_interrupts(); + bool res; + { + InterruptLock lock; + res = sensor->read_scratch_pad(); + } if (!res) { ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); @@ -170,7 +175,7 @@ const std::string &DallasTemperatureSensor::get_address_name() { return this->address_name_; } -bool DallasTemperatureSensor::read_scratch_pad() { +bool ICACHE_RAM_ATTR DallasTemperatureSensor::read_scratch_pad() { ESPOneWire *wire = this->parent_->one_wire_; if (!wire->reset()) { return false; @@ -185,9 +190,11 @@ bool DallasTemperatureSensor::read_scratch_pad() { return true; } bool DallasTemperatureSensor::setup_sensor() { - disable_interrupts(); - bool r = this->read_scratch_pad(); - enable_interrupts(); + bool r; + { + InterruptLock lock; + r = this->read_scratch_pad(); + } if (!r) { ESP_LOGE(TAG, "Reading scratchpad failed: reset"); @@ -222,20 +229,21 @@ bool DallasTemperatureSensor::setup_sensor() { } ESPOneWire *wire = this->parent_->one_wire_; - disable_interrupts(); - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); + { + InterruptLock lock; + if (wire->reset()) { + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); + wire->write8(this->scratch_pad_[2]); // high alarm temp + wire->write8(this->scratch_pad_[3]); // low alarm temp + wire->write8(this->scratch_pad_[4]); // resolution + wire->reset(); - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); + // write value to EEPROM + wire->select(this->address_); + wire->write8(0x48); + } } - enable_interrupts(); delay(20); // allow it to finish operation wire->reset(); diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index e0db3118b9..d90b10894d 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -12,7 +12,7 @@ const int ONE_WIRE_ROM_SEARCH = 0xF0; ESPOneWire::ESPOneWire(GPIOPin *pin) : pin_(pin) {} -bool HOT ESPOneWire::reset() { +bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { uint8_t retries = 125; // Wait for communication to clear @@ -39,7 +39,7 @@ bool HOT ESPOneWire::reset() { return r; } -void HOT ESPOneWire::write_bit(bool bit) { +void HOT ICACHE_RAM_ATTR ESPOneWire::write_bit(bool bit) { // Initiate write/read by pulling low. this->pin_->pin_mode(OUTPUT); this->pin_->digital_write(false); @@ -60,7 +60,7 @@ void HOT ESPOneWire::write_bit(bool bit) { } } -bool HOT ESPOneWire::read_bit() { +bool HOT ICACHE_RAM_ATTR ESPOneWire::read_bit() { // Initiate read slot by pulling LOW for at least 1µs this->pin_->pin_mode(OUTPUT); this->pin_->digital_write(false); @@ -76,43 +76,43 @@ bool HOT ESPOneWire::read_bit() { return r; } -void ESPOneWire::write8(uint8_t val) { +void ICACHE_RAM_ATTR ESPOneWire::write8(uint8_t val) { for (uint8_t i = 0; i < 8; i++) { this->write_bit(bool((1u << i) & val)); } } -void ESPOneWire::write64(uint64_t val) { +void ICACHE_RAM_ATTR ESPOneWire::write64(uint64_t val) { for (uint8_t i = 0; i < 64; i++) { this->write_bit(bool((1ULL << i) & val)); } } -uint8_t ESPOneWire::read8() { +uint8_t ICACHE_RAM_ATTR ESPOneWire::read8() { uint8_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint8_t(this->read_bit()) << i); } return ret; } -uint64_t ESPOneWire::read64() { +uint64_t ICACHE_RAM_ATTR ESPOneWire::read64() { uint64_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint64_t(this->read_bit()) << i); } return ret; } -void ESPOneWire::select(uint64_t address) { +void ICACHE_RAM_ATTR ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void ESPOneWire::reset_search() { +void ICACHE_RAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } -uint64_t HOT ESPOneWire::search() { +uint64_t HOT ICACHE_RAM_ATTR ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } @@ -196,7 +196,7 @@ uint64_t HOT ESPOneWire::search() { return this->rom_number_; } -std::vector ESPOneWire::search_vec() { +std::vector ICACHE_RAM_ATTR ESPOneWire::search_vec() { std::vector res; this->reset_search(); @@ -206,12 +206,12 @@ std::vector ESPOneWire::search_vec() { return res; } -void ESPOneWire::skip() { +void ICACHE_RAM_ATTR ESPOneWire::skip() { this->write8(0xCC); // skip ROM } GPIOPin *ESPOneWire::get_pin() { return this->pin_; } -uint8_t *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } +uint8_t ICACHE_RAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast(&this->rom_number_); } } // namespace dallas } // namespace esphome diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 28a365c49f..c311aa43ec 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -71,80 +71,101 @@ void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; } -bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_errors) { +bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool report_errors) { *humidity = NAN; *temperature = NAN; - disable_interrupts(); - this->pin_->digital_write(false); - this->pin_->pin_mode(OUTPUT); - this->pin_->digital_write(false); - - if (this->model_ == DHT_MODEL_DHT11) { - delayMicroseconds(18000); - } else if (this->model_ == DHT_MODEL_SI7021) { - delayMicroseconds(500); - this->pin_->digital_write(true); - delayMicroseconds(40); - } else { - delayMicroseconds(800); - } - this->pin_->pin_mode(INPUT_PULLUP); - delayMicroseconds(40); - + int error_code = 0; + int8_t i = 0; uint8_t data[5] = {0, 0, 0, 0, 0}; - uint8_t bit = 7; - uint8_t byte = 0; - for (int8_t i = -1; i < 40; i++) { - uint32_t start_time = micros(); + { + InterruptLock lock; - // Wait for rising edge - while (!this->pin_->digital_read()) { - if (micros() - start_time > 90) { - enable_interrupts(); - if (report_errors) { - if (i < 0) { - ESP_LOGW(TAG, "Waiting for DHT communication to clear failed!"); - } else { - ESP_LOGW(TAG, "Rising edge for bit %d failed!", i); - } + this->pin_->digital_write(false); + this->pin_->pin_mode(OUTPUT); + this->pin_->digital_write(false); + + if (this->model_ == DHT_MODEL_DHT11) { + delayMicroseconds(18000); + } else if (this->model_ == DHT_MODEL_SI7021) { + delayMicroseconds(500); + this->pin_->digital_write(true); + delayMicroseconds(40); + } else { + delayMicroseconds(800); + } + this->pin_->pin_mode(INPUT_PULLUP); + delayMicroseconds(40); + + uint8_t bit = 7; + uint8_t byte = 0; + + for (i = -1; i < 40; i++) { + uint32_t start_time = micros(); + + // Wait for rising edge + while (!this->pin_->digital_read()) { + if (micros() - start_time > 90) { + if (i < 0) + error_code = 1; + else + error_code = 2; + break; } - return false; } - } + if (error_code != 0) + break; - start_time = micros(); - uint32_t end_time = start_time; + start_time = micros(); + uint32_t end_time = start_time; - // Wait for falling edge - while (this->pin_->digital_read()) { - if ((end_time = micros()) - start_time > 90) { - enable_interrupts(); - if (report_errors) { - if (i < 0) { - ESP_LOGW(TAG, "Requesting data from DHT failed!"); - } else { - ESP_LOGW(TAG, "Falling edge for bit %d failed!", i); - } + // Wait for falling edge + while (this->pin_->digital_read()) { + if ((end_time = micros()) - start_time > 90) { + if (i < 0) + error_code = 3; + else + error_code = 4; + break; } - return false; } - } + if (error_code != 0) + break; - if (i < 0) - continue; + if (i < 0) + continue; - if (end_time - start_time >= 40) { - data[byte] |= 1 << bit; + if (end_time - start_time >= 40) { + data[byte] |= 1 << bit; + } + if (bit == 0) { + bit = 7; + byte++; + } else + bit--; } - if (bit == 0) { - bit = 7; - byte++; - } else - bit--; } - enable_interrupts(); + if (!report_errors && error_code != 0) + return false; + + switch (error_code) { + case 1: + ESP_LOGW(TAG, "Waiting for DHT communication to clear failed!"); + return false; + case 2: + ESP_LOGW(TAG, "Rising edge for bit %d failed!", i); + return false; + case 3: + ESP_LOGW(TAG, "Requesting data from DHT failed!"); + return false; + case 4: + ESP_LOGW(TAG, "Falling edge for bit %d failed!", i); + return false; + case 0: + default: + break; + } ESP_LOGVV(TAG, "Data: Hum=0b" BYTE_TO_BINARY_PATTERN BYTE_TO_BINARY_PATTERN diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 1c808a2501..605f534f91 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -42,23 +42,24 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { this->status_clear_warning(); uint32_t data = 0; - disable_interrupts(); - for (uint8_t i = 0; i < 24; i++) { - this->sck_pin_->digital_write(true); - delayMicroseconds(1); - data |= uint32_t(this->dout_pin_->digital_read()) << (23 - i); - this->sck_pin_->digital_write(false); - delayMicroseconds(1); - } + { + InterruptLock lock; + for (uint8_t i = 0; i < 24; i++) { + this->sck_pin_->digital_write(true); + delayMicroseconds(1); + data |= uint32_t(this->dout_pin_->digital_read()) << (23 - i); + this->sck_pin_->digital_write(false); + delayMicroseconds(1); + } - // Cycle clock pin for gain setting - for (uint8_t i = 0; i < this->gain_; i++) { - this->sck_pin_->digital_write(true); - delayMicroseconds(1); - this->sck_pin_->digital_write(false); - delayMicroseconds(1); + // Cycle clock pin for gain setting + for (uint8_t i = 0; i < this->gain_; i++) { + this->sck_pin_->digital_write(true); + delayMicroseconds(1); + this->sck_pin_->digital_write(false); + delayMicroseconds(1); + } } - enable_interrupts(); if (data & 0x800000ULL) { data |= 0xFF000000ULL; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 7704f1d9ab..e8906e87aa 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -67,22 +67,22 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); for (uint32_t i = 0; i < send_times; i++) { - disable_interrupts(); - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); + { + InterruptLock lock; + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); + } + App.feed_wdt(); } - App.feed_wdt(); } - enable_interrupts(); if (i + 1 < send_times) { - delay(send_wait / 1000UL); - delayMicroseconds(send_wait % 1000UL); + delay_microseconds_accurate(send_wait); } } } diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 1f40498606..205e9e2300 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -295,24 +295,25 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { return; } - disable_interrupts(); - uint32_t wait = this->bit_time_; - const uint32_t start = ESP.getCycleCount(); - // Start bit - this->write_bit_(false, &wait, start); - this->write_bit_(data & (1 << 0), &wait, start); - this->write_bit_(data & (1 << 1), &wait, start); - this->write_bit_(data & (1 << 2), &wait, start); - this->write_bit_(data & (1 << 3), &wait, start); - this->write_bit_(data & (1 << 4), &wait, start); - this->write_bit_(data & (1 << 5), &wait, start); - this->write_bit_(data & (1 << 6), &wait, start); - this->write_bit_(data & (1 << 7), &wait, start); - // Stop bit - this->write_bit_(true, &wait, start); - if (this->stop_bits_ == 2) - this->wait_(&wait, start); - enable_interrupts(); + { + InterruptLock lock; + uint32_t wait = this->bit_time_; + const uint32_t start = ESP.getCycleCount(); + // Start bit + this->write_bit_(false, &wait, start); + this->write_bit_(data & (1 << 0), &wait, start); + this->write_bit_(data & (1 << 1), &wait, start); + this->write_bit_(data & (1 << 2), &wait, start); + this->write_bit_(data & (1 << 3), &wait, start); + this->write_bit_(data & (1 << 4), &wait, start); + this->write_bit_(data & (1 << 5), &wait, start); + this->write_bit_(data & (1 << 6), &wait, start); + this->write_bit_(data & (1 << 7), &wait, start); + // Stop bit + this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); + } } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { while (ESP.getCycleCount() - start < *wait) @@ -323,7 +324,7 @@ bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint this->wait_(wait, start); return this->rx_pin_->digital_read(); } -void ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { this->tx_pin_->digital_write(bit); this->wait_(wait, start); } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 88bcb03450..deee578b4c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -6,8 +6,16 @@ #include #include + +extern "C" { #include "lwip/err.h" #include "lwip/dns.h" +#include "lwip/dhcp.h" +#include "lwip/init.h" // LWIP_VERSION_ +#if LWIP_IPV6 +#include "lwip/netif.h" // struct netif +#endif +} #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -74,6 +82,19 @@ bool WiFiComponent::wifi_apply_power_save_() { } return wifi_set_sleep_type(power_save); } + +#if LWIP_VERSION_MAJOR != 1 +/* + lwip v2 needs to be notified of IP changes, see also + https://github.com/d-a-v/Arduino/blob/0e7d21e17144cfc5f53c016191daca8723e89ee8/libraries/ESP8266WiFi/src/ESP8266WiFiSTA.cpp#L251 + */ +#undef netif_set_addr // need to call lwIP-v1.4 netif_set_addr() +extern "C" { +struct netif *eagle_lwip_getif(int netif_index); +void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw); +}; +#endif + bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) @@ -94,6 +115,13 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { bool ret = true; +#if LWIP_VERSION_MAJOR != 1 + // get current->previous IP address + // (check below) + ip_info previp{}; + wifi_get_ip_info(STATION_IF, &previp); +#endif + struct ip_info info {}; info.ip.addr = static_cast(manual_ip->static_ip); info.gw.addr = static_cast(manual_ip->gateway); @@ -122,6 +150,14 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { dns_setserver(1, &dns); } +#if LWIP_VERSION_MAJOR != 1 + // trigger address change by calling lwIP-v1.4 api + // only when ip is already set by other mean (generally dhcp) + if (previp.ip.addr != 0 && previp.ip.addr != info.ip.addr) { + netif_set_addr(eagle_lwip_getif(STATION_IF), reinterpret_cast(&info.ip), + reinterpret_cast(&info.netmask), reinterpret_cast(&info.gw)); + } +#endif return ret; } @@ -133,10 +169,31 @@ IPAddress WiFiComponent::wifi_sta_ip_() { return {ip.ip.addr}; } bool WiFiComponent::wifi_apply_hostname_() { - bool ret = wifi_station_set_hostname(const_cast(App.get_name().c_str())); + const std::string &hostname = App.get_name(); + bool ret = wifi_station_set_hostname(const_cast(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Setting WiFi Hostname failed!"); } + + // inform dhcp server of hostname change using dhcp_renew() + for (netif *intf = netif_list; intf; intf = intf->next) { + // unconditionally update all known interfaces +#if LWIP_VERSION_MAJOR == 1 + intf->hostname = (char *) wifi_station_get_hostname(); +#else + intf->hostname = wifi_station_get_hostname(); +#endif + if (netif_dhcp_data(intf) != nullptr) { + // renew already started DHCP leases + err_t lwipret = dhcp_renew(intf); + if (lwipret != ERR_OK) { + ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, + (int) lwipret, intf->name[0], intf->name[1], intf->num); + ret = false; + } + } + } + return ret; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6d6aa80b66..3ff87678e8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,21 +156,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; -void disable_interrupts() { -#ifdef ARDUINO_ARCH_ESP32 - portDISABLE_INTERRUPTS(); -#else - noInterrupts(); -#endif -} -void enable_interrupts() { -#ifdef ARDUINO_ARCH_ESP32 - portENABLE_INTERRUPTS(); -#else - interrupts(); -#endif -} - uint8_t crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; @@ -193,8 +178,8 @@ void delay_microseconds_accurate(uint32_t usec) { if (usec <= 16383UL) { delayMicroseconds(usec); } else { - delay(usec / 1000UL); - delayMicroseconds(usec % 1000UL); + delay(usec / 16383UL); + delayMicroseconds(usec % 16383UL); } } @@ -330,4 +315,13 @@ std::string hexencode(const uint8_t *data, uint32_t len) { return res; } +#ifdef ARDUINO_ARCH_ESP8266 +ICACHE_RAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } +ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } +#endif +#ifdef ARDUINO_ARCH_ESP32 +ICACHE_RAM_ATTR InterruptLock::InterruptLock() { portENABLE_INTERRUPTS(); } +ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { portDISABLE_INTERRUPTS(); } +#endif + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 91b78b73a0..ab3d883e05 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -133,16 +133,38 @@ uint16_t encode_uint16(uint8_t msb, uint8_t lsb); /// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte. std::array decode_uint16(uint16_t value); -/** Cross-platform method to disable interrupts. +/*** + * An interrupt helper class. * - * Useful when you need to do some timing-dependent communication. + * This behaves like std::lock_guard. As long as the value is visible in the current stack, all interrupts + * (including flash reads) will be disabled. * - * @see Do not forget to call `enable_interrupts()` again or otherwise things will go very wrong. + * Please note all functions called when the interrupt lock must be marked ICACHE_RAM_ATTR (loading code into + * instruction cache is done via interrupts; disabling interrupts prevents data not already in cache from being + * pulled from flash). + * + * Example: + * + * ```cpp + * // interrupts are enabled + * { + * InterruptLock lock; + * // do something + * // interrupts are disabled + * } + * // interrupts are enabled + * ``` */ -void disable_interrupts(); +class InterruptLock { + public: + InterruptLock(); + ~InterruptLock(); -/// Cross-platform method to enable interrupts after they have been disabled. -void enable_interrupts(); + protected: +#ifdef ARDUINO_ARCH_ESP8266 + uint32_t xt_state_; +#endif +}; /// Calculate a crc8 of data with the provided data length. uint8_t crc8(uint8_t *data, uint8_t len); diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp index 2329ed34f5..8b41cbc7b5 100644 --- a/esphome/core/preferences.cpp +++ b/esphome/core/preferences.cpp @@ -105,16 +105,18 @@ void ESPPreferences::save_esp8266_flash_() { return; ESP_LOGVV(TAG, "Saving preferences to flash..."); - disable_interrupts(); - auto erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res == SPI_FLASH_RESULT_OK) { + write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); + } + } if (erase_res != SPI_FLASH_RESULT_OK) { - enable_interrupts(); ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); return; } - - auto write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); - enable_interrupts(); if (write_res != SPI_FLASH_RESULT_OK) { ESP_LOGV(TAG, "Write ESP8266 flash failed!"); return; @@ -173,9 +175,11 @@ ESPPreferences::ESPPreferences() void ESPPreferences::begin() { this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; ESP_LOGVV(TAG, "Loading preferences from flash..."); - disable_interrupts(); - spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); - enable_interrupts(); + + { + InterruptLock lock; + spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); + } } ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { diff --git a/esphome/core_config.py b/esphome/core_config.py index f7149db797..63092891d3 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -45,6 +45,8 @@ def validate_board(value): validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) PLATFORMIO_ESP8266_LUT = { + '2.6.2': 'espressif8266@2.3.1', + '2.6.1': 'espressif8266@2.3.0', '2.5.2': 'espressif8266@2.2.3', '2.5.1': 'espressif8266@2.1.0', '2.5.0': 'espressif8266@2.0.1', diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 36e451c21d..317670710b 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -60,6 +60,7 @@ FILTER_PLATFORMIO_LINES = [ r"Using cache: .*", r'Installing dependencies', r'.* @ .* is already installed', + r'Building in .* mode', ] From ea652e35879e554a478422eb0832d737ca6cc26c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 4 Dec 2019 23:51:27 +0100 Subject: [PATCH 118/412] Fix ESP32 interrupt enable/disable switched Needs to be manually cherry-picked --- esphome/core/helpers.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 3ff87678e8..2222a1a664 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -320,8 +320,8 @@ ICACHE_RAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } #endif #ifdef ARDUINO_ARCH_ESP32 -ICACHE_RAM_ATTR InterruptLock::InterruptLock() { portENABLE_INTERRUPTS(); } -ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { portDISABLE_INTERRUPTS(); } +ICACHE_RAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } +ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } #endif } // namespace esphome From 8a08d1fb5d5914a5e5fbc860b74b6efdc5952806 Mon Sep 17 00:00:00 2001 From: Niclas Larsson Date: Thu, 5 Dec 2019 00:27:49 +0100 Subject: [PATCH 119/412] Handle yaml merge keys correcly. (#888) * Handle yaml merge keys correcly. * Removed old debug bool. * Deleted after request from @OttoWinder. * Small refactoring. Removed unused variable `value` Small refactoring to make the code clearer. Added comments. * Fix merge sequence edge case --- esphome/yaml_util.py | 58 ++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d80334cedf..69f3c70ede 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -93,50 +93,66 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return super(ESPHomeLoader, self).construct_yaml_seq(node) def custom_flatten_mapping(self, node): - pre_merge = [] - post_merge = [] + merge = [] index = 0 while index < len(node.value): - if isinstance(node.value[index], yaml.ScalarNode): - index += 1 - continue - key_node, value_node = node.value[index] - if key_node.tag == u'tag:yaml.org,2002:merge': + if key_node.tag == 'tag:yaml.org,2002:merge': del node.value[index] - if isinstance(value_node, yaml.MappingNode): self.custom_flatten_mapping(value_node) - node.value = node.value[:index] + value_node.value + node.value[index:] + merge.extend(value_node.value) elif isinstance(value_node, yaml.SequenceNode): submerge = [] for subnode in value_node.value: if not isinstance(subnode, yaml.MappingNode): raise yaml.constructor.ConstructorError( "while constructing a mapping", node.start_mark, - "expected a mapping for merging, but found %{}".format(subnode.id), + "expected a mapping for merging, but found {}".format(subnode.id), subnode.start_mark) self.custom_flatten_mapping(subnode) submerge.append(subnode.value) - # submerge.reverse() - node.value = node.value[:index] + submerge + node.value[index:] - elif isinstance(value_node, yaml.ScalarNode): - node.value = node.value[:index] + [value_node] + node.value[index:] - # post_merge.append(value_node) + submerge.reverse() + for value in submerge: + merge.extend(value) else: raise yaml.constructor.ConstructorError( "while constructing a mapping", node.start_mark, "expected a mapping or list of mappings for merging, " "but found {}".format(value_node.id), value_node.start_mark) - elif key_node.tag == u'tag:yaml.org,2002:value': - key_node.tag = u'tag:yaml.org,2002:str' + elif key_node.tag == 'tag:yaml.org,2002:value': + key_node.tag = 'tag:yaml.org,2002:str' index += 1 else: index += 1 - if pre_merge: - node.value = pre_merge + node.value - if post_merge: - node.value = node.value + post_merge + if merge: + # https://yaml.org/type/merge.html + # Generate a set of keys that should override values in `merge`. + haystack = {key.value for (key, _) in node.value} + + # Construct a new merge set with values overridden by current mapping or earlier + # sequence entries removed + new_merge = [] + + for key, value in merge: + if key.value in haystack: + # key already in the current map or from an earlier merge sequence entry, + # do not override + # + # "... each of its key/value pairs is inserted into the current mapping, + # unless the key already exists in it." + # + # "If the value associated with the merge key is a sequence, then this sequence + # is expected to contain mapping nodes and each of these nodes is merged in + # turn according to its order in the sequence. Keys in mapping nodes earlier + # in the sequence override keys specified in later mapping nodes." + continue + new_merge.append((key, value)) + # Add key node to haystack, for sequence merge values. + haystack.add(key.value) + + # Merge + node.value = new_merge + node.value def custom_construct_pairs(self, node): pairs = [] From d280380c8d3abce04e7066733b1425c890a033ed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 6 Dec 2019 00:01:12 +1300 Subject: [PATCH 120/412] Allow loading esphome version from a fork (#907) --- docker/rootfs/etc/cont-init.d/30-esphome.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh index 086c5af19f..d9a80cde2e 100644 --- a/docker/rootfs/etc/cont-init.d/30-esphome.sh +++ b/docker/rootfs/etc/cont-init.d/30-esphome.sh @@ -8,7 +8,15 @@ declare esphome_version if bashio::config.has_value 'esphome_version'; then esphome_version=$(bashio::config 'esphome_version') - full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip" + if [[ $esphome_version == *":"* ]]; then + IFS=':' read -r -a array <<< "$esphome_version" + username=${array[0]} + ref=${array[1]} + else + username="esphome" + ref=$esphome_version + fi + full_url="https://github.com/${username}/esphome/archive/${ref}.zip" bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." pip3 install -U --no-cache-dir "${full_url}" \ || bashio::exit.nok "Failed installing esphome pinned version." From d09dff3ae31285e7afff255e679b0cf637dda5d6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 7 Dec 2019 13:43:51 +0100 Subject: [PATCH 121/412] Clean up YAML Mapping construction (#910) * Clean up YAML Mapping construction Fixes https://github.com/esphome/issues/issues/902 * Clean up DataBase * Update error messages --- esphome/config.py | 3 +- esphome/helpers.py | 10 +-- esphome/yaml_util.py | 180 ++++++++++++++++++++----------------------- 3 files changed, 89 insertions(+), 104 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index 53449c3e85..027c28bc5d 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -663,8 +663,7 @@ class InvalidYAMLError(EsphomeError): except UnicodeDecodeError: base = repr(base_exc) base = decode_text(base) - message = u"Invalid YAML syntax. Please see YAML syntax reference or use an " \ - u"online YAML syntax validator:\n\n{}".format(base) + message = u"Invalid YAML syntax:\n\n{}".format(base) super(InvalidYAMLError, self).__init__(message) self.base_exc = base_exc diff --git a/esphome/helpers.py b/esphome/helpers.py index e91b13a735..179452c353 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -266,11 +266,11 @@ def file_compare(path1, path2): # A dict of types that need to be converted to heaptypes before a class can be added # to the object _TYPE_OVERLOADS = { - int: type('int', (int,), dict()), - float: type('float', (float,), dict()), - str: type('str', (str,), dict()), - dict: type('dict', (str,), dict()), - list: type('list', (list,), dict()), + int: type('EInt', (int,), dict()), + float: type('EFloat', (float,), dict()), + str: type('EStr', (str,), dict()), + dict: type('EDict', (str,), dict()), + list: type('EList', (list,), dict()), } if IS_PY2: diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 69f3c70ede..0e5b4593e9 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -43,7 +43,11 @@ class ESPForceValue(object): def make_data_base(value): - return add_class_to_obj(value, ESPHomeDataBase) + try: + return add_class_to_obj(value, ESPHomeDataBase) + except TypeError: + # Adding class failed, ignore error + return value def _add_data_ref(fn): @@ -92,50 +96,82 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors def construct_yaml_seq(self, node): return super(ESPHomeLoader, self).construct_yaml_seq(node) - def custom_flatten_mapping(self, node): - merge = [] - index = 0 - while index < len(node.value): - key_node, value_node = node.value[index] - if key_node.tag == 'tag:yaml.org,2002:merge': - del node.value[index] - if isinstance(value_node, yaml.MappingNode): - self.custom_flatten_mapping(value_node) - merge.extend(value_node.value) - elif isinstance(value_node, yaml.SequenceNode): - submerge = [] - for subnode in value_node.value: - if not isinstance(subnode, yaml.MappingNode): - raise yaml.constructor.ConstructorError( - "while constructing a mapping", node.start_mark, - "expected a mapping for merging, but found {}".format(subnode.id), - subnode.start_mark) - self.custom_flatten_mapping(subnode) - submerge.append(subnode.value) - submerge.reverse() - for value in submerge: - merge.extend(value) - else: - raise yaml.constructor.ConstructorError( - "while constructing a mapping", node.start_mark, - "expected a mapping or list of mappings for merging, " - "but found {}".format(value_node.id), value_node.start_mark) - elif key_node.tag == 'tag:yaml.org,2002:value': - key_node.tag = 'tag:yaml.org,2002:str' - index += 1 - else: - index += 1 - if merge: - # https://yaml.org/type/merge.html - # Generate a set of keys that should override values in `merge`. - haystack = {key.value for (key, _) in node.value} + @_add_data_ref + def construct_yaml_map(self, node): + """Traverses the given mapping node and returns a list of constructed key-value pairs.""" + assert isinstance(node, yaml.MappingNode) + # A list of key-value pairs we find in the current mapping + pairs = [] + # A list of key-value pairs we find while resolving merges ('<<' key), will be + # added to pairs in a second pass + merge_pairs = [] + # A dict of seen keys so far, used to alert the user of duplicate keys and checking + # which keys to merge. + # Value of dict items is the start mark of the previous declaration. + seen_keys = {} + for key_node, value_node in node.value: + # merge key is '<<' + is_merge_key = key_node.tag == 'tag:yaml.org,2002:merge' + # key has no explicit tag set + is_default_tag = key_node.tag == 'tag:yaml.org,2002:value' + + if is_default_tag: + # Default tag for mapping keys is string + key_node.tag = 'tag:yaml.org,2002:str' + + if not is_merge_key: + # base case, this is a simple key-value pair + key = self.construct_object(key_node) + value = self.construct_object(value_node) + + # Check if key is hashable + try: + hash(key) + except TypeError: + raise yaml.constructor.ConstructorError( + 'Invalid key "{}" (not hashable)'.format(key), key_node.start_mark) + + # Check if it is a duplicate key + if key in seen_keys: + raise yaml.constructor.ConstructorError( + 'Duplicate key "{}"'.format(key), key_node.start_mark, + 'NOTE: Previous declaration here:', seen_keys[key], + ) + seen_keys[key] = key_node.start_mark + + # Add to pairs + pairs.append((key, value)) + continue + + # This is a merge key, resolve value and add to merge_pairs + value = self.construct_object(value_node) + if isinstance(value, dict): + # base case, copy directly to merge_pairs + # direct merge, like "<<: {some_key: some_value}" + merge_pairs.extend(value.items()) + elif isinstance(value, list): + # sequence merge, like "<<: [{some_key: some_value}, {other_key: some_value}]" + for item in value: + if not isinstance(item, dict): + raise yaml.constructor.ConstructorError( + "While constructing a mapping", node.start_mark, + "Expected a mapping for merging, but found {}".format(type(item)), + value_node.start_mark) + merge_pairs.extend(item.items()) + else: + raise yaml.constructor.ConstructorError( + "While constructing a mapping", node.start_mark, + "Expected a mapping or list of mappings for merging, " + "but found {}".format(type(value)), value_node.start_mark) + + if merge_pairs: + # We found some merge keys along the way, merge them into base pairs + # https://yaml.org/type/merge.html # Construct a new merge set with values overridden by current mapping or earlier # sequence entries removed - new_merge = [] - - for key, value in merge: - if key.value in haystack: + for key, value in merge_pairs: + if key in seen_keys: # key already in the current map or from an earlier merge sequence entry, # do not override # @@ -147,59 +183,11 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors # turn according to its order in the sequence. Keys in mapping nodes earlier # in the sequence override keys specified in later mapping nodes." continue - new_merge.append((key, value)) - # Add key node to haystack, for sequence merge values. - haystack.add(key.value) - - # Merge - node.value = new_merge + node.value - - def custom_construct_pairs(self, node): - pairs = [] - for kv in node.value: - if isinstance(kv, yaml.ScalarNode): - obj = self.construct_object(kv) - if not isinstance(obj, dict): - raise EsphomeError( - "Expected mapping for anchored include tag, got {}".format(type(obj))) - for key, value in obj.items(): - pairs.append((key, value)) - else: - key_node, value_node = kv - key = self.construct_object(key_node) - value = self.construct_object(value_node) pairs.append((key, value)) + # Add key node to seen keys, for sequence merge values. + seen_keys[key] = None - return pairs - - @_add_data_ref - def construct_yaml_map(self, node): - self.custom_flatten_mapping(node) - nodes = self.custom_construct_pairs(node) - - seen = {} - for (key, _), nv in zip(nodes, node.value): - if isinstance(nv, yaml.ScalarNode): - line = nv.start_mark.line - else: - line = nv[0].start_mark.line - - try: - hash(key) - except TypeError: - raise yaml.MarkedYAMLError( - context="invalid key: \"{}\"".format(key), - context_mark=yaml.Mark(self.name, 0, line, -1, None, None) - ) - - if key in seen: - raise yaml.MarkedYAMLError( - context="duplicate key: \"{}\"".format(key), - context_mark=yaml.Mark(self.name, 0, line, -1, None, None) - ) - seen[key] = line - - return OrderedDict(nodes) + return OrderedDict(pairs) @_add_data_ref def construct_env_var(self, node): @@ -210,8 +198,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors if args[0] in os.environ: return os.environ[args[0]] raise yaml.MarkedYAMLError( - context=u"Environment variable '{}' not defined".format(node.value), - context_mark=node.start_mark + u"Environment variable '{}' not defined".format(node.value), node.start_mark ) @property @@ -226,8 +213,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) if node.value not in secrets: raise yaml.MarkedYAMLError( - context=u"Secret '{}' not defined".format(node.value), - context_mark=node.start_mark + u"Secret '{}' not defined".format(node.value), node.start_mark ) val = secrets[node.value] _SECRET_VALUES[text_type(val)] = node.value From 74aca2137b22b3c9f650cf7e9595cada06c2f611 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Sun, 8 Dec 2019 03:15:04 +1100 Subject: [PATCH 122/412] Add duty cycle output component (#894) * Add duty cycle output component * cleanup + tests * format * duty_cycle -> slow_pwm * . * clang-format * ESP_LOGD -> ESPLOGVV Co-Authored-By: Otto Winter --- esphome/components/slow_pwm/__init__.py | 0 esphome/components/slow_pwm/output.py | 29 ++++++++++++++ .../components/slow_pwm/slow_pwm_output.cpp | 40 +++++++++++++++++++ esphome/components/slow_pwm/slow_pwm_output.h | 32 +++++++++++++++ esphome/const.py | 1 + tests/test1.yaml | 4 ++ 6 files changed, 106 insertions(+) create mode 100644 esphome/components/slow_pwm/__init__.py create mode 100644 esphome/components/slow_pwm/output.py create mode 100644 esphome/components/slow_pwm/slow_pwm_output.cpp create mode 100644 esphome/components/slow_pwm/slow_pwm_output.h diff --git a/esphome/components/slow_pwm/__init__.py b/esphome/components/slow_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py new file mode 100644 index 0000000000..f7b26a953a --- /dev/null +++ b/esphome/components/slow_pwm/output.py @@ -0,0 +1,29 @@ +from esphome import pins, core +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_PIN, CONF_PERIOD + +slow_pwm_ns = cg.esphome_ns.namespace("slow_pwm") +SlowPWMOutput = slow_pwm_ns.class_("SlowPWMOutput", output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_PERIOD): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=core.TimePeriod(milliseconds=100)), + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield output.register_output(var, config) + + pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + cg.add(var.set_period(config[CONF_PERIOD])) diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp new file mode 100644 index 0000000000..04a0d86bf7 --- /dev/null +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -0,0 +1,40 @@ +#include "slow_pwm_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace slow_pwm { + +static const char *TAG = "output.slow_pwm"; + +void SlowPWMOutput::setup() { + this->pin_->setup(); + this->turn_off(); +} + +void SlowPWMOutput::loop() { + unsigned long now = millis(); + float scaled_state = this->state_ * this->period_; + + if (now - this->period_start_time_ > this->period_) { + ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); + this->period_start_time_ += this->period_; + } + + if (scaled_state > now - this->period_start_time_) { + this->pin_->digital_write(true); + } else { + this->pin_->digital_write(false); + } +} + +void SlowPWMOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Slow PWM Output:"); + LOG_PIN(" Pin: ", this->pin_); + ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); + LOG_FLOAT_OUTPUT(this); +} + +void SlowPWMOutput::write_state(float state) { this->state_ = state; } + +} // namespace slow_pwm +} // namespace esphome diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h new file mode 100644 index 0000000000..4a2c1d0a14 --- /dev/null +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace slow_pwm { + +class SlowPWMOutput : public output::FloatOutput, public Component { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; }; + void set_period(unsigned int period) { period_ = period; }; + + /// Initialize pin + void setup() override; + void dump_config() override; + /// HARDWARE setup_priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + void loop() override; + + GPIOPin *pin_; + float state_{0}; + unsigned int period_start_time_{0}; + unsigned int period_{5000}; +}; + +} // namespace slow_pwm +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index e225862e70..0790a49552 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -317,6 +317,7 @@ CONF_PASSWORD = 'password' CONF_PAYLOAD = 'payload' CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' +CONF_PERIOD = 'period' CONF_PHASE_BALANCER = 'phase_balancer' CONF_PIN = 'pin' CONF_PIN_A = 'pin_a' diff --git a/tests/test1.yaml b/tests/test1.yaml index b7f3d04b40..1080339b67 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -995,6 +995,10 @@ output: - platform: my9231 id: my_5 channel: 5 + - platform: slow_pwm + id: id24 + pin: GPIO26 + period: 15s light: - platform: binary From b5714cd70fdf716f313bd2b578fa2ddeff9fc937 Mon Sep 17 00:00:00 2001 From: adamgreg Date: Sat, 7 Dec 2019 16:19:27 +0000 Subject: [PATCH 123/412] ESP32 GPIOs 33 to 38 can be used for deep sleep wakeup (#911) --- esphome/components/deep_sleep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 5babf422bd..07c1d117b6 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_C def validate_pin_number(value): - valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 39] + valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid(u"Only pins {} support wakeup" u"".format(', '.join(str(x) for x in valid_pins))) From 056c72d50d6274a528f736345691e9c51de67b63 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 7 Dec 2019 18:28:55 +0100 Subject: [PATCH 124/412] Drop Python 2 Support (#793) * Remove Python 2 support * Remove u-strings * Remove docker symlinks * Remove from travis * Update requirements * Upgrade flake8/pylint * Fixes * Manual * Run pyupgrade * Lint * Remove base_int * Fix * Update platformio_api.py * Update component.cpp --- .travis.yml | 6 - docker/Dockerfile.lint | 2 - esphome/__main__.py | 61 +++--- esphome/api/client.py | 45 ++-- esphome/automation.py | 4 +- esphome/components/ade7953/sensor.py | 2 +- esphome/components/ads1115/sensor.py | 7 +- esphome/components/api/__init__.py | 2 +- esphome/components/binary_sensor/__init__.py | 17 +- esphome/components/deep_sleep/__init__.py | 4 +- esphome/components/display/__init__.py | 3 +- .../components/esp32_ble_tracker/__init__.py | 12 +- .../components/esp32_touch/binary_sensor.py | 2 +- esphome/components/font/__init__.py | 20 +- esphome/components/globals/__init__.py | 3 +- esphome/components/hmc5883l/sensor.py | 6 +- esphome/components/http_request/__init__.py | 8 +- esphome/components/image/__init__.py | 3 +- esphome/components/ina219/sensor.py | 1 - esphome/components/ina226/sensor.py | 1 - esphome/components/ina3221/sensor.py | 1 - esphome/components/light/effects.py | 4 +- esphome/components/logger/__init__.py | 19 +- esphome/components/mpu6050/sensor.py | 8 +- esphome/components/mqtt/__init__.py | 10 +- esphome/components/neopixelbus/light.py | 2 +- esphome/components/ntc/sensor.py | 3 +- esphome/components/partition/light.py | 4 +- esphome/components/pmsx003/sensor.py | 2 +- esphome/components/qmc5883l/sensor.py | 6 +- esphome/components/remote_base/__init__.py | 25 ++- .../components/remote_transmitter/switch.py | 6 +- esphome/components/stepper/__init__.py | 4 +- esphome/components/substitutions/__init__.py | 19 +- esphome/components/sun/__init__.py | 3 +- esphome/components/tcs34725/sensor.py | 1 - esphome/components/time/__init__.py | 42 ++-- esphome/components/uart/__init__.py | 7 +- esphome/components/uart/switch/__init__.py | 5 +- esphome/components/wifi/__init__.py | 4 +- esphome/config.py | 172 +++++++-------- esphome/config_helpers.py | 7 +- esphome/config_validation.py | 202 ++++++++--------- esphome/const.py | 25 ++- esphome/core.py | 129 ++++++----- esphome/core/component.cpp | 4 + esphome/core_config.py | 12 +- esphome/cpp_generator.py | 203 +++++++++--------- esphome/cpp_helpers.py | 15 +- esphome/dashboard/dashboard.py | 49 ++--- esphome/espota2.py | 51 ++--- esphome/helpers.py | 54 ++--- esphome/legacy.py | 1 - esphome/mqtt.py | 46 ++-- esphome/pins.py | 30 ++- esphome/platformio_api.py | 11 +- esphome/py_compat.py | 89 -------- esphome/storage_json.py | 16 +- esphome/util.py | 59 ++--- esphome/voluptuous_schema.py | 18 +- esphome/vscode.py | 14 +- esphome/wizard.py | 73 +++---- esphome/writer.py | 76 ++++--- esphome/yaml_util.py | 76 +++---- esphome/zeroconf.py | 32 ++- pylintrc | 6 - requirements.txt | 1 - requirements_test.txt | 1 - script/api_protobuf/api_options_pb2.py | 1 - script/api_protobuf/api_protobuf.py | 8 +- script/build_compile_commands.py | 2 +- script/ci-custom.py | 17 +- script/clang-format | 2 +- script/clang-tidy | 2 +- script/helpers.py | 12 +- script/lint-python | 6 +- setup.cfg | 1 - setup.py | 5 +- 78 files changed, 815 insertions(+), 1097 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7f75a2cb9..ca0a3082db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,12 +21,6 @@ matrix: - esphome tests/test1.yaml compile - esphome tests/test2.yaml compile - esphome tests/test3.yaml compile - - python: "2.7" - env: TARGET=Test2.7 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - env: TARGET=Cpp-Lint dist: trusty sudo: required diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 5d8893bdbe..2d77502dc2 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -14,7 +14,5 @@ RUN \ COPY requirements_test.txt /requirements_test.txt RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt -RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python - VOLUME ["/esphome"] WORKDIR /esphome diff --git a/esphome/__main__.py b/esphome/__main__.py index ae2db3e35c..73723dfa00 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import argparse import functools import logging @@ -14,7 +12,6 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.py_compat import IS_PY2, safe_input, IS_PY3 from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files _LOGGER = logging.getLogger(__name__) @@ -42,12 +39,12 @@ def choose_prompt(options): if len(options) == 1: return options[0][1] - safe_print(u"Found multiple options, please choose one:") + safe_print("Found multiple options, please choose one:") for i, (desc, _) in enumerate(options): - safe_print(u" [{}] {}".format(i + 1, desc)) + safe_print(f" [{i+1}] {desc}") while True: - opt = safe_input('(number): ') + opt = input('(number): ') if opt in options: opt = options.index(opt) break @@ -57,20 +54,20 @@ def choose_prompt(options): raise ValueError break except ValueError: - safe_print(color('red', u"Invalid option: '{}'".format(opt))) + safe_print(color('red', f"Invalid option: '{opt}'")) return options[opt - 1][1] def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] for res, desc in get_serial_ports(): - options.append((u"{} ({})".format(res, desc), res)) + options.append((f"{res} ({desc})", res)) if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): - options.append((u"Over The Air ({})".format(CORE.address), CORE.address)) + options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == 'OTA': return CORE.address if show_mqtt and 'mqtt' in CORE.config: - options.append((u"MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) + options.append(("MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) if default == 'OTA': return 'MQTT' if default is not None: @@ -108,11 +105,7 @@ def run_miniterm(config, port): except serial.SerialException: _LOGGER.error("Serial port closed!") return - if IS_PY2: - line = raw.replace('\r', '').replace('\n', '') - else: - line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', - 'backslashreplace') + line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', 'backslashreplace') time = datetime.now().time().strftime('[%H:%M:%S]') message = time + line safe_print(message) @@ -127,11 +120,9 @@ def wrap_to_code(name, comp): @functools.wraps(comp.to_code) @coroutine_with_priority(coro.priority) def wrapped(conf): - cg.add(cg.LineComment(u"{}:".format(name))) + cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: conf_str = yaml_util.dump(conf) - if IS_PY2: - conf_str = conf_str.decode('utf-8') conf_str = conf_str.replace('//', '') cg.add(cg.LineComment(indent(conf_str))) yield coro(conf) @@ -243,7 +234,7 @@ def setup_log(debug=False, quiet=False): log_level = logging.INFO logging.basicConfig(level=log_level) fmt = "%(levelname)s %(message)s" - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + colorfmt = f"%(log_color)s{fmt}%(reset)s" datefmt = '%H:%M:%S' logging.getLogger('urllib3').setLevel(logging.WARNING) @@ -292,12 +283,12 @@ def command_compile(args, config): if exit_code != 0: return exit_code if args.only_generate: - _LOGGER.info(u"Successfully generated source code.") + _LOGGER.info("Successfully generated source code.") return 0 exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") return 0 @@ -307,7 +298,7 @@ def command_upload(args, config): exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") return 0 @@ -324,13 +315,13 @@ def command_run(args, config): exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") port = choose_upload_log_host(default=args.upload_port, check_default=None, show_ota=True, show_mqtt=False, show_api=True) exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") if args.no_logs: return 0 port = choose_upload_log_host(default=args.upload_port, check_default=port, @@ -349,7 +340,7 @@ def command_mqtt_fingerprint(args, config): def command_version(args): - safe_print(u"Version: {}".format(const.__version__)) + safe_print(f"Version: {const.__version__}") return 0 @@ -377,10 +368,10 @@ def command_update_all(args): twidth = 60 def print_bar(middle_text): - middle_text = " {} ".format(middle_text) + middle_text = f" {middle_text} " width = len(click.unstyle(middle_text)) half_line = "=" * ((twidth - width) // 2) - click.echo("%s%s%s" % (half_line, middle_text, half_line)) + click.echo(f"{half_line}{middle_text}{half_line}") for f in files: print("Updating {}".format(color('cyan', f))) @@ -431,7 +422,7 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): - parser = argparse.ArgumentParser(description='ESPHome v{}'.format(const.__version__)) + parser = argparse.ArgumentParser(description=f'ESPHome v{const.__version__}') parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", action='store_true') parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", @@ -525,14 +516,10 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 - if IS_PY2: - _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " - "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " - "or higher.") - elif IS_PY3 and sys.version_info < (3, 6, 0): - _LOGGER.warning("You're using ESPHome with python 3.5. Support for python 3.5 is " - "deprecated and will be removed in 1.15.0. Please reinstall ESPHome with " - "python 3.6 or higher.") + if sys.version_info < (3, 6, 0): + _LOGGER.error("You're running ESPHome with Python <3.6. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.6+") + return 1 if args.command in PRE_CONFIG_ACTIONS: try: @@ -551,7 +538,7 @@ def run_esphome(argv): CORE.config = config if args.command not in POST_CONFIG_ACTIONS: - safe_print(u"Unknown command {}".format(args.command)) + safe_print(f"Unknown command {args.command}") try: rc = POST_CONFIG_ACTIONS[args.command](args, config) diff --git a/esphome/api/client.py b/esphome/api/client.py index 0c52674287..fcea90e3b4 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -14,7 +14,6 @@ import esphome.api.api_pb2 as pb from esphome.const import CONF_PASSWORD, CONF_PORT from esphome.core import EsphomeError from esphome.helpers import resolve_ip_address, indent, color -from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -67,16 +66,16 @@ MESSAGE_TYPE_TO_PROTO = { def _varuint_to_bytes(value): if value <= 0x7F: - return byte_to_bytes(value) + return bytes([value]) ret = bytes() while value: temp = value & 0x7F value >>= 7 if value: - ret += byte_to_bytes(temp | 0x80) + ret += bytes([temp | 0x80]) else: - ret += byte_to_bytes(temp) + ret += bytes([temp]) return ret @@ -84,8 +83,7 @@ def _varuint_to_bytes(value): def _bytes_to_varuint(value): result = 0 bitpos = 0 - for c in value: - val = char_to_byte(c) + for val in value: result |= (val & 0x7F) << bitpos bitpos += 7 if (val & 0x80) == 0: @@ -191,8 +189,8 @@ class APIClient(threading.Thread): self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) try: self._socket.connect((ip, self._port)) - except socket.error as err: - err = APIConnectionError("Error connecting to {}: {}".format(ip, err)) + except OSError as err: + err = APIConnectionError(f"Error connecting to {ip}: {err}") self._fatal_error(err) raise err self._socket.settimeout(0.1) @@ -200,7 +198,7 @@ class APIClient(threading.Thread): self._socket_open_event.set() hello = pb.HelloRequest() - hello.client_info = 'ESPHome v{}'.format(const.__version__) + hello.client_info = f'ESPHome v{const.__version__}' try: resp = self._send_message_await_response(hello, pb.HelloResponse) except APIConnectionError as err: @@ -251,8 +249,8 @@ class APIClient(threading.Thread): with self._socket_write_lock: try: self._socket.sendall(data) - except socket.error as err: - err = APIConnectionError("Error while writing data: {}".format(err)) + except OSError as err: + err = APIConnectionError(f"Error while writing data: {err}") self._fatal_error(err) raise err @@ -265,11 +263,8 @@ class APIClient(threading.Thread): raise ValueError encoded = msg.SerializeToString() - _LOGGER.debug("Sending %s:\n%s", type(msg), indent(text_type(msg))) - if IS_PY2: - req = chr(0x00) - else: - req = bytes([0]) + _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) + req = bytes([0]) req += _varuint_to_bytes(len(encoded)) req += _varuint_to_bytes(message_type) req += encoded @@ -355,14 +350,14 @@ class APIClient(threading.Thread): raise APIConnectionError("Socket was closed") except socket.timeout: continue - except socket.error as err: - raise APIConnectionError("Error while receiving data: {}".format(err)) + except OSError as err: + raise APIConnectionError(f"Error while receiving data: {err}") ret += val return ret def _recv_varint(self): raw = bytes() - while not raw or char_to_byte(raw[-1]) & 0x80: + while not raw or raw[-1] & 0x80: raw += self._recv(1) return _bytes_to_varuint(raw) @@ -371,7 +366,7 @@ class APIClient(threading.Thread): return # Preamble - if char_to_byte(self._recv(1)[0]) != 0x00: + if self._recv(1)[0] != 0x00: raise APIConnectionError("Invalid preamble") length = self._recv_varint() @@ -436,7 +431,7 @@ def run_logs(config, address): return if err: - _LOGGER.warning(u"Disconnected from API: %s", err) + _LOGGER.warning("Disconnected from API: %s", err) while retry_timer: retry_timer.pop(0).cancel() @@ -454,18 +449,18 @@ def run_logs(config, address): wait_time = int(min(1.5**min(tries, 100), 30)) if not has_connects: - _LOGGER.warning(u"Initial connection failed. The ESP might not be connected " - u"to WiFi yet (%s). Re-Trying in %s seconds", + _LOGGER.warning("Initial connection failed. The ESP might not be connected " + "to WiFi yet (%s). Re-Trying in %s seconds", error, wait_time) else: - _LOGGER.warning(u"Couldn't connect to API (%s). Trying to reconnect in %s seconds", + _LOGGER.warning("Couldn't connect to API (%s). Trying to reconnect in %s seconds", error, wait_time) timer = threading.Timer(wait_time, functools.partial(try_connect, None, tries + 1)) timer.start() retry_timer.append(timer) def on_log(msg): - time_ = datetime.now().time().strftime(u'[%H:%M:%S]') + time_ = datetime.now().time().strftime('[%H:%M:%S]') text = msg.message if msg.send_failed: text = color('white', '(Message skipped because it was too big to fit in ' diff --git a/esphome/automation.py b/esphome/automation.py index f758191268..5df884e7c2 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -83,9 +83,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): try: return cv.Schema([schema])(value) except cv.Invalid as err2: - if u'extra keys not allowed' in str(err2) and len(err2.path) == 2: + if 'extra keys not allowed' in str(err2) and len(err2.path) == 2: raise err - if u'Unable to find action' in str(err): + if 'Unable to find action' in str(err): raise err2 raise cv.MultipleInvalid([err, err2]) elif isinstance(value, dict): diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 4fcd307332..b048b1ed71 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -36,4 +36,4 @@ def to_code(config): continue conf = config[key] sens = yield sensor.new_sensor(conf) - cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens)) + cg.add(getattr(var, f'set_{key}_sensor')(sens)) diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index 204ccb99d7..55619b22e9 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler from esphome.const import CONF_GAIN, CONF_MULTIPLEXER, ICON_FLASH, UNIT_VOLT, CONF_ID -from esphome.py_compat import string_types from . import ads1115_ns, ADS1115Component DEPENDENCIES = ['ads1115'] @@ -32,9 +31,9 @@ GAIN = { def validate_gain(value): if isinstance(value, float): - value = u'{:0.03f}'.format(value) - elif not isinstance(value, string_types): - raise cv.Invalid('invalid gain "{}"'.format(value)) + value = f'{value:0.03f}' + elif not isinstance(value, str): + raise cv.Invalid(f'invalid gain "{value}"') return cv.enum(GAIN)(value) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a0568863ad..eef60602ba 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -102,7 +102,7 @@ def homeassistant_service_to_code(config, action_id, template_arg, args): def validate_homeassistant_event(value): value = cv.string(value) - if not value.startswith(u'esphome.'): + if not value.startswith('esphome.'): raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with " "esphome. For example 'esphome.xyz'") return value diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c082e2e9af..7c78c3a369 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -9,7 +9,6 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID from esphome.core import CORE, coroutine, coroutine_with_priority -from esphome.py_compat import string_types from esphome.util import Registry DEVICE_CLASSES = [ @@ -94,7 +93,7 @@ MULTI_CLICK_TIMING_SCHEMA = cv.Schema({ def parse_multi_click_timing_str(value): - if not isinstance(value, string_types): + if not isinstance(value, str): return value parts = value.lower().split(' ') @@ -104,10 +103,10 @@ def parse_multi_click_timing_str(value): try: state = cv.boolean(parts[0]) except cv.Invalid: - raise cv.Invalid(u"First word must either be ON or OFF, not {}".format(parts[0])) + raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) if parts[1] != 'for': - raise cv.Invalid(u"Second word must be 'for', got {}".format(parts[1])) + raise cv.Invalid("Second word must be 'for', got {}".format(parts[1])) if parts[2] == 'at': if parts[3] == 'least': @@ -115,12 +114,12 @@ def parse_multi_click_timing_str(value): elif parts[3] == 'most': key = CONF_MAX_LENGTH else: - raise cv.Invalid(u"Third word after at must either be 'least' or 'most', got {}" - u"".format(parts[3])) + raise cv.Invalid("Third word after at must either be 'least' or 'most', got {}" + "".format(parts[3])) try: length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}") return { CONF_STATE: state, key: str(length) @@ -132,12 +131,12 @@ def parse_multi_click_timing_str(value): try: min_length = cv.positive_time_period_milliseconds(parts[2]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") try: max_length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") return { CONF_STATE: state, diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 07c1d117b6..1b766c9928 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -8,8 +8,8 @@ from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_C def validate_pin_number(value): valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] if value[CONF_NUMBER] not in valid_pins: - raise cv.Invalid(u"Only pins {} support wakeup" - u"".format(', '.join(str(x) for x in valid_pins))) + raise cv.Invalid("Only pins {} support wakeup" + "".format(', '.join(str(x) for x in valid_pins))) return value diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 38d19d832e..951d561caa 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation @@ -27,7 +26,7 @@ DISPLAY_ROTATIONS = { def validate_rotation(value): value = cv.string(value) - if value.endswith(u"°"): + if value.endswith("°"): value = value[:-1] return cv.enum(DISPLAY_ROTATIONS, int=True)(value) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index ccc15eb451..3311801b6c 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -46,33 +46,33 @@ def bt_uuid(value): pattern = re.compile("^[A-F|0-9]{4,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 16 bit UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'") return value if len(value) == len(bt_uuid32_format): pattern = re.compile("^[A-F|0-9]{8,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 32 bit UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'") return value if len(value) == len(bt_uuid128_format): pattern = re.compile( "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 128 UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'") return value raise cv.Invalid( - u"Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( + "Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( bt_uuid16_format, bt_uuid32_format, bt_uuid128_format)) def as_hex(value): - return cg.RawExpression('0x{}ULL'.format(value)) + return cg.RawExpression(f'0x{value}ULL') def as_hex_array(value): value = value.replace("-", "") - cpp_array = ['0x{}'.format(part) for part in [value[i:i+2] for i in range(0, len(value), 2)]] + cpp_array = [f'0x{part}' for part in [value[i:i+2] for i in range(0, len(value), 2)]] return cg.RawExpression( '(uint8_t*)(const uint8_t[16]){{{}}}'.format(','.join(reversed(cpp_array)))) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index a72ca5796f..5142879a04 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -27,7 +27,7 @@ TOUCH_PADS = { def validate_touch_pad(value): value = validate_gpio_pin(value) if value not in TOUCH_PADS: - raise cv.Invalid("Pin {} does not support touch pads.".format(value)) + raise cv.Invalid(f"Pin {value} does not support touch pads.") return value diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index e77bd5da8e..5b19dc74e0 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,11 +1,11 @@ -# coding=utf-8 +import functools + 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_SIZE from esphome.core import CORE, HexInt -from esphome.py_compat import sort_by_cmp DEPENDENCIES = ['display'] MULTI_CONF = True @@ -33,9 +33,9 @@ def validate_glyphs(value): return -1 if len(x_) > len(y_): return 1 - raise cv.Invalid(u"Found duplicate glyph {}".format(x)) + raise cv.Invalid(f"Found duplicate glyph {x}") - sort_by_cmp(value, comparator) + value.sort(key=functools.cmp_to_key(comparator)) return value @@ -55,15 +55,15 @@ def validate_pillow_installed(value): def validate_truetype_file(value): if value.endswith('.zip'): # for Google Fonts downloads - raise cv.Invalid(u"Please unzip the font archive '{}' first and then use the .ttf files " - u"inside.".format(value)) + raise cv.Invalid("Please unzip the font archive '{}' first and then use the .ttf files " + "inside.".format(value)) if not value.endswith('.ttf'): - raise cv.Invalid(u"Only truetype (.ttf) files are supported. Please make sure you're " - u"using the correct format or rename the extension to .ttf") + raise cv.Invalid("Only truetype (.ttf) files are supported. Please make sure you're " + "using the correct format or rename the extension to .ttf") return cv.file_(value) -DEFAULT_GLYPHS = u' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' +DEFAULT_GLYPHS = ' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' CONF_RAW_DATA_ID = 'raw_data_id' FONT_SCHEMA = cv.Schema({ @@ -84,7 +84,7 @@ def to_code(config): try: font = ImageFont.truetype(path, config[CONF_SIZE]) except Exception as e: - raise core.EsphomeError(u"Could not load truetype file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load truetype file {path}: {e}") ascent, descent = font.getmetrics() diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e7030978ed..d285f1e97f 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -4,7 +4,6 @@ from esphome import config_validation as cv, automation from esphome import codegen as cg from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE from esphome.core import coroutine_with_priority -from esphome.py_compat import IS_PY3 globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) @@ -36,7 +35,7 @@ def to_code(config): if config[CONF_RESTORE_VALUE]: value = config[CONF_ID].id - if IS_PY3 and isinstance(value, str): + if isinstance(value, str): value = value.encode() hash_ = int(hashlib.md5(value).hexdigest()[:8], 16) cg.add(glob.set_restore_value(hash_)) diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 6e2366cadc..b063284698 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -1,11 +1,9 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, CONF_UPDATE_INTERVAL) -from esphome.py_compat import text_type DEPENDENCIES = ['i2c'] @@ -54,7 +52,7 @@ def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: _units = units if isinstance(units, list) else [units] - _units = [text_type(x) for x in _units] + _units = [str(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) def validate_enum_bound(value): @@ -74,7 +72,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(HMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, cv.Optional(CONF_OVERSAMPLING, default='1x'): validate_enum(HMC5883LOversamplings, units="x"), - cv.Optional(CONF_RANGE, default=u'130µT'): validate_enum(HMC5883L_RANGES, units=["uT", u"µT"]), + cv.Optional(CONF_RANGE, default='130µT'): validate_enum(HMC5883L_RANGES, units=["uT", "µT"]), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 23fc38ba40..897440a454 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -1,3 +1,5 @@ +import urllib.parse as urlparse + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -5,12 +7,6 @@ from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_0 from esphome.core import CORE, Lambda from esphome.core_config import PLATFORMIO_ESP8266_LUT -from esphome.py_compat import IS_PY3 - -if IS_PY3: - import urllib.parse as urlparse # pylint: disable=no-name-in-module,import-error -else: - import urlparse # pylint: disable=import-error DEPENDENCIES = ['network'] AUTO_LOAD = ['json'] diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d933af1a93..d0a41a7379 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 import logging from esphome import core @@ -33,7 +32,7 @@ def to_code(config): try: image = Image.open(path) except Exception as e: - raise core.EsphomeError(u"Could not load image file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load image file {path}: {e}") if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index a6f415edb0..6a61e16226 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index 02a13f98c4..066363b3d4 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 199f7be624..1c26533cc4 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c2250e7e0c..005a66a5ad 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -253,8 +253,8 @@ def validate_effects(allowed_effects): name = x[key][CONF_NAME] if name in names: errors.append( - cv.Invalid(u"Found the effect name '{}' twice. All effects must have " - u"unique names".format(name), [i]) + cv.Invalid("Found the effect name '{}' twice. All effects must have " + "unique names".format(name), [i]) ) continue names.add(name) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 850f955f65..329c515aee 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -7,7 +7,6 @@ from esphome.automation import LambdaAction from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, \ CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.py_compat import text_type logger_ns = cg.esphome_ns.namespace('logger') LOG_LEVELS = { @@ -64,8 +63,8 @@ def validate_local_no_higher_than_global(value): global_level = value.get(CONF_LEVEL, 'DEBUG') for tag, level in value.get(CONF_LOGS, {}).items(): if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level): - raise EsphomeError(u"The local log level {} for {} must be less severe than the " - u"global log level {}.".format(level, tag, global_level)) + raise EsphomeError("The local log level {} for {} must be less severe than the " + "global log level {}.".format(level, tag, global_level)) return value @@ -119,7 +118,7 @@ def to_code(config): if CORE.is_esp8266 and has_serial_logging and is_at_least_verbose: debug_serial_port = HARDWARE_UART_TO_SERIAL[config.get(CONF_HARDWARE_UART)] - cg.add_build_flag("-DDEBUG_ESP_PORT={}".format(debug_serial_port)) + cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}") cg.add_build_flag("-DLWIP_DEBUG") DEBUG_COMPONENTS = { 'HTTP_CLIENT', @@ -134,7 +133,7 @@ def to_code(config): # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: - cg.add_build_flag("-DDEBUG_ESP_{}".format(comp)) + cg.add_build_flag(f"-DDEBUG_ESP_{comp}") if CORE.is_esp32 and is_at_least_verbose: cg.add_build_flag('-DCORE_DEBUG_LEVEL=5') if CORE.is_esp32 and is_at_least_very_verbose: @@ -165,7 +164,7 @@ def maybe_simple_message(schema): def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python # pylint: disable=anomalous-backslash-in-string - cfmt = u"""\ + cfmt = """\ ( # start of capture group 1 % # literal "%" (?: # first option @@ -179,8 +178,8 @@ def validate_printf(value): """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) if len(matches) != len(value[CONF_ARGS]): - raise cv.Invalid(u"Found {} printf-patterns ({}), but {} args were given!" - u"".format(len(matches), u', '.join(matches), len(value[CONF_ARGS]))) + raise cv.Invalid("Found {} printf-patterns ({}), but {} args were given!" + "".format(len(matches), ', '.join(matches), len(value[CONF_ARGS]))) return value @@ -196,9 +195,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(maybe_simple_message({ @automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] - args_ = [cg.RawExpression(text_type(x)) for x in config[CONF_ARGS]] + args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] - text = text_type(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) + text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void) yield cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index bf44c67848..73c78e7f16 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -39,14 +39,14 @@ def to_code(config): yield i2c.register_i2c_device(var, config) for d in ['x', 'y', 'z']: - accel_key = 'accel_{}'.format(d) + accel_key = f'accel_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_accel_{}_sensor'.format(d))(sens)) - accel_key = 'gyro_{}'.format(d) + cg.add(getattr(var, f'set_accel_{d}_sensor')(sens)) + accel_key = f'gyro_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_gyro_{}_sensor'.format(d))(sens)) + cg.add(getattr(var, f'set_gyro_{d}_sensor')(sens)) if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 20d7f5aafe..2f0ed0f8e2 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -64,28 +64,28 @@ def validate_config(value): topic_prefix = value[CONF_TOPIC_PREFIX] if CONF_BIRTH_MESSAGE not in value: out[CONF_BIRTH_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'online', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_WILL_MESSAGE not in value: out[CONF_WILL_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_SHUTDOWN_MESSAGE not in value: out[CONF_SHUTDOWN_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_LOG_TOPIC not in value: out[CONF_LOG_TOPIC] = { - CONF_TOPIC: '{}/debug'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/debug', CONF_QOS: 0, CONF_RETAIN: True, } @@ -95,7 +95,7 @@ def validate_config(value): def validate_fingerprint(value): value = cv.string(value) if re.match(r'^[0-9a-f]{40}$', value) is None: - raise cv.Invalid(u"fingerprint must be valid SHA1 hash") + raise cv.Invalid("fingerprint must be valid SHA1 hash") return value diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index e405b3d7a8..ea1b67f8ce 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -82,7 +82,7 @@ def validate_method_pin(value): for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): if opt in value and value[opt] not in pins_: raise cv.Invalid("Method {} only supports pin(s) {}".format( - method, ', '.join('GPIO{}'.format(x) for x in pins_) + method, ', '.join(f'GPIO{x}' for x in pins_) ), path=[CONF_METHOD]) return value diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index 7ff4f4e137..f2eb601ed2 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 from math import log import esphome.config_validation as cv @@ -28,7 +27,7 @@ def validate_calibration_parameter(value): value = cv.string(value) parts = value.split('->') if len(parts) != 2: - raise cv.Invalid(u"Calibration parameter must be of form 3000 -> 23°C") + raise cv.Invalid("Calibration parameter must be of form 3000 -> 23°C") voltage = cv.resistance(parts[0].strip()) temperature = cv.temperature(parts[1].strip()) return validate_calibration_parameter({ diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index 62434e7a19..ba1059e36b 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -10,8 +10,8 @@ PartitionLightOutput = partitions_ns.class_('PartitionLightOutput', light.Addres def validate_from_to(value): if value[CONF_FROM] > value[CONF_TO]: - raise cv.Invalid(u"From ({}) must not be larger than to ({})" - u"".format(value[CONF_FROM], value[CONF_TO])) + raise cv.Invalid("From ({}) must not be larger than to ({})" + "".format(value[CONF_FROM], value[CONF_TO])) return value diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 0cbaf1bf29..4dbe500d3d 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -36,7 +36,7 @@ SENSORS_TO_TYPE = { def validate_pmsx003_sensors(value): for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: - raise cv.Invalid(u"{} does not have {} sensor!".format(value[CONF_TYPE], key)) + raise cv.Invalid("{} does not have {} sensor!".format(value[CONF_TYPE], key)) return value diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index 8ccd542753..8a2952f54f 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,11 +1,9 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, CONF_UPDATE_INTERVAL) -from esphome.py_compat import text_type DEPENDENCIES = ['i2c'] @@ -46,7 +44,7 @@ def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: _units = units if isinstance(units, list) else [units] - _units = [text_type(x) for x in _units] + _units = [str(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) def validate_enum_bound(value): @@ -65,7 +63,7 @@ heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(QMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, - cv.Optional(CONF_RANGE, default=u'200µT'): validate_enum(QMC5883L_RANGES, units=["uT", u"µT"]), + cv.Optional(CONF_RANGE, default='200µT'): validate_enum(QMC5883L_RANGES, units=["uT", "µT"]), cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum(QMC5883LOversamplings, units="x"), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 023f0f253e..3d57041e56 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -7,7 +7,6 @@ from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY from esphome.core import coroutine -from esphome.py_compat import string_types, text_type from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ['binary_sensor'] @@ -52,7 +51,7 @@ def register_trigger(name, type, data_type): cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), }) - registerer = TRIGGER_REGISTRY.register('on_{}'.format(name), validator) + registerer = TRIGGER_REGISTRY.register(f'on_{name}', validator) def decorator(func): @coroutine @@ -98,7 +97,7 @@ def register_action(name, type_, schema): cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, }) - registerer = automation.register_action('remote_transmitter.transmit_{}'.format(name), + registerer = automation.register_action(f'remote_transmitter.transmit_{name}', type_, validator) def decorator(func): @@ -122,11 +121,11 @@ def register_action(name, type_, schema): def declare_protocol(name): - data = ns.struct('{}Data'.format(name)) - binary_sensor_ = ns.class_('{}BinarySensor'.format(name), RemoteReceiverBinarySensorBase) - trigger = ns.class_('{}Trigger'.format(name), RemoteReceiverTrigger) - action = ns.class_('{}Action'.format(name), RemoteTransmitterActionBase) - dumper = ns.class_('{}Dumper'.format(name), RemoteTransmitterDumper) + data = ns.struct(f'{name}Data') + binary_sensor_ = ns.class_(f'{name}BinarySensor', RemoteReceiverBinarySensorBase) + trigger = ns.class_(f'{name}Trigger', RemoteReceiverTrigger) + action = ns.class_(f'{name}Action', RemoteTransmitterActionBase) + dumper = ns.class_(f'{name}Dumper', RemoteTransmitterDumper) return data, binary_sensor_, trigger, action, dumper @@ -141,7 +140,7 @@ DUMPER_REGISTRY = Registry({ def validate_dumpers(value): - if isinstance(value, string_types) and value.lower() == 'all': + if isinstance(value, str) and value.lower() == 'all': return validate_dumpers(list(DUMPER_REGISTRY.keys())) return cv.validate_registry('dumper', DUMPER_REGISTRY)(value) @@ -432,12 +431,12 @@ RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( def validate_rc_switch_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch codes must be in quotes ('')") for c in value: if c not in ('0', '1'): - raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" - u"".format(c)) + raise cv.Invalid("Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" + "".format(c)) if len(value) > 64: raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}" "".format(value, len(value))) @@ -447,7 +446,7 @@ def validate_rc_switch_code(value): def validate_rc_switch_raw_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") for c in value: if c not in ('0', '1', 'x'): diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py index 5802ece807..5e0be04d7a 100644 --- a/esphome/components/remote_transmitter/switch.py +++ b/esphome/components/remote_transmitter/switch.py @@ -19,12 +19,12 @@ def show_new(value): if 'name' in value: args.append(('name', value['name'])) args.append(('turn_on_action', { - 'remote_transmitter.transmit_{}'.format(key): val + f'remote_transmitter.transmit_{key}': val })) text = yaml_util.dump([OrderedDict(args)]) - raise cv.Invalid(u"This platform has been removed in 1.13, please change to:\n\n{}\n\n." - u"".format(text)) + raise cv.Invalid("This platform has been removed in 1.13, please change to:\n\n{}\n\n." + "".format(text)) CONFIG_SCHEMA = show_new diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 087fb18137..e8d6acbd1c 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -28,7 +28,7 @@ def validate_acceleration(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected acceleration as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") if value <= 0: raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!") @@ -48,7 +48,7 @@ def validate_speed(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected speed as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected speed as floating point number, got {value}") if value <= 0: raise cv.Invalid("Speed must be larger than 0 steps/s!") diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 433a3066ee..292a7bf299 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,7 +3,6 @@ import re import esphome.config_validation as cv from esphome import core -from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) @@ -24,8 +23,8 @@ def validate_substitution_key(value): for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - u"Substitution must only consist of upper/lowercase characters, the underscore " - u"and numbers. The character '{}' cannot be used".format(char)) + "Substitution must only consist of upper/lowercase characters, the underscore " + "and numbers. The character '{}' cannot be used".format(char)) return value @@ -42,7 +41,7 @@ VARIABLE_PROG = re.compile('\\$([{0}]+|\\{{[{0}]*\\}})'.format(VALID_SUBSTITUTIO def _expand_substitutions(substitutions, value, path): - if u'$' not in value: + if '$' not in value: return value orig_value = value @@ -56,11 +55,11 @@ def _expand_substitutions(substitutions, value, path): i, j = m.span(0) name = m.group(1) - if name.startswith(u'{') and name.endswith(u'}'): + if name.startswith('{') and name.endswith('}'): name = name[1:-1] if name not in substitutions: - _LOGGER.warning(u"Found '%s' (see %s) which looks like a substitution, but '%s' was " - u"not declared", orig_value, u'->'.join(str(x) for x in path), name) + _LOGGER.warning("Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", orig_value, '->'.join(str(x) for x in path), name) i = j continue @@ -91,7 +90,7 @@ def _substitute_item(substitutions, item, path): for old, new in replace_keys: item[new] = item[old] del item[old] - elif isinstance(item, string_types): + elif isinstance(item, str): sub = _expand_substitutions(substitutions, item, path) if sub != item: return sub @@ -109,8 +108,8 @@ def do_substitution_pass(config): substitutions = config[CONF_SUBSTITUTIONS] with cv.prepend_path('substitutions'): if not isinstance(substitutions, dict): - raise cv.Invalid(u"Substitutions must be a key to value mapping, got {}" - u"".format(type(substitutions))) + raise cv.Invalid("Substitutions must be a key to value mapping, got {}" + "".format(type(substitutions))) replace_keys = [] for key, value in substitutions.items(): diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index fef0902181..e4d2023a8e 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID -from esphome.py_compat import string_types sun_ns = cg.esphome_ns.namespace('sun') @@ -32,7 +31,7 @@ ELEVATION_MAP = { def elevation(value): - if isinstance(value, string_types): + if isinstance(value, str): try: value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] except cv.Invalid: diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index 478fe4aba6..287b2e441c 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 58739772b0..6283392103 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -14,7 +14,6 @@ from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE from esphome.core import coroutine, coroutine_with_priority -from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) @@ -33,10 +32,10 @@ def _tz_timedelta(td): if offset_hour == 0 and offset_minute == 0 and offset_second == 0: return '0' if offset_minute == 0 and offset_second == 0: - return '{}'.format(offset_hour) + return f'{offset_hour}' if offset_second == 0: - return '{}:{}'.format(offset_hour, offset_minute) - return '{}:{}:{}'.format(offset_hour, offset_minute, offset_second) + return f'{offset_hour}:{offset_minute}' + return f'{offset_hour}:{offset_minute}:{offset_second}' # https://stackoverflow.com/a/16804556/8924614 @@ -133,7 +132,7 @@ def detect_tz(): def _parse_cron_int(value, special_mapping, message): special_mapping = special_mapping or {} - if isinstance(value, string_types) and value in special_mapping: + if isinstance(value, str) and value in special_mapping: return special_mapping[value] try: return int(value) @@ -143,41 +142,40 @@ def _parse_cron_int(value, special_mapping, message): def _parse_cron_part(part, min_value, max_value, special_mapping): if part in ('*', '?'): - return set(x for x in range(min_value, max_value + 1)) + return set(range(min_value, max_value + 1)) if '/' in part: data = part.split('/') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '/' in one time expression, got {}" + raise cv.Invalid("Can't have more than two '/' in one time expression, got {}" .format(part)) offset, repeat = data offset_n = 0 if offset: offset_n = _parse_cron_int(offset, special_mapping, - u"Offset for '/' time expression must be an integer, got {}") + "Offset for '/' time expression must be an integer, got {}") try: repeat_n = int(repeat) except ValueError: - raise cv.Invalid(u"Repeat for '/' time expression must be an integer, got {}" + raise cv.Invalid("Repeat for '/' time expression must be an integer, got {}" .format(repeat)) - return set(x for x in range(offset_n, max_value + 1, repeat_n)) + return set(range(offset_n, max_value + 1, repeat_n)) if '-' in part: data = part.split('-') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '-' in range time expression '{}'" + raise cv.Invalid("Can't have more than two '-' in range time expression '{}'" .format(part)) begin, end = data - begin_n = _parse_cron_int(begin, special_mapping, u"Number for time range must be integer, " - u"got {}") - end_n = _parse_cron_int(end, special_mapping, u"Number for time range must be integer, " - u"got {}") + begin_n = _parse_cron_int(begin, special_mapping, "Number for time range must be integer, " + "got {}") + end_n = _parse_cron_int(end, special_mapping, "Number for time range must be integer, " + "got {}") if end_n < begin_n: - return set(x for x in range(end_n, max_value + 1)) | \ - set(x for x in range(min_value, begin_n + 1)) - return set(x for x in range(begin_n, end_n + 1)) + return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1)) + return set(range(begin_n, end_n + 1)) - return {_parse_cron_int(part, special_mapping, u"Number for time expression must be an " - u"integer, got {}")} + return {_parse_cron_int(part, special_mapping, "Number for time expression must be an " + "integer, got {}")} def cron_expression_validator(name, min_value, max_value, special_mapping=None): @@ -249,7 +247,7 @@ def validate_cron_keys(value): if CONF_CRON in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when cron: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when cron: is specified.") if CONF_AT in value: raise cv.Invalid("Cannot use option at with cron!") cron_ = value[CONF_CRON] @@ -259,7 +257,7 @@ def validate_cron_keys(value): if CONF_AT in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when at: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when at: is specified.") at_ = value[CONF_AT] value = {x: value[x] for x in value if x != CONF_AT} value.update(at_) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 2511cf28b1..4312bd5d10 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import pins, automation from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA from esphome.core import CORE, coroutine -from esphome.py_compat import text_type, binary_type, char_to_byte uart_ns = cg.esphome_ns.namespace('uart') UARTComponent = uart_ns.class_('UARTComponent', cg.Component) @@ -13,7 +12,7 @@ MULTI_CONF = True def validate_raw_data(value): - if isinstance(value, text_type): + if isinstance(value, str): return value.encode('utf-8') if isinstance(value, str): return value @@ -77,8 +76,8 @@ def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) yield cg.register_parented(var, config[CONF_ID]) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [char_to_byte(x) for x in data] + if isinstance(data, bytes): + data = list(data) if cg.is_template(data): templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8)) diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index b6f622604f..6cc11d8bbe 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import switch, uart from esphome.const import CONF_DATA, CONF_ID, CONF_INVERTED from esphome.core import HexInt -from esphome.py_compat import binary_type, char_to_byte from .. import uart_ns, validate_raw_data DEPENDENCIES = ['uart'] @@ -25,6 +24,6 @@ def to_code(config): yield uart.register_uart_device(var, config) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [HexInt(char_to_byte(x)) for x in data] + if isinstance(data, bytes): + data = [HexInt(x) for x in data] cg.add(var.set_data(data)) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 37ea20c6e4..d3c7e51603 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -30,9 +30,9 @@ def validate_password(value): if not value: return value if len(value) < 8: - raise cv.Invalid(u"WPA password must be at least 8 characters long") + raise cv.Invalid("WPA password must be at least 8 characters long") if len(value) > 64: - raise cv.Invalid(u"WPA password must be at most 64 characters long") + raise cv.Invalid("WPA password must be at most 64 characters long") return value diff --git a/esphome/config.py b/esphome/config.py index 027c28bc5d..8d7c622a27 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import collections import importlib import logging @@ -18,7 +16,6 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2, decode_text, string_types from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa @@ -31,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_CACHE = {} -class ComponentManifest(object): +class ComponentManifest: def __init__(self, module, base_components_path, is_core=False, is_platform=False): self.module = module self._is_core = is_core @@ -89,7 +86,7 @@ class ComponentManifest(object): source_files = core.find_source_files(os.path.join(core_p, 'dummy')) ret = {} for f in source_files: - ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f) + ret[f'esphome/core/{f}'] = os.path.join(core_p, f) return ret source_files = core.find_source_files(self.module.__file__) @@ -101,7 +98,7 @@ class ComponentManifest(object): rel = os.path.relpath(full_file, self.base_components_path) # Always use / for C++ include names rel = rel.replace(os.sep, '/') - target_file = 'esphome/components/{}'.format(rel) + target_file = f'esphome/components/{rel}' ret[target_file] = full_file return ret @@ -119,12 +116,6 @@ def _mount_config_dir(): if not os.path.isdir(custom_path): CUSTOM_COMPONENTS_PATH = None return - init_path = os.path.join(custom_path, '__init__.py') - if IS_PY2 and not os.path.isfile(init_path): - _LOGGER.warning("Found 'custom_components' folder, but file __init__.py was not found. " - "ESPHome will automatically create it now....") - with open(init_path, 'w') as f: - f.write('\n') if CORE.config_dir not in sys.path: sys.path.insert(0, CORE.config_dir) CUSTOM_COMPONENTS_PATH = custom_path @@ -137,7 +128,7 @@ def _lookup_module(domain, is_platform): _mount_config_dir() # First look for custom_components try: - module = importlib.import_module('custom_components.{}'.format(domain)) + module = importlib.import_module(f'custom_components.{domain}') except ImportError as e: # ImportError when no such module if 'No module named' not in str(e): @@ -153,7 +144,7 @@ def _lookup_module(domain, is_platform): return manif try: - module = importlib.import_module('esphome.components.{}'.format(domain)) + module = importlib.import_module(f'esphome.components.{domain}') except ImportError as e: if 'No module named' not in str(e): _LOGGER.error("Unable to import component %s:", domain, exc_info=True) @@ -173,7 +164,7 @@ def get_component(domain): def get_platform(domain, platform): - full = '{}.{}'.format(platform, domain) + full = f'{platform}.{domain}' return _lookup_module(full, True) @@ -192,7 +183,7 @@ def iter_components(config): yield domain, component, conf if component.is_platform_component: for p_config in conf: - p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM]) + p_name = "{}.{}".format(domain, p_config[CONF_PLATFORM]) platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config @@ -208,13 +199,13 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool class Config(OrderedDict): def __init__(self): - super(Config, self).__init__() + super().__init__() # A list of voluptuous errors self.errors = [] # type: List[vol.Invalid] # A list of paths that should be fully outputted # The values will be the paths to all "domain", for example (['logger'], 'logger') # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') - self.output_paths = [] # type: List[Tuple[ConfigPath, unicode]] + self.output_paths = [] # type: List[Tuple[ConfigPath, str]] def add_error(self, error): # type: (vol.Invalid) -> None @@ -234,15 +225,15 @@ class Config(OrderedDict): self.add_error(e) def add_str_error(self, message, path): - # type: (basestring, ConfigPath) -> None + # type: (str, ConfigPath) -> None self.add_error(vol.Invalid(message, path)) def add_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.append((path, domain)) def remove_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.remove((path, domain)) def is_in_error_path(self, path): @@ -312,12 +303,10 @@ def iter_ids(config, path=None): yield id, path elif isinstance(config, list): for i, item in enumerate(config): - for result in iter_ids(item, path + [i]): - yield result + yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): - for result in iter_ids(value, path + [key]): - yield result + yield from iter_ids(value, path + [key]) def do_id_pass(result): # type: (Config) -> None @@ -332,8 +321,8 @@ def do_id_pass(result): # type: (Config) -> None # Look for duplicate definitions match = next((v for v in declare_ids if v[0].id == id.id), None) if match is not None: - opath = u'->'.join(text_type(v) for v in match[1]) - result.add_str_error(u"ID {} redefined! Check {}".format(id.id, opath), path) + opath = '->'.join(str(v) for v in match[1]) + result.add_str_error(f"ID {id.id} redefined! Check {opath}", path) continue declare_ids.append((id, path)) else: @@ -357,8 +346,8 @@ def do_id_pass(result): # type: (Config) -> None # Find candidates matches = difflib.get_close_matches(id.id, [v[0].id for v in declare_ids]) if matches: - matches_s = ', '.join('"{}"'.format(x) for x in matches) - error += " These IDs look similar: {}.".format(matches_s) + matches_s = ', '.join(f'"{x}"' for x in matches) + error += f" These IDs look similar: {matches_s}." result.add_str_error(error, path) continue if not isinstance(match.type, MockObjClass) or not isinstance(id.type, MockObjClass): @@ -377,7 +366,7 @@ def do_id_pass(result): # type: (Config) -> None id.id = v[0].id break else: - result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) + result.add_str_error(f"Couldn't resolve ID for type '{id.type}'", path) def recursive_check_replaceme(value): @@ -389,7 +378,7 @@ def recursive_check_replaceme(value): return cv.Schema({cv.valid: recursive_check_replaceme})(value) if isinstance(value, ESPForceValue): pass - if isinstance(value, string_types) and value == 'REPLACEME': + if isinstance(value, str) and value == 'REPLACEME': raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. " "Please make sure you have replaced all fields from the sample " "configuration.\n" @@ -455,8 +444,8 @@ def validate_config(config): while load_queue: domain, conf = load_queue.popleft() - domain = text_type(domain) - if domain.startswith(u'.'): + domain = str(domain) + if domain.startswith('.'): # Ignore top-level keys starting with a dot continue result.add_output_path([domain], domain) @@ -464,7 +453,7 @@ def validate_config(config): component = get_component(domain) path = [domain] if component is None: - result.add_str_error(u"Component not found: {}".format(domain), path) + result.add_str_error(f"Component not found: {domain}", path) continue CORE.loaded_integrations.add(domain) @@ -492,24 +481,24 @@ def validate_config(config): for i, p_config in enumerate(conf): path = [domain, i] # Construct temporary unknown output path - p_domain = u'{}.unknown'.format(domain) + p_domain = f'{domain}.unknown' result.add_output_path(path, p_domain) result[domain][i] = p_config if not isinstance(p_config, dict): - result.add_str_error(u"Platform schemas must be key-value pairs.", path) + result.add_str_error("Platform schemas must be key-value pairs.", path) continue p_name = p_config.get('platform') if p_name is None: - result.add_str_error(u"No platform specified! See 'platform' key.", path) + result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one result.remove_output_path(path, p_domain) - p_domain = u'{}.{}'.format(domain, p_name) + p_domain = f'{domain}.{p_name}' result.add_output_path(path, p_domain) # Try Load platform platform = get_platform(domain, p_name) if platform is None: - result.add_str_error(u"Platform not found: '{}'".format(p_domain), path) + result.add_str_error(f"Platform not found: '{p_domain}'", path) continue CORE.loaded_integrations.add(p_name) @@ -537,8 +526,8 @@ def validate_config(config): success = True for dependency in comp.dependencies: if dependency not in config: - result.add_str_error(u"Component {} requires component {}" - u"".format(domain, dependency), path) + result.add_str_error("Component {} requires component {}" + "".format(domain, dependency), path) success = False if not success: continue @@ -546,22 +535,22 @@ def validate_config(config): success = True for conflict in comp.conflicts_with: if conflict in config: - result.add_str_error(u"Component {} cannot be used together with component {}" - u"".format(domain, conflict), path) + result.add_str_error("Component {} cannot be used together with component {}" + "".format(domain, conflict), path) success = False if not success: continue if CORE.esp_platform not in comp.esp_platforms: - result.add_str_error(u"Component {} doesn't support {}.".format(domain, - CORE.esp_platform), + result.add_str_error("Component {} doesn't support {}.".format(domain, + CORE.esp_platform), path) continue if not comp.is_platform_component and comp.config_schema is None and \ not isinstance(conf, core.AutoLoad): - result.add_str_error(u"Component {} cannot be loaded via YAML " - u"(no CONFIG_SCHEMA).".format(domain), path) + result.add_str_error("Component {} cannot be loaded via YAML " + "(no CONFIG_SCHEMA).".format(domain), path) continue if comp.is_multi_conf: @@ -611,13 +600,13 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): - validation_error = text_type(validation_error) + validation_error = str(validation_error) m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() - if not validation_error.endswith(u'.'): - validation_error += u'.' + if not validation_error.endswith('.'): + validation_error += '.' return validation_error @@ -634,22 +623,22 @@ def _get_parent_name(path, config): def _format_vol_invalid(ex, config): - # type: (vol.Invalid, Config) -> unicode - message = u'' + # type: (vol.Invalid, Config) -> str + message = '' paren = _get_parent_name(ex.path[:-1], config) if isinstance(ex, ExtraKeysInvalid): if ex.candidates: - message += u'[{}] is an invalid option for [{}]. Did you mean {}?'.format( - ex.path[-1], paren, u', '.join(u'[{}]'.format(x) for x in ex.candidates)) + message += '[{}] is an invalid option for [{}]. Did you mean {}?'.format( + ex.path[-1], paren, ', '.join(f'[{x}]' for x in ex.candidates)) else: - message += u'[{}] is an invalid option for [{}]. Please check the indentation.'.format( + message += '[{}] is an invalid option for [{}]. Please check the indentation.'.format( ex.path[-1], paren) - elif u'extra keys not allowed' in text_type(ex): - message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) - elif u'required key not provided' in text_type(ex): - message += u"'{}' is a required option for [{}].".format(ex.path[-1], paren) + elif 'extra keys not allowed' in str(ex): + message += '[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) + elif 'required key not provided' in str(ex): + message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) else: message += humanize_error(config, ex) @@ -662,9 +651,8 @@ class InvalidYAMLError(EsphomeError): base = str(base_exc) except UnicodeDecodeError: base = repr(base_exc) - base = decode_text(base) - message = u"Invalid YAML syntax:\n\n{}".format(base) - super(InvalidYAMLError, self).__init__(message) + message = f"Invalid YAML syntax:\n\n{base}" + super().__init__(message) self.base_exc = base_exc @@ -680,7 +668,7 @@ def _load_config(): except EsphomeError: raise except Exception: - _LOGGER.error(u"Unexpected exception while reading configuration:") + _LOGGER.error("Unexpected exception while reading configuration:") raise return result @@ -690,7 +678,7 @@ def load_config(): try: return _load_config() except vol.Invalid as err: - raise EsphomeError("Error while parsing config: {}".format(err)) + raise EsphomeError(f"Error while parsing config: {err}") def line_info(obj, highlight=True): @@ -699,7 +687,7 @@ def line_info(obj, highlight=True): return None if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None: mark = obj.esp_range.start_mark - source = u"[source {}:{}]".format(mark.document, mark.line + 1) + source = "[source {}:{}]".format(mark.document, mark.line + 1) return color('cyan', source) return None @@ -715,82 +703,82 @@ def _print_on_next_line(obj): def dump_dict(config, path, at_root=True): - # type: (Config, ConfigPath, bool) -> Tuple[unicode, bool] + # type: (Config, ConfigPath, bool) -> Tuple[str, bool] conf = config.get_nested_item(path) - ret = u'' + ret = '' multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' if isinstance(conf, (list, tuple)): multiline = True if not conf: - ret += u'[]' + ret += '[]' multiline = False for i in range(len(conf)): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - sep = u'- ' + sep = '- ' if config.is_in_error_path(path_): sep = color('red', sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if inf is not None: - msg = inf + u'\n' + msg + msg = inf + '\n' + msg elif msg: msg = msg[2:] - ret += sep + msg + u'\n' + ret += sep + msg + '\n' elif isinstance(conf, dict): multiline = True if not conf: - ret += u'{}' + ret += '{}' multiline = False for k in conf.keys(): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - st = u'{}: '.format(k) + st = f'{k}: ' if config.is_in_error_path(path_): st = color('red', st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if m: - msg = u'\n' + indent(msg) + msg = '\n' + indent(msg) if inf is not None: if m: - msg = u' ' + inf + msg + msg = ' ' + inf + msg else: - msg = msg + u' ' + inf - ret += st + msg + u'\n' + msg = msg + ' ' + inf + ret += st + msg + '\n' elif isinstance(conf, str): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) if not conf: - conf += u"''" + conf += "''" if len(conf) > 80: - conf = u'|-\n' + indent(conf) + conf = '|-\n' + indent(conf) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) + ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) - conf = u'!lambda |-\n' + indent(text_type(conf.value)) + conf = '!lambda |-\n' + indent(str(conf.value)) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, conf) @@ -799,8 +787,8 @@ def dump_dict(config, path, at_root=True): else: error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) - multiline = u'\n' in ret + ret += color(col, str(conf)) + multiline = '\n' in ret return ret, multiline @@ -830,20 +818,20 @@ def read_config(): try: res = load_config() except EsphomeError as err: - _LOGGER.error(u"Error while reading config: %s", err) + _LOGGER.error("Error while reading config: %s", err) return None if res.errors: if not CORE.verbose: res = strip_default_ids(res) - safe_print(color('bold_red', u"Failed config")) + safe_print(color('bold_red', "Failed config")) safe_print('') for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - safe_print(color('bold_red', u'{}:'.format(domain)) + u' ' + - (line_info(res.get_nested_item(path)) or u'')) + safe_print(color('bold_red', f'{domain}:') + ' ' + + (line_info(res.get_nested_item(path)) or '')) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 0c508d2202..dcbcb70efe 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,22 +1,19 @@ -from __future__ import print_function - import json import os from esphome.core import CORE from esphome.helpers import read_file -from esphome.py_compat import safe_input def read_config_file(path): - # type: (basestring) -> unicode + # type: (str) -> str if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ 'type': 'read_file', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_response' return data['content'] diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 88fb55e841..55199e6647 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,6 +1,4 @@ -# coding=utf-8 """Helpers for config validation using voluptuous.""" -from __future__ import print_function import logging import os @@ -20,7 +18,6 @@ from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes from esphome.helpers import list_starts_with, add_class_to_obj -from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text from esphome.voluptuous_schema import _Schema _LOGGER = logging.getLogger(__name__) @@ -43,7 +40,7 @@ ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword @@ -82,7 +79,7 @@ class Optional(vol.Optional): """ def __init__(self, key, default=UNDEFINED): - super(Optional, self).__init__(key, default=default) + super().__init__(key, default=default) class Required(vol.Required): @@ -94,7 +91,7 @@ class Required(vol.Required): """ def __init__(self, key): - super(Required, self).__init__(key) + super().__init__(key) def check_not_templatable(value): @@ -105,7 +102,7 @@ def check_not_templatable(value): def alphanumeric(value): if value is None: raise Invalid("string value is None") - value = text_type(value) + value = str(value) if not value.isalnum(): raise Invalid("string value is not alphanumeric") return value @@ -115,8 +112,8 @@ def valid_name(value): value = string_strict(value) for c in value: if c not in ALLOWED_NAME_CHARS: - raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" - u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) + raise Invalid(f"'{c}' is an invalid character for names. Valid characters are: " + f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)") return value @@ -131,10 +128,10 @@ def string(value): raise Invalid("string value cannot be dictionary or list.") if isinstance(value, bool): raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") - if isinstance(value, text_type): + if isinstance(value, str): return value if value is not None: - return text_type(value) + return str(value) raise Invalid("string value is None") @@ -142,10 +139,8 @@ def string_strict(value): """Like string, but only allows strings, and does not automatically convert other types to strings.""" check_not_templatable(value) - if isinstance(value, text_type): + if isinstance(value, str): return value - if isinstance(value, string_types): - return text_type(value) raise Invalid("Must be string, got {}. did you forget putting quotes " "around the value?".format(type(value))) @@ -172,14 +167,14 @@ def boolean(value): check_not_templatable(value) if isinstance(value, bool): return value - if isinstance(value, string_types): + if isinstance(value, str): value = value.lower() if value in ('true', 'yes', 'on', 'enable'): return True if value in ('false', 'no', 'off', 'disable'): return False - raise Invalid(u"Expected boolean value, but cannot convert {} to a boolean. " - u"Please use 'true' or 'false'".format(value)) + raise Invalid("Expected boolean value, but cannot convert {} to a boolean. " + "Please use 'true' or 'false'".format(value)) def ensure_list(*validators): @@ -228,7 +223,7 @@ def int_(value): Automatically also converts strings to ints. """ check_not_templatable(value) - if isinstance(value, integer_types): + if isinstance(value, int): return value if isinstance(value, float): if int(value) == value: @@ -242,15 +237,15 @@ def int_(value): try: return int(value, base) except ValueError: - raise Invalid(u"Expected integer, but cannot parse {} as an integer".format(value)) + raise Invalid(f"Expected integer, but cannot parse {value} as an integer") def int_range(min=None, max=None, min_included=True, max_included=True): """Validate that the config option is an integer in the given range.""" if min is not None: - assert isinstance(min, integer_types) + assert isinstance(min, int) if max is not None: - assert isinstance(max, integer_types) + assert isinstance(max, int) return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included)) @@ -291,14 +286,14 @@ def validate_id_name(value): valid_chars = ascii_letters + digits + '_' for char in value: if char not in valid_chars: - raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" - u"character and numbers. The character '{}' cannot be used" - u"".format(char)) + raise Invalid("IDs must only consist of upper/lowercase characters, the underscore" + "character and numbers. The character '{}' cannot be used" + "".format(char)) if value in RESERVED_IDS: - raise Invalid(u"ID '{}' is reserved internally and cannot be used".format(value)) + raise Invalid(f"ID '{value}' is reserved internally and cannot be used") if value in CORE.loaded_integrations: - raise Invalid(u"ID '{}' conflicts with the name of an esphome integration, please use " - u"another ID name.".format(value)) + raise Invalid("ID '{}' conflicts with the name of an esphome integration, please use " + "another ID name.".format(value)) return value @@ -358,7 +353,7 @@ def only_on(platforms): def validator_(obj): if CORE.esp_platform not in platforms: - raise Invalid(u"This feature is only available on {}".format(platforms)) + raise Invalid(f"This feature is only available on {platforms}") return obj return validator_ @@ -463,7 +458,7 @@ def time_period_str_unit(value): "'{0}s'?".format(value)) if isinstance(value, TimePeriod): value = str(value) - if not isinstance(value, string_types): + if not isinstance(value, str): raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { @@ -485,8 +480,8 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise Invalid(u"Expected time period with unit, " - u"got {}".format(value)) + raise Invalid("Expected time period with unit, " + "got {}".format(value)) kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] return TimePeriod(**{kwarg: float(match.group(1))}) @@ -545,7 +540,7 @@ def time_of_day(value): try: date = datetime.strptime(value, '%H:%M:%S %p') except ValueError: - raise Invalid("Invalid time of day: {}".format(err)) + raise Invalid(f"Invalid time of day: {err}") return { CONF_HOUR: date.hour, @@ -577,7 +572,7 @@ def uuid(value): METRIC_SUFFIXES = { 'E': 1e18, 'P': 1e15, 'T': 1e12, 'G': 1e9, 'M': 1e6, 'k': 1e3, 'da': 10, 'd': 1e-1, - 'c': 1e-2, 'm': 0.001, u'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, + 'c': 1e-2, 'm': 0.001, 'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, '': 1 } @@ -594,11 +589,11 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): match = pattern.match(string(value)) if match is None: - raise Invalid(u"Expected {} with unit, got {}".format(quantity, value)) + raise Invalid(f"Expected {quantity} with unit, got {value}") mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid("Invalid {} suffix {}".format(quantity, match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -606,30 +601,17 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): return validator -frequency = float_with_unit("frequency", u"(Hz|HZ|hz)?") -resistance = float_with_unit("resistance", u"(Ω|Ω|ohm|Ohm|OHM)?") -current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") -voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?") -distance = float_with_unit("distance", u"(m)") -framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") -angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) -_temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") -_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") -_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") -decibel = float_with_unit("decibel", u"(dB|dBm|db|dbm)", optional_unit=True) - -if IS_PY2: - # Override voluptuous invalid to unicode for py2 - def _vol_invalid_unicode(self): - path = u' @ data[%s]' % u']['.join(map(repr, self.path)) \ - if self.path else u'' - # pylint: disable=no-member - output = decode_text(self.message) - if self.error_type: - output += u' for ' + self.error_type - return output + path - - Invalid.__unicode__ = _vol_invalid_unicode +frequency = float_with_unit("frequency", "(Hz|HZ|hz)?") +resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?") +current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") +voltage = float_with_unit("voltage", "(v|V|volt|Volts)?") +distance = float_with_unit("distance", "(m)") +framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)") +angle = float_with_unit("angle", "(°|deg)", optional_unit=True) +_temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") +_temperature_k = float_with_unit("temperature", "(° K|° K|K)?") +_temperature_f = float_with_unit("temperature", "(°F|° F|F)?") +decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) def temperature(value): @@ -672,15 +654,15 @@ def validate_bytes(value): match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value) if match is None: - raise Invalid(u"Expected number of bytes with unit, got {}".format(value)) + raise Invalid(f"Expected number of bytes with unit, got {value}") mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid metric suffix {}".format(match.group(2))) + raise Invalid("Invalid metric suffix {}".format(match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: - raise Invalid(u"Only suffixes with positive exponents are supported. " - u"Got {}".format(match.group(2))) + raise Invalid("Only suffixes with positive exponents are supported. " + "Got {}".format(match.group(2))) return int(mantissa * multiplier) @@ -701,7 +683,7 @@ def domain(value): try: return str(ipv4(value)) except Invalid: - raise Invalid("Invalid domain: {}".format(value)) + raise Invalid(f"Invalid domain: {value}") def domain_name(value): @@ -730,7 +712,7 @@ def ssid(value): def ipv4(value): if isinstance(value, list): parts = value - elif isinstance(value, string_types): + elif isinstance(value, str): parts = value.split('.') elif isinstance(value, IPAddress): return value @@ -806,7 +788,7 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) + raise Invalid(f"MQTT Quality of Service must be integer, got {value}") return one_of(0, 1, 2)(value) @@ -814,7 +796,7 @@ def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" def validator(value): if comp not in CORE.raw_config: - raise Invalid("This option requires component {}".format(comp)) + raise Invalid(f"This option requires component {comp}") return value return validator @@ -839,7 +821,7 @@ def percentage(value): def possibly_negative_percentage(value): - has_percent_sign = isinstance(value, string_types) and value.endswith('%') + has_percent_sign = isinstance(value, str) and value.endswith('%') if has_percent_sign: value = float(value[:-1].rstrip()) / 100.0 if value > 1: @@ -856,7 +838,7 @@ def possibly_negative_percentage(value): def percentage_int(value): - if isinstance(value, string_types) and value.endswith('%'): + if isinstance(value, str) and value.endswith('%'): value = int(value[:-1].rstrip()) return value @@ -916,7 +898,7 @@ def one_of(*values, **kwargs): - *float* (``bool``, default=False): Whether to convert the incoming values to floats. - *space* (``str``, default=' '): What to convert spaces in the input string to. """ - options = u', '.join(u"'{}'".format(x) for x in values) + options = ', '.join(f"'{x}'" for x in values) lower = kwargs.pop('lower', False) upper = kwargs.pop('upper', False) string_ = kwargs.pop('string', False) or lower or upper @@ -940,13 +922,13 @@ def one_of(*values, **kwargs): value = Upper(value) if value not in values: import difflib - options_ = [text_type(x) for x in values] - option = text_type(value) + options_ = [str(x) for x in values] + option = str(value) matches = difflib.get_close_matches(option, options_) if matches: - raise Invalid(u"Unknown value '{}', did you mean {}?" - u"".format(value, u", ".join(u"'{}'".format(x) for x in matches))) - raise Invalid(u"Unknown value '{}', valid options are {}.".format(value, options)) + raise Invalid("Unknown value '{}', did you mean {}?" + "".format(value, ", ".join(f"'{x}'" for x in matches))) + raise Invalid(f"Unknown value '{value}', valid options are {options}.") return value return validator @@ -996,7 +978,7 @@ def returning_lambda(value): Additionally, make sure the lambda returns something. """ value = lambda_(value) - if u'return' not in value.value: + if 'return' not in value.value: raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda " "is expected to return a value. \n" "Please make sure the lambda contains at least one " @@ -1007,24 +989,23 @@ def returning_lambda(value): def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + raise Invalid("Dimensions must have a length of two, not {}".format(len(value))) try: width, height = int(value[0]), int(value[1]) except ValueError: - raise Invalid(u"Width and height dimensions must be integers") + raise Invalid("Width and height dimensions must be integers") if width <= 0 or height <= 0: - raise Invalid(u"Width and height must at least be 1") + raise Invalid("Width and height must at least be 1") return [width, height] value = string(value) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: - raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + raise Invalid("Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") return dimensions([match.group(1), match.group(2)]) def directory(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1034,25 +1015,24 @@ def directory(value): 'type': 'check_directory_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'directory_exists_response' if data['content']: return value - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isdir(path): - raise Invalid(u"Path '{}' is not a directory (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a directory (full path: {})." + "".format(path, os.path.abspath(path))) return value def file_(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1062,19 +1042,19 @@ def file_(value): 'type': 'check_file_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_exists_response' if data['content']: return value - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isfile(path): - raise Invalid(u"Path '{}' is not a file (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a file (full path: {})." + "".format(path, os.path.abspath(path))) return value @@ -1092,7 +1072,7 @@ def entity_id(value): for x in value.split('.'): for c in x: if c not in ENTITY_ID_CHARACTERS: - raise Invalid("Invalid character for entity ID: {}".format(c)) + raise Invalid(f"Invalid character for entity ID: {c}") return value @@ -1103,9 +1083,9 @@ def extract_keys(schema): assert isinstance(schema, dict) keys = [] for skey in list(schema.keys()): - if isinstance(skey, string_types): + if isinstance(skey, str): keys.append(skey) - elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str): keys.append(skey.schema) else: raise ValueError() @@ -1136,14 +1116,14 @@ class GenerateID(Optional): """Mark this key as being an auto-generated ID key.""" def __init__(self, key=CONF_ID): - super(GenerateID, self).__init__(key, default=lambda: None) + super().__init__(key, default=lambda: None) class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): - super(SplitDefault, self).__init__(key) + super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_default = vol.default_factory(esp32) @@ -1165,7 +1145,7 @@ class OnlyWith(Optional): """Set the default value only if the given component is loaded.""" def __init__(self, key, component, default=None): - super(OnlyWith, self).__init__(key) + super().__init__(key) self._component = component self._default = vol.default_factory(default) @@ -1207,21 +1187,21 @@ def validate_registry_entry(name, registry): ignore_keys = extract_keys(base_schema) def validator(value): - if isinstance(value, string_types): + if isinstance(value, str): value = {value: {}} if not isinstance(value, dict): - raise Invalid(u"{} must consist of key-value mapping! Got {}" - u"".format(name.title(), value)) + raise Invalid("{} must consist of key-value mapping! Got {}" + "".format(name.title(), value)) key = next((x for x in value if x not in ignore_keys), None) if key is None: - raise Invalid(u"Key missing from {}! Got {}".format(name, value)) + raise Invalid(f"Key missing from {name}! Got {value}") if key not in registry: - raise Invalid(u"Unable to find {} with the name '{}'".format(name, key), [key]) + raise Invalid(f"Unable to find {name} with the name '{key}'", [key]) key2 = next((x for x in value if x != key and x not in ignore_keys), None) if key2 is not None: - raise Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " - u"Did you forget to indent the block inside the {0}?" - u"".format(name, key, key2)) + raise Invalid("Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " + "Did you forget to indent the block inside the {0}?" + "".format(name, key, key2)) if value[key] is None: value[key] = {} @@ -1296,7 +1276,7 @@ def polling_component_schema(default_update_interval): return COMPONENT_SCHEMA.extend({ Required(CONF_UPDATE_INTERVAL): default_update_interval, }) - assert isinstance(default_update_interval, string_types) + assert isinstance(default_update_interval, str) return COMPONENT_SCHEMA.extend({ Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval, }) diff --git a/esphome/const.py b/esphome/const.py index 0790a49552..8a45f27880 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,17 +1,16 @@ -# coding=utf-8 """Constants used by esphome.""" MAJOR_VERSION = 1 MINOR_VERSION = 15 PATCH_VERSION = '0-dev' -__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) -__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) +__short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' +__version__ = f'{__short_version__}.{PATCH_VERSION}' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' @@ -544,12 +543,12 @@ ICON_WEATHER_WINDY = 'mdi:weather-windy' ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' -UNIT_CELSIUS = u'°C' -UNIT_COUNTS_PER_CUBIC_METER = u'#/m³' +UNIT_CELSIUS = '°C' +UNIT_COUNTS_PER_CUBIC_METER = '#/m³' UNIT_DECIBEL = 'dB' UNIT_DECIBEL_MILLIWATT = 'dBm' -UNIT_DEGREE_PER_SECOND = u'°/s' -UNIT_DEGREES = u'°' +UNIT_DEGREE_PER_SECOND = '°/s' +UNIT_DEGREES = '°' UNIT_EMPTY = '' UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' @@ -559,12 +558,12 @@ UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' UNIT_LUX = 'lx' UNIT_METER = 'm' -UNIT_METER_PER_SECOND_SQUARED = u'm/s²' -UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_METER_PER_SECOND_SQUARED = 'm/s²' +UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³' UNIT_MICROMETER = 'µm' -UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' -UNIT_MICROTESLA = u'µT' -UNIT_OHM = u'Ω' +UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm' +UNIT_MICROTESLA = 'µT' +UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' diff --git a/esphome/core.py b/esphome/core.py index 9df30384bc..96447e560d 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -13,7 +13,6 @@ from typing import Any, Dict, List, Optional, Set # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio -from esphome.py_compat import IS_PY2, integer_types, text_type, string_types from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -23,53 +22,47 @@ class EsphomeError(Exception): """General ESPHome exception occurred.""" -if IS_PY2: - base_int = long -else: - base_int = int - - -class HexInt(base_int): +class HexInt(int): def __str__(self): if 0 <= self <= 255: - return "0x{:02X}".format(self) - return "0x{:X}".format(self) + return f"0x{self:02X}" + return f"0x{self:X}" -class IPAddress(object): +class IPAddress: def __init__(self, *args): if len(args) != 4: - raise ValueError(u"IPAddress must consist up 4 items") + raise ValueError("IPAddress must consist up 4 items") self.args = args def __str__(self): return '.'.join(str(x) for x in self.args) -class MACAddress(object): +class MACAddress: def __init__(self, *parts): if len(parts) != 6: - raise ValueError(u"MAC Address must consist of 6 items") + raise ValueError("MAC Address must consist of 6 items") self.parts = parts def __str__(self): - return ':'.join('{:02X}'.format(part) for part in self.parts) + return ':'.join(f'{part:02X}' for part in self.parts) @property def as_hex(self): from esphome.cpp_generator import RawExpression - num = ''.join('{:02X}'.format(part) for part in self.parts) - return RawExpression('0x{}ULL'.format(num)) + num = ''.join(f'{part:02X}' for part in self.parts) + return RawExpression(f'0x{num}ULL') def is_approximately_integer(value): - if isinstance(value, integer_types): + if isinstance(value, int): return True return abs(value - round(value)) < 0.001 -class TimePeriod(object): +class TimePeriod: def __init__(self, microseconds=None, milliseconds=None, seconds=None, minutes=None, hours=None, days=None): if days is not None: @@ -137,17 +130,17 @@ class TimePeriod(object): def __str__(self): if self.microseconds is not None: - return '{}us'.format(self.total_microseconds) + return f'{self.total_microseconds}us' if self.milliseconds is not None: - return '{}ms'.format(self.total_milliseconds) + return f'{self.total_milliseconds}ms' if self.seconds is not None: - return '{}s'.format(self.total_seconds) + return f'{self.total_seconds}s' if self.minutes is not None: - return '{}min'.format(self.total_minutes) + return f'{self.total_minutes}min' if self.hours is not None: - return '{}h'.format(self.total_hours) + return f'{self.total_hours}h' if self.days is not None: - return '{}d'.format(self.total_days) + return f'{self.total_days}d' return '0s' @property @@ -224,7 +217,7 @@ class TimePeriodMinutes(TimePeriod): LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)') -class Lambda(object): +class Lambda: def __init__(self, value): # pylint: disable=protected-access if isinstance(value, Lambda): @@ -260,10 +253,10 @@ class Lambda(object): return self.value def __repr__(self): - return u'Lambda<{}>'.format(self.value) + return f'Lambda<{self.value}>' -class ID(object): +class ID: def __init__(self, id, is_declaration=False, type=None, is_manual=None): self.id = id if is_manual is None: @@ -289,7 +282,7 @@ class ID(object): return self.id def __repr__(self): - return u'ID<{} declaration={}, type={}, manual={}>'.format( + return 'ID<{} declaration={}, type={}, manual={}>'.format( self.id, self.is_declaration, self.type, self.is_manual) def __eq__(self, other): @@ -305,10 +298,10 @@ class ID(object): is_manual=self.is_manual) -class DocumentLocation(object): +class DocumentLocation: def __init__(self, document, line, column): - # type: (basestring, int, int) -> None - self.document = document # type: basestring + # type: (str, int, int) -> None + self.document = document # type: str self.line = line # type: int self.column = column # type: int @@ -321,10 +314,10 @@ class DocumentLocation(object): ) def __str__(self): - return u'{} {}:{}'.format(self.document, self.line, self.column) + return f'{self.document} {self.line}:{self.column}' -class DocumentRange(object): +class DocumentRange: def __init__(self, start_mark, end_mark): # type: (DocumentLocation, DocumentLocation) -> None self.start_mark = start_mark # type: DocumentLocation @@ -338,10 +331,10 @@ class DocumentRange(object): ) def __str__(self): - return u'[{} - {}]'.format(self.start_mark, self.end_mark) + return f'[{self.start_mark} - {self.end_mark}]' -class Define(object): +class Define: def __init__(self, name, value=None): self.name = name self.value = value @@ -349,14 +342,14 @@ class Define(object): @property def as_build_flag(self): if self.value is None: - return u'-D{}'.format(self.name) - return u'-D{}={}'.format(self.name, self.value) + return f'-D{self.name}' + return f'-D{self.name}={self.value}' @property def as_macro(self): if self.value is None: - return u'#define {}'.format(self.name) - return u'#define {} {}'.format(self.name, self.value) + return f'#define {self.name}' + return f'#define {self.name} {self.value}' @property def as_tuple(self): @@ -369,7 +362,7 @@ class Define(object): return isinstance(self, type(other)) and self.as_tuple == other.as_tuple -class Library(object): +class Library: def __init__(self, name, version): self.name = name self.version = version @@ -378,7 +371,7 @@ class Library(object): def as_lib_dep(self): if self.version is None: return self.name - return u'{}@{}'.format(self.name, self.version) + return f'{self.name}@{self.version}' @property def as_tuple(self): @@ -461,7 +454,7 @@ def find_source_files(file): # pylint: disable=too-many-instance-attributes,too-many-public-methods -class EsphomeCore(object): +class EsphomeCore: def __init__(self): # True if command is run from dashboard self.dashboard = False @@ -499,7 +492,7 @@ class EsphomeCore(object): # A set of build flags to set in the platformio project self.build_flags = set() # type: Set[str] # A set of defines to set for the compile process in esphome/core/defines.h - self.defines = set() # type: Set[Define] + self.defines = set() # type: Set['Define'] # A dictionary of started coroutines, used to warn when a coroutine was not # awaited. self.active_coroutines = {} # type: Dict[int, Any] @@ -634,15 +627,15 @@ class EsphomeCore(object): # Print not-awaited coroutines for obj in self.active_coroutines.values(): - _LOGGER.warning(u"Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) - _LOGGER.warning(u"Please file a bug report with your configuration.") + _LOGGER.warning("Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) + _LOGGER.warning("Please file a bug report with your configuration.") if self.active_coroutines: raise EsphomeError() if self.component_ids: - comps = u', '.join(u"'{}'".format(x) for x in self.component_ids) - _LOGGER.warning(u"Components %s were never registered. Please create a bug report", + comps = ', '.join(f"'{x}'" for x in self.component_ids) + _LOGGER.warning("Components %s were never registered. Please create a bug report", comps) - _LOGGER.warning(u"with your configuration.") + _LOGGER.warning("with your configuration.") raise EsphomeError() self.active_coroutines.clear() @@ -652,8 +645,8 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.main_statements.append(expression) _LOGGER.debug("Adding: %s", expression) @@ -665,16 +658,16 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.global_statements.append(expression) _LOGGER.debug("Adding global: %s", expression) return expression def add_library(self, library): if not isinstance(library, Library): - raise ValueError(u"Library {} must be instance of Library, not {}" - u"".format(library, type(library))) + raise ValueError("Library {} must be instance of Library, not {}" + "".format(library, type(library))) _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: if other.name != library.name: @@ -689,9 +682,9 @@ class EsphomeCore(object): if other.version == library.version: break - raise ValueError(u"Version pinning failed! Libraries {} and {} " - u"requested with conflicting versions!" - u"".format(library, other)) + raise ValueError("Version pinning failed! Libraries {} and {} " + "requested with conflicting versions!" + "".format(library, other)) else: self.libraries.append(library) return library @@ -702,20 +695,20 @@ class EsphomeCore(object): return build_flag def add_define(self, define): - if isinstance(define, string_types): + if isinstance(define, str): define = Define(define) elif isinstance(define, Define): pass else: - raise ValueError(u"Define {} must be string or Define, not {}" - u"".format(define, type(define))) + raise ValueError("Define {} must be string or Define, not {}" + "".format(define, type(define))) self.defines.add(define) _LOGGER.debug("Adding define: %s", define) return define def get_variable(self, id): if not isinstance(id, ID): - raise ValueError("ID {!r} must be of type ID!".format(id)) + raise ValueError(f"ID {id!r} must be of type ID!") while True: if id in self.variables: yield self.variables[id] @@ -735,7 +728,7 @@ class EsphomeCore(object): def register_variable(self, id, obj): if id in self.variables: - raise EsphomeError("ID {} is already registered".format(id)) + raise EsphomeError(f"ID {id} is already registered") _LOGGER.debug("Registered variable %s of type %s", id.id, id.type) self.variables[id] = obj @@ -748,10 +741,10 @@ class EsphomeCore(object): main_code = [] for exp in self.main_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() main_code.append(text) - return u'\n'.join(main_code) + u'\n\n' + return '\n'.join(main_code) + '\n\n' @property def cpp_global_section(self): @@ -759,17 +752,17 @@ class EsphomeCore(object): global_code = [] for exp in self.global_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() global_code.append(text) - return u'\n'.join(global_code) + u'\n' + return '\n'.join(global_code) + '\n' class AutoLoad(OrderedDict): pass -class EnumValue(object): +class EnumValue: """Special type used by ESPHome to mark enum values for cv.enum.""" @property def enum_value(self): diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 26662b0061..6297013247 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -138,6 +138,9 @@ float Component::get_actual_setup_priority() const { return this->setup_priority_override_; } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpmf-conversions" bool Component::has_overridden_loop() const { #ifdef CLANG_TIDY bool loop_overridden = true; @@ -148,6 +151,7 @@ bool Component::has_overridden_loop() const { #endif return loop_overridden || call_loop_overridden; } +#pragma GCC diagnostic pop PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} diff --git a/esphome/core_config.py b/esphome/core_config.py index 63092891d3..5cfff6c4d9 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -37,8 +37,8 @@ def validate_board(value): raise NotImplementedError if value not in board_pins: - raise cv.Invalid(u"Could not find board '{}'. Valid boards are {}".format( - value, u', '.join(sorted(board_pins.keys())))) + raise cv.Invalid("Could not find board '{}'. Valid boards are {}".format( + value, ', '.join(sorted(board_pins.keys())))) return value @@ -108,8 +108,8 @@ def valid_include(value): value = cv.file_(value) _, ext = os.path.splitext(value) if ext not in VALID_INCLUDE_EXTS: - raise cv.Invalid(u"Include has invalid file extension {} - valid extensions are {}" - u"".format(ext, ', '.join(VALID_INCLUDE_EXTS))) + raise cv.Invalid("Include has invalid file extension {} - valid extensions are {}" + "".format(ext, ', '.join(VALID_INCLUDE_EXTS))) return value @@ -184,7 +184,7 @@ def include_file(path, basename): _, ext = os.path.splitext(path) if ext in ['.h', '.hpp', '.tcc']: # Header, add include statement - cg.add_global(cg.RawStatement(u'#include "{}"'.format(basename))) + cg.add_global(cg.RawStatement(f'#include "{basename}"')) @coroutine_with_priority(-1000.0) @@ -238,7 +238,7 @@ def to_code(config): ld_script = ld_scripts[1] if ld_script is not None: - cg.add_build_flag('-Wl,-T{}'.format(ld_script)) + cg.add_build_flag(f'-Wl,-T{ld_script}') cg.add_build_flag('-fno-exceptions') diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 09b542b3cc..b5239e9413 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -10,22 +10,21 @@ from esphome.core import ( # noqa TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define, EnumValue) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last -from esphome.py_compat import integer_types, string_types, text_type from esphome.util import OrderedDict -class Expression(object): +class Expression: def __str__(self): raise NotImplementedError -SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod, +SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod, Type[bool], Type[int], Type[float], List[Any]] class RawExpression(Expression): - def __init__(self, text): # type: (Union[str, unicode]) -> None - super(RawExpression, self).__init__() + def __init__(self, text): # type: (Union[str, str]) -> None + super().__init__() self.text = text def __str__(self): @@ -35,7 +34,7 @@ class RawExpression(Expression): # pylint: disable=redefined-builtin class AssignmentExpression(Expression): def __init__(self, type, modifier, name, rhs, obj): - super(AssignmentExpression, self).__init__() + super().__init__() self.type = type self.modifier = modifier self.name = name @@ -44,24 +43,24 @@ class AssignmentExpression(Expression): def __str__(self): if self.type is None: - return u"{} = {}".format(self.name, self.rhs) - return u"{} {}{} = {}".format(self.type, self.modifier, self.name, self.rhs) + return f"{self.name} = {self.rhs}" + return f"{self.type} {self.modifier}{self.name} = {self.rhs}" class VariableDeclarationExpression(Expression): def __init__(self, type, modifier, name): - super(VariableDeclarationExpression, self).__init__() + super().__init__() self.type = type self.modifier = modifier self.name = name def __str__(self): - return u"{} {}{}".format(self.type, self.modifier, self.name) + return f"{self.type} {self.modifier}{self.name}" class ExpressionList(Expression): def __init__(self, *args): - super(ExpressionList, self).__init__() + super().__init__() # Remove every None on end args = list(args) while args and args[-1] is None: @@ -69,7 +68,7 @@ class ExpressionList(Expression): self.args = [safe_exp(arg) for arg in args] def __str__(self): - text = u", ".join(text_type(x) for x in self.args) + text = ", ".join(str(x) for x in self.args) return indent_all_but_first_and_last(text) def __iter__(self): @@ -78,11 +77,11 @@ class ExpressionList(Expression): class TemplateArguments(Expression): def __init__(self, *args): # type: (*SafeExpType) -> None - super(TemplateArguments, self).__init__() + super().__init__() self.args = ExpressionList(*args) def __str__(self): - return u'<{}>'.format(self.args) + return f'<{self.args}>' def __iter__(self): return iter(self.args) @@ -90,7 +89,7 @@ class TemplateArguments(Expression): class CallExpression(Expression): def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None - super(CallExpression, self).__init__() + super().__init__() self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] @@ -101,13 +100,13 @@ class CallExpression(Expression): def __str__(self): if self.template_args is not None: - return u'{}{}({})'.format(self.base, self.template_args, self.args) - return u'{}({})'.format(self.base, self.args) + return f'{self.base}{self.template_args}({self.args})' + return f'{self.base}({self.args})' class StructInitializer(Expression): def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None - super(StructInitializer, self).__init__() + super().__init__() self.base = base if not isinstance(args, OrderedDict): args = OrderedDict(args) @@ -119,16 +118,16 @@ class StructInitializer(Expression): self.args[key] = exp def __str__(self): - cpp = u'{}{{\n'.format(self.base) + cpp = f'{self.base}{{\n' for key, value in self.args.items(): - cpp += u' .{} = {},\n'.format(key, value) - cpp += u'}' + cpp += f' .{key} = {value},\n' + cpp += '}' return cpp class ArrayInitializer(Expression): def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - super(ArrayInitializer, self).__init__() + super().__init__() self.multiline = kwargs.get('multiline', False) self.args = [] for arg in args: @@ -139,30 +138,30 @@ class ArrayInitializer(Expression): def __str__(self): if not self.args: - return u'{}' + return '{}' if self.multiline: - cpp = u'{\n' + cpp = '{\n' for arg in self.args: - cpp += u' {},\n'.format(arg) - cpp += u'}' + cpp += f' {arg},\n' + cpp += '}' else: - cpp = u'{' + u', '.join(str(arg) for arg in self.args) + u'}' + cpp = '{' + ', '.join(str(arg) for arg in self.args) + '}' return cpp class ParameterExpression(Expression): def __init__(self, type, id): - super(ParameterExpression, self).__init__() + super().__init__() self.type = safe_exp(type) self.id = id def __str__(self): - return u"{} {}".format(self.type, self.id) + return f"{self.type} {self.id}" class ParameterListExpression(Expression): def __init__(self, *parameters): - super(ParameterListExpression, self).__init__() + super().__init__() self.parameters = [] for parameter in parameters: if not isinstance(parameter, ParameterExpression): @@ -170,12 +169,12 @@ class ParameterListExpression(Expression): self.parameters.append(parameter) def __str__(self): - return u", ".join(text_type(x) for x in self.parameters) + return ", ".join(str(x) for x in self.parameters) class LambdaExpression(Expression): def __init__(self, parts, parameters, capture='=', return_type=None): - super(LambdaExpression, self).__init__() + super().__init__() self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -184,15 +183,15 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): - cpp = u'[{}]({})'.format(self.capture, self.parameters) + cpp = f'[{self.capture}]({self.parameters})' if self.return_type is not None: - cpp += u' -> {}'.format(self.return_type) - cpp += u' {{\n{}\n}}'.format(self.content) + cpp += f' -> {self.return_type}' + cpp += f' {{\n{self.content}\n}}' return indent_all_but_first_and_last(cpp) @property def content(self): - return u''.join(text_type(part) for part in self.parts) + return ''.join(str(part) for part in self.parts) class Literal(Expression): @@ -201,41 +200,41 @@ class Literal(Expression): class StringLiteral(Literal): - def __init__(self, string): # type: (Union[str, unicode]) -> None - super(StringLiteral, self).__init__() + def __init__(self, string): # type: (Union[str, str]) -> None + super().__init__() self.string = string def __str__(self): - return u'{}'.format(cpp_string_escape(self.string)) + return '{}'.format(cpp_string_escape(self.string)) class IntLiteral(Literal): - def __init__(self, i): # type: (Union[int, long]) -> None - super(IntLiteral, self).__init__() + def __init__(self, i): # type: (Union[int]) -> None + super().__init__() self.i = i def __str__(self): if self.i > 4294967295: - return u'{}ULL'.format(self.i) + return f'{self.i}ULL' if self.i > 2147483647: - return u'{}UL'.format(self.i) + return f'{self.i}UL' if self.i < -2147483648: - return u'{}LL'.format(self.i) - return text_type(self.i) + return f'{self.i}LL' + return str(self.i) class BoolLiteral(Literal): def __init__(self, binary): # type: (bool) -> None - super(BoolLiteral, self).__init__() + super().__init__() self.binary = binary def __str__(self): - return u"true" if self.binary else u"false" + return "true" if self.binary else "false" class HexIntLiteral(Literal): def __init__(self, i): # type: (int) -> None - super(HexIntLiteral, self).__init__() + super().__init__() self.i = HexInt(i) def __str__(self): @@ -244,18 +243,18 @@ class HexIntLiteral(Literal): class FloatLiteral(Literal): def __init__(self, value): # type: (float) -> None - super(FloatLiteral, self).__init__() + super().__init__() self.float_ = value def __str__(self): if math.isnan(self.float_): - return u"NAN" - return u"{}f".format(self.float_) + return "NAN" + return f"{self.float_}f" # pylint: disable=bad-continuation def safe_exp( - obj # type: Union[Expression, bool, str, unicode, int, long, float, TimePeriod, list] + obj # type: Union[Expression, bool, str, int, float, TimePeriod, list] ): # type: (...) -> Expression """Try to convert obj to an expression by automatically converting native python types to @@ -269,11 +268,11 @@ def safe_exp( return safe_exp(obj.enum_value) if isinstance(obj, bool): return BoolLiteral(obj) - if isinstance(obj, string_types): + if isinstance(obj, str): return StringLiteral(obj) if isinstance(obj, HexInt): return HexIntLiteral(obj) - if isinstance(obj, integer_types): + if isinstance(obj, int): return IntLiteral(obj) if isinstance(obj, float): return FloatLiteral(obj) @@ -294,15 +293,15 @@ def safe_exp( if obj is float: return float_ if isinstance(obj, ID): - raise ValueError(u"Object {} is an ID. Did you forget to register the variable?" - u"".format(obj)) + raise ValueError("Object {} is an ID. Did you forget to register the variable?" + "".format(obj)) if inspect.isgenerator(obj): - raise ValueError(u"Object {} is a coroutine. Did you forget to await the expression with " - u"'yield'?".format(obj)) - raise ValueError(u"Object is not an expression", obj) + raise ValueError("Object {} is a coroutine. Did you forget to await the expression with " + "'yield'?".format(obj)) + raise ValueError("Object is not an expression", obj) -class Statement(object): +class Statement: def __init__(self): pass @@ -312,7 +311,7 @@ class Statement(object): class RawStatement(Statement): def __init__(self, text): - super(RawStatement, self).__init__() + super().__init__() self.text = text def __str__(self): @@ -321,38 +320,38 @@ class RawStatement(Statement): class ExpressionStatement(Statement): def __init__(self, expression): - super(ExpressionStatement, self).__init__() + super().__init__() self.expression = safe_exp(expression) def __str__(self): - return u"{};".format(self.expression) + return f"{self.expression};" class LineComment(Statement): - def __init__(self, value): # type: (unicode) -> None - super(LineComment, self).__init__() + def __init__(self, value): # type: (str) -> None + super().__init__() self._value = value def __str__(self): - parts = self._value.split(u'\n') - parts = [u'// {}'.format(x) for x in parts] - return u'\n'.join(parts) + parts = self._value.split('\n') + parts = [f'// {x}' for x in parts] + return '\n'.join(parts) class ProgmemAssignmentExpression(AssignmentExpression): def __init__(self, type, name, rhs, obj): - super(ProgmemAssignmentExpression, self).__init__( + super().__init__( type, '', name, rhs, obj ) def __str__(self): type_ = self.type - return u"static const {} {}[] PROGMEM = {}".format(type_, self.name, self.rhs) + return f"static const {type_} {self.name}[] PROGMEM = {self.rhs}" def progmem_array(id, rhs): rhs = safe_exp(rhs) - obj = MockObj(id, u'.') + obj = MockObj(id, '.') assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) @@ -381,7 +380,7 @@ def variable(id, # type: ID """ assert isinstance(id, ID) rhs = safe_exp(rhs) - obj = MockObj(id, u'.') + obj = MockObj(id, '.') if type is not None: id.type = type assignment = AssignmentExpression(id.type, '', id, rhs, obj) @@ -405,7 +404,7 @@ def Pvariable(id, # type: ID :returns The new variable as a MockObj. """ rhs = safe_exp(rhs) - obj = MockObj(id, u'->') + obj = MockObj(id, '->') if type is not None: id.type = type decl = VariableDeclarationExpression(id.type, '*', id) @@ -594,51 +593,51 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - def __init__(self, base, op=u'.'): + def __init__(self, base, op='.'): self.base = base self.op = op - super(MockObj, self).__init__() + super().__init__() def __getattr__(self, attr): # type: (str) -> MockObj - next_op = u'.' - if attr.startswith(u'P') and self.op not in ['::', '']: + next_op = '.' + if attr.startswith('P') and self.op not in ['::', '']: attr = attr[1:] - next_op = u'->' - if attr.startswith(u'_'): + next_op = '->' + if attr.startswith('_'): attr = attr[1:] - return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) + return MockObj(f'{self.base}{self.op}{attr}', next_op) def __call__(self, *args): # type: (SafeExpType) -> MockObj call = CallExpression(self.base, *args) return MockObj(call, self.op) - def __str__(self): # type: () -> unicode - return text_type(self.base) + def __str__(self): # type: () -> str + return str(self.base) def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return 'MockObj<{}>'.format(str(self.base)) @property def _(self): # type: () -> MockObj - return MockObj(u'{}{}'.format(self.base, self.op)) + return MockObj(f'{self.base}{self.op}') @property def new(self): # type: () -> MockObj - return MockObj(u'new {}'.format(self.base), u'->') + return MockObj(f'new {self.base}', '->') def template(self, *args): # type: (*SafeExpType) -> MockObj if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] - return MockObj(u'{}{}'.format(self.base, args)) + return MockObj(f'{self.base}{args}') def namespace(self, name): # type: (str) -> MockObj - return MockObj(u'{}{}'.format(self._, name), u'::') + return MockObj(f'{self._}{name}', '::') def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass op = '' if self.op == '' else '::' - return MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) + return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents) def struct(self, name): # type: (str) -> MockObjClass return self.class_(name) @@ -648,24 +647,24 @@ class MockObj(Expression): def operator(self, name): # type: (str) -> MockObj if name == 'ref': - return MockObj(u'{} &'.format(self.base), u'') + return MockObj(f'{self.base} &', '') if name == 'ptr': - return MockObj(u'{} *'.format(self.base), u'') + return MockObj(f'{self.base} *', '') if name == "const": - return MockObj(u'const {}'.format(self.base), u'') + return MockObj(f'const {self.base}', '') raise NotImplementedError @property def using(self): # type: () -> MockObj assert self.op == '::' - return MockObj(u'using namespace {}'.format(self.base)) + return MockObj(f'using namespace {self.base}') def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj - next_op = u'.' - if isinstance(item, str) and item.startswith(u'P'): + next_op = '.' + if isinstance(item, str) and item.startswith('P'): item = item[1:] - next_op = u'->' - return MockObj(u'{}[{}]'.format(self.base, item), next_op) + next_op = '->' + return MockObj(f'{self.base}[{item}]', next_op) class MockObjEnum(MockObj): @@ -679,13 +678,13 @@ class MockObjEnum(MockObj): kwargs['base'] = base MockObj.__init__(self, *args, **kwargs) - def __str__(self): # type: () -> unicode + def __str__(self): # type: () -> str if self._is_class: - return super(MockObjEnum, self).__str__() - return u'{}{}{}'.format(self.base, self.op, self._enum) + return super().__str__() + return f'{self.base}{self.op}{self._enum}' def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return 'MockObj<{}>'.format(str(self.base)) class MockObjClass(MockObj): @@ -716,7 +715,7 @@ class MockObjClass(MockObj): args = args[0] new_parents = self._parents[:] new_parents.append(self) - return MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) + return MockObjClass(f'{self.base}{args}', parents=new_parents) def __repr__(self): - return u'MockObjClass<{}, parents={}>'.format(text_type(self.base), self._parents) + return 'MockObjClass<{}, parents={}>'.format(str(self.base), self._parents) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 39ac8e7118..f01981acc8 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,9 +1,10 @@ from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ CONF_UPDATE_INTERVAL, CONF_TYPE_ID -from esphome.core import coroutine, ID, CORE +# pylint: disable=unused-import +from esphome.core import coroutine, ID, CORE, ConfigType from esphome.cpp_generator import RawExpression, add, get_variable from esphome.cpp_types import App, GPIOPin -from esphome.py_compat import text_type +from esphome.util import Registry, RegistryEntry @coroutine @@ -35,11 +36,11 @@ def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ - id_ = text_type(var.base) + id_ = str(var.base) if id_ not in CORE.component_ids: - raise ValueError(u"Component ID {} was not declared to inherit from Component, " - u"or was registered twice. Please create a bug report with your " - u"configuration.".format(id_)) + raise ValueError("Component ID {} was not declared to inherit from Component, " + "or was registered twice. Please create a bug report with your " + "configuration.".format(id_)) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) @@ -59,7 +60,7 @@ def register_parented(var, value): def extract_registry_entry_config(registry, full_config): - # type: ('Registry', 'ConfigType') -> 'RegistryEntry' + # type: (Registry, ConfigType) -> RegistryEntry key, config = next((k, v) for k, v in full_config.items() if k in registry) return registry[key], config diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c934626da8..8aea841247 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,4 @@ # pylint: disable=wrong-import-position -from __future__ import print_function import codecs import collections @@ -29,7 +28,6 @@ import tornado.websocket from esphome import const, util from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2, decode_text, encode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote @@ -42,7 +40,7 @@ from esphome.zeroconf import DashboardStatus, Zeroconf _LOGGER = logging.getLogger(__name__) -class DashboardSettings(object): +class DashboardSettings: def __init__(self): self.config_dir = '' self.password_digest = '' @@ -58,10 +56,7 @@ class DashboardSettings(object): self.username = args.username or os.getenv('USERNAME', '') self.using_password = bool(password) if self.using_password: - if IS_PY2: - self.password_digest = hmac.new(password).digest() - else: - self.password_digest = hmac.new(password.encode()).digest() + self.password_digest = hmac.new(password.encode()).digest() self.config_dir = args.configuration[0] @property @@ -88,8 +83,8 @@ class DashboardSettings(object): if username != self.username: return False - password_digest = hmac.new(encode_text(password)).digest() - return hmac.compare_digest(self.password_digest, password_digest) + password = hmac.new(password.encode()).digest() + return username == self.username and hmac.compare_digest(self.password_digest, password) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -100,10 +95,7 @@ class DashboardSettings(object): settings = DashboardSettings() -if IS_PY2: - cookie_authenticated_yes = 'yes' -else: - cookie_authenticated_yes = b'yes' +cookie_authenticated_yes = b'yes' def template_args(): @@ -181,7 +173,7 @@ def websocket_method(name): @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): - super(EsphomeCommandWebSocket, self).__init__(application, request, **kwargs) + super().__init__(application, request, **kwargs) self._proc = None self._is_closed = False @@ -204,7 +196,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # spawn can only be called once return command = self.build_command(json_message) - _LOGGER.info(u"Running command '%s'", ' '.join(shlex_quote(x) for x in command)) + _LOGGER.info("Running command '%s'", ' '.join(shlex_quote(x) for x in command)) self._proc = tornado.process.Subprocess(command, stdout=tornado.process.Subprocess.STREAM, stderr=subprocess.STDOUT, @@ -227,10 +219,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): @tornado.gen.coroutine def _redirect_stdout(self): - if IS_PY2: - reg = '[\n\r]' - else: - reg = b'[\n\r]' + reg = b'[\n\r]' while True: try: @@ -336,8 +325,8 @@ class WizardRequestHandler(BaseHandler): def post(self): from esphome import wizard - kwargs = {k: u''.join(decode_text(x) for x in v) for k, v in self.request.arguments.items()} - destination = settings.rel_path(kwargs['name'] + u'.yaml') + kwargs = {k: ''.join(str(x) for x in v) for k, v in self.request.arguments.items()} + destination = settings.rel_path(kwargs['name'] + '.yaml') wizard.wizard_write(path=destination, **kwargs) self.redirect('./?begin=True') @@ -355,8 +344,8 @@ class DownloadBinaryRequestHandler(BaseHandler): path = storage_json.firmware_bin_path self.set_header('Content-Type', 'application/octet-stream') - filename = '{}.bin'.format(storage_json.name) - self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename)) + filename = f'{storage_json.name}.bin' + self.set_header("Content-Disposition", f'attachment; filename="{filename}"') with open(path, 'rb') as f: while True: data = f.read(16384) @@ -371,7 +360,7 @@ def _list_dashboard_entries(): return [DashboardEntry(file) for file in files] -class DashboardEntry(object): +class DashboardEntry: def __init__(self, path): self.path = path self._storage = None @@ -609,8 +598,8 @@ class LoginHandler(BaseHandler): 'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'), } data = { - 'username': decode_text(self.get_argument('username', '')), - 'password': decode_text(self.get_argument('password', '')) + 'username': self.get_argument('username', ''), + 'password': self.get_argument('password', '') } try: req = requests.post('http://hassio/auth', headers=headers, data=data) @@ -627,8 +616,8 @@ class LoginHandler(BaseHandler): self.render_login_page(error="Invalid username or password") def post_native_login(self): - username = decode_text(self.get_argument("username", '')) - password = decode_text(self.get_argument("password", '')) + username = self.get_argument("username", '') + password = self.get_argument("password", '') if settings.check_password(username, password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -663,7 +652,7 @@ def get_static_file_url(name): with open(path, 'rb') as f_handle: hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] _STATIC_FILE_HASHES[name] = hash_ - return u'./static/{}?hash={}'.format(name, hash_) + return f'./static/{name}?hash={hash_}' def make_app(debug=False): @@ -754,7 +743,7 @@ def start_web_server(args): if args.open_ui: import webbrowser - webbrowser.open('localhost:{}'.format(args.port)) + webbrowser.open(f'localhost:{args.port}') if settings.status_use_ping: status_thread = PingStatusThread() diff --git a/esphome/espota2.py b/esphome/espota2.py index 40417b9ab2..edfa4e63e6 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -7,7 +7,6 @@ import time from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address -from esphome.py_compat import IS_PY2, char_to_byte RESPONSE_OK = 0 RESPONSE_REQUEST_AUTH = 1 @@ -38,7 +37,7 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] _LOGGER = logging.getLogger(__name__) -class ProgressBar(object): +class ProgressBar: def __init__(self): self.last_progress = None @@ -72,33 +71,31 @@ def recv_decode(sock, amount, decode=True): data = sock.recv(amount) if not decode: return data - return [char_to_byte(x) for x in data] + return list(data) def receive_exactly(sock, amount, msg, expect, decode=True): if decode: data = [] - elif IS_PY2: - data = '' else: data = b'' try: data += recv_decode(sock, 1, decode=decode) - except socket.error as err: - raise OTAError("Error receiving acknowledge {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error receiving acknowledge {msg}: {err}") try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError("Error {}: {}".format(msg, err)) + raise OTAError(f"Error {msg}: {err}") while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) - except socket.error as err: - raise OTAError("Error receiving {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error receiving {msg}: {err}") return data @@ -145,22 +142,16 @@ def check_error(data, expect): def send_check(sock, data, msg): try: - if IS_PY2: - if isinstance(data, (list, tuple)): - data = ''.join([chr(x) for x in data]) - elif isinstance(data, int): - data = chr(data) - else: - if isinstance(data, (list, tuple)): - data = bytes(data) - elif isinstance(data, int): - data = bytes([data]) - elif isinstance(data, str): - data = data.encode('utf8') + if isinstance(data, (list, tuple)): + data = bytes(data) + elif isinstance(data, int): + data = bytes([data]) + elif isinstance(data, str): + data = data.encode('utf8') sock.sendall(data) - except socket.error as err: - raise OTAError("Error sending {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error sending {msg}: {err}") def perform_ota(sock, password, file_handle, filename): @@ -176,7 +167,7 @@ def perform_ota(sock, password, file_handle, filename): _, version = receive_exactly(sock, 2, 'version', RESPONSE_OK) if version != OTA_VERSION_1_0: - raise OTAError("Unsupported OTA version {}".format(version)) + raise OTAError(f"Unsupported OTA version {version}") # Features send_check(sock, 0x00, 'features') @@ -186,9 +177,7 @@ def perform_ota(sock, password, file_handle, filename): if auth == RESPONSE_REQUEST_AUTH: if not password: raise OTAError("ESP requests password, but no password given!") - nonce = receive_exactly(sock, 32, 'authentication nonce', [], decode=False) - if not IS_PY2: - nonce = nonce.decode() + nonce = receive_exactly(sock, 32, 'authentication nonce', [], decode=False).decode() _LOGGER.debug("Auth: Nonce is %s", nonce) cnonce = hashlib.md5(str(random.random()).encode()).hexdigest() _LOGGER.debug("Auth: CNonce is %s", cnonce) @@ -235,9 +224,9 @@ def perform_ota(sock, password, file_handle, filename): try: sock.sendall(chunk) - except socket.error as err: + except OSError as err: sys.stderr.write('\n') - raise OTAError("Error sending data: {}".format(err)) + raise OTAError(f"Error sending data: {err}") progress.update(offset / float(file_size)) progress.done() @@ -277,7 +266,7 @@ def run_ota_impl_(remote_host, remote_port, password, filename): sock.settimeout(10.0) try: sock.connect((ip, remote_port)) - except socket.error as err: + except OSError as err: sock.close() _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) return 1 diff --git a/esphome/helpers.py b/esphome/helpers.py index 179452c353..6413a25e01 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,12 +1,8 @@ -from __future__ import print_function - import codecs import logging import os -from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text - _LOGGER = logging.getLogger(__name__) @@ -18,24 +14,24 @@ def ensure_unique_string(preferred_string, current_strings): while test_string in current_strings_set: tries += 1 - test_string = u"{}_{}".format(preferred_string, tries) + test_string = f"{preferred_string}_{tries}" return test_string -def indent_all_but_first_and_last(text, padding=u' '): +def indent_all_but_first_and_last(text, padding=' '): lines = text.splitlines(True) if len(lines) <= 2: return text - return lines[0] + u''.join(padding + line for line in lines[1:-1]) + lines[-1] + return lines[0] + ''.join(padding + line for line in lines[1:-1]) + lines[-1] -def indent_list(text, padding=u' '): +def indent_list(text, padding=' '): return [padding + line for line in text.splitlines()] -def indent(text, padding=u' '): - return u'\n'.join(indent_list(text, padding)) +def indent(text, padding=' '): + return '\n'.join(indent_list(text, padding)) # From https://stackoverflow.com/a/14945195/8924614 @@ -43,17 +39,16 @@ def cpp_string_escape(string, encoding='utf-8'): def _should_escape(byte): # type: (int) -> bool if not 32 <= byte < 127: return True - if byte in (char_to_byte('\\'), char_to_byte('"')): + if byte in (ord('\\'), ord('"')): return True return False - if isinstance(string, text_type): + if isinstance(string, str): string = string.encode(encoding) result = '' for character in string: - character = char_to_byte(character) if _should_escape(character): - result += '\\%03o' % character + result += f'\\{character:03o}' else: result += chr(character) return '"' + result + '"' @@ -91,7 +86,7 @@ def mkdir_p(path): pass else: from esphome.core import EsphomeError - raise EsphomeError(u"Error creating directories {}: {}".format(path, err)) + raise EsphomeError(f"Error creating directories {path}: {err}") def is_ip_address(host): @@ -118,7 +113,7 @@ def _resolve_with_zeroconf(host): try: info = zc.resolve_host(host + '.') except Exception as err: - raise EsphomeError("Error resolving mDNS hostname: {}".format(err)) + raise EsphomeError(f"Error resolving mDNS hostname: {err}") finally: zc.close() if info is None: @@ -141,7 +136,7 @@ def resolve_ip_address(host): try: return socket.gethostbyname(host) - except socket.error as err: + except OSError as err: errs.append(str(err)) raise EsphomeError("Error resolving IP address: {}" "".format(', '.join(errs))) @@ -167,10 +162,10 @@ def read_file(path): return f_handle.read() except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + raise EsphomeError(f"Error reading file {path}: {err}") except UnicodeDecodeError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + raise EsphomeError(f"Error reading file {path}: {err}") def _write_file(path, text): @@ -179,20 +174,17 @@ def _write_file(path, text): mkdir_p(directory) tmp_path = None - data = encode_text(text) + data = text + if isinstance(text, str): + data = text.encode() try: with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: tmp_path = f_handle.name f_handle.write(data) # Newer tempfile implementations create the file with mode 0o600 os.chmod(tmp_path, 0o644) - if IS_PY2: - if os.path.exists(path): - os.remove(path) - os.rename(tmp_path, path) - else: - # If destination exists, will be overwritten - os.replace(tmp_path, path) + # If destination exists, will be overwritten + os.replace(tmp_path, path) finally: if tmp_path is not None and os.path.exists(tmp_path): try: @@ -206,7 +198,7 @@ def write_file(path, text): _write_file(path, text) except OSError: from esphome.core import EsphomeError - raise EsphomeError(u"Could not write file at {}".format(path)) + raise EsphomeError(f"Could not write file at {path}") def write_file_if_changed(path, text): @@ -226,7 +218,7 @@ def copy_file_if_changed(src, dst): shutil.copy(src, dst) except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err)) + raise EsphomeError(f"Error copying file {src} to {dst}: {err}") def list_starts_with(list_, sub): @@ -273,10 +265,6 @@ _TYPE_OVERLOADS = { list: type('EList', (list,), dict()), } -if IS_PY2: - _TYPE_OVERLOADS[long] = type('long', (long,), dict()) - _TYPE_OVERLOADS[unicode] = type('unicode', (unicode,), dict()) - # cache created classes here _CLASS_LOOKUP = {} diff --git a/esphome/legacy.py b/esphome/legacy.py index 0e19545d68..27373ee1a3 100644 --- a/esphome/legacy.py +++ b/esphome/legacy.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 77eb941363..cbcf067c44 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,9 +1,6 @@ -from __future__ import print_function - from datetime import datetime import hashlib import logging -import socket import ssl import sys import time @@ -15,7 +12,6 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME from esphome.core import CORE, EsphomeError from esphome.helpers import color -from esphome.py_compat import decode_text from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -37,7 +33,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) if client.reconnect() == 0: _LOGGER.info("Successfully reconnected to the MQTT server") break - except socket.error: + except OSError: pass wait_time = min(2**tries, 300) @@ -47,7 +43,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) time.sleep(wait_time) tries += 1 - client = mqtt.Client(client_id or u'') + client = mqtt.Client(client_id or '') client.on_connect = on_connect client.on_message = on_message client.on_disconnect = on_disconnect @@ -70,8 +66,8 @@ def initialize(config, subscriptions, on_message, username, password, client_id) host = str(config[CONF_MQTT][CONF_BROKER]) port = int(config[CONF_MQTT][CONF_PORT]) client.connect(host, port) - except socket.error as err: - raise EsphomeError("Cannot connect to MQTT broker: {}".format(err)) + except OSError as err: + raise EsphomeError(f"Cannot connect to MQTT broker: {err}") try: client.loop_forever() @@ -88,17 +84,17 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): if CONF_LOG_TOPIC in conf: topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: - topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + u'/debug' + topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + '/debug' else: - topic = config[CONF_ESPHOME][CONF_NAME] + u'/debug' + topic = config[CONF_ESPHOME][CONF_NAME] + '/debug' else: - _LOGGER.error(u"MQTT isn't setup, can't start MQTT logs") + _LOGGER.error("MQTT isn't setup, can't start MQTT logs") return 1 - _LOGGER.info(u"Starting log output from %s", topic) + _LOGGER.info("Starting log output from %s", topic) def on_message(client, userdata, msg): - time_ = datetime.now().time().strftime(u'[%H:%M:%S]') - payload = decode_text(msg.payload) + time_ = datetime.now().time().strftime('[%H:%M:%S]') + payload = msg.payload.decode(errors='backslashreplace') message = time_ + payload safe_print(message) @@ -107,20 +103,20 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def clear_topic(config, topic, username=None, password=None, client_id=None): if topic is None: - discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, u'homeassistant') + discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, 'homeassistant') name = config[CONF_ESPHOME][CONF_NAME] - topic = u'{}/+/{}/#'.format(discovery_prefix, name) - _LOGGER.info(u"Clearing messages from '%s'", topic) - _LOGGER.info(u"Please close this window when no more messages appear and the " - u"MQTT topic has been cleared of retained messages.") + topic = f'{discovery_prefix}/+/{name}/#' + _LOGGER.info("Clearing messages from '%s'", topic) + _LOGGER.info("Please close this window when no more messages appear and the " + "MQTT topic has been cleared of retained messages.") def on_message(client, userdata, msg): if not msg.payload or not msg.retain: return try: - print(u"Clearing topic {}".format(msg.topic)) + print(f"Clearing topic {msg.topic}") except UnicodeDecodeError: - print(u"Skipping non-UTF-8 topic (prohibited by MQTT standard)") + print("Skipping non-UTF-8 topic (prohibited by MQTT standard)") return client.publish(msg.topic, None, retain=True) @@ -133,14 +129,14 @@ def get_fingerprint(config): _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) try: cert_pem = ssl.get_server_certificate(addr) - except IOError as err: + except OSError as err: _LOGGER.error("Unable to connect to server: %s", err) return 1 cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print(u"SHA1 Fingerprint: " + color('cyan', sha1)) - safe_print(u"Copy the string above into mqtt.ssl_fingerprints section of {}" - u"".format(CORE.config_path)) + safe_print("SHA1 Fingerprint: " + color('cyan', sha1)) + safe_print("Copy the string above into mqtt.ssl_fingerprints section of {}" + "".format(CORE.config_path)) return 0 diff --git a/esphome/pins.py b/esphome/pins.py index 9a2cf90984..22192b599e 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import esphome.config_validation as cv @@ -271,13 +269,13 @@ def _lookup_pin(value): return board_pins[value] if value in base_pins: return base_pins[value] - raise cv.Invalid(u"Cannot resolve pin name '{}' for board {}.".format(value, CORE.board)) + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {CORE.board}.") def _translate_pin(value): if isinstance(value, dict) or value is None: - raise cv.Invalid(u"This variable only supports pin numbers, not full pin schemas " - u"(with inverted and mode).") + raise cv.Invalid("This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode).") if isinstance(value, int): return value try: @@ -301,27 +299,27 @@ def validate_gpio_pin(value): value = _translate_pin(value) if CORE.is_esp32: if value < 0 or value > 39: - raise cv.Invalid(u"ESP32: Invalid pin number: {}".format(value)) + raise cv.Invalid(f"ESP32: Invalid pin number: {value}") if value in _ESP_SDIO_PINS: raise cv.Invalid("This pin cannot be used on ESP32s and is already used by " "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) if 9 <= value <= 10: - _LOGGER.warning(u"ESP32: Pin %s (9-10) might already be used by the " - u"flash interface in QUAD IO flash mode.", value) + _LOGGER.warning("ESP32: Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", value) if value in (20, 24, 28, 29, 30, 31): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet - raise cv.Invalid("The pin GPIO{} is not usable on ESP32s.".format(value)) + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") return value if CORE.is_esp8266: if value < 0 or value > 17: - raise cv.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) + raise cv.Invalid(f"ESP8266: Invalid pin number: {value}") if value in _ESP_SDIO_PINS: raise cv.Invalid("This pin cannot be used on ESP8266s and is already used by " "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) if 9 <= value <= 10: - _LOGGER.warning(u"ESP8266: Pin %s (9-10) might already be used by the " - u"flash interface in QUAD IO flash mode.", value) + _LOGGER.warning("ESP8266: Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", value) return value raise NotImplementedError @@ -349,8 +347,8 @@ def output_pin(value): value = validate_gpio_pin(value) if CORE.is_esp32: if 34 <= value <= 39: - raise cv.Invalid(u"ESP32: GPIO{} (34-39) can only be used as an " - u"input pin.".format(value)) + raise cv.Invalid("ESP32: GPIO{} (34-39) can only be used as an " + "input pin.".format(value)) return value if CORE.is_esp8266: if value == 17: @@ -364,11 +362,11 @@ def analog_pin(value): if CORE.is_esp32: if 32 <= value <= 39: # ADC1 return value - raise cv.Invalid(u"ESP32: Only pins 32 though 39 support ADC.") + raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") if CORE.is_esp8266: if value == 17: # A0 return value - raise cv.Invalid(u"ESP8266: Only pin A0 (GPIO17) supports ADC.") + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") raise NotImplementedError diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 317670710b..59f5bf20ae 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import json import logging import os @@ -7,7 +5,6 @@ import re import subprocess from esphome.core import CORE -from esphome.py_compat import decode_text from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) @@ -61,6 +58,7 @@ FILTER_PLATFORMIO_LINES = [ r'Installing dependencies', r'.* @ .* is already installed', r'Building in .* mode', + r'Advanced Memory Usage is available via .*', ] @@ -100,8 +98,7 @@ def run_upload(config, verbose, port): def run_idedata(config): args = ['-t', 'idedata'] - stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) - stdout = decode_text(stdout) + stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True).decode() match = re.search(r'{\s*".*}', stdout) if match is None: _LOGGER.debug("Could not match IDEData for %s", stdout) @@ -172,7 +169,7 @@ def _decode_pc(config, addr): return command = [idedata.addr2line_path, '-pfiaC', '-e', idedata.firmware_elf_path, addr] try: - translation = decode_text(subprocess.check_output(command)).strip() + translation = subprocess.check_output(command).decode().strip() except Exception: # pylint: disable=broad-except _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return @@ -246,7 +243,7 @@ def process_stacktrace(config, line, backtrace_state): return backtrace_state -class IDEData(object): +class IDEData: def __init__(self, raw): if not isinstance(raw, dict): self.raw = {} diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 6cdaa5b047..e69de29bb2 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -1,89 +0,0 @@ -import functools -import sys -import codecs - -PYTHON_MAJOR = sys.version_info[0] -IS_PY2 = PYTHON_MAJOR == 2 -IS_PY3 = PYTHON_MAJOR == 3 - - -# pylint: disable=no-else-return -def safe_input(prompt=None): - if IS_PY2: - if prompt is None: - return raw_input() - return raw_input(prompt) - else: - if prompt is None: - return input() - return input(prompt) - - -if IS_PY2: - text_type = unicode - string_types = (str, unicode) - integer_types = (int, long) - binary_type = str -else: - text_type = str - string_types = (str,) - integer_types = (int,) - binary_type = bytes - - -def byte_to_bytes(val): # type: (int) -> bytes - if IS_PY2: - return chr(val) - else: - return bytes([val]) - - -def char_to_byte(val): # type: (str) -> int - if IS_PY2: - if isinstance(val, string_types): - return ord(val) - elif isinstance(val, int): - return val - else: - raise ValueError - else: - if isinstance(val, str): - return ord(val) - elif isinstance(val, int): - return val - else: - raise ValueError - - -def format_bytes(val): - if IS_PY2: - return ' '.join('{:02X}'.format(ord(x)) for x in val) - else: - return ' '.join('{:02X}'.format(x) for x in val) - - -def sort_by_cmp(list_, cmp): - if IS_PY2: - list_.sort(cmp=cmp) - else: - list_.sort(key=functools.cmp_to_key(cmp)) - - -def indexbytes(buf, i): - if IS_PY3: - return buf[i] - else: - return ord(buf[i]) - - -def decode_text(data, encoding='utf-8', errors='strict'): - if isinstance(data, text_type): - return data - return codecs.decode(data, encoding, errors) - - -def encode_text(data, encoding='utf-8', errors='strict'): - if isinstance(data, binary_type): - return data - - return codecs.encode(data, encoding, errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index f3e53ce168..b4dc29c9cd 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -13,17 +13,15 @@ from esphome.helpers import write_file_if_changed from esphome.core import CoreType from typing import Any, Optional, List -from esphome.py_compat import text_type - _LOGGER = logging.getLogger(__name__) def storage_path(): # type: () -> str - return CORE.relative_config_path('.esphome', '{}.json'.format(CORE.config_filename)) + return CORE.relative_config_path('.esphome', f'{CORE.config_filename}.json') def ext_storage_path(base_path, config_filename): # type: (str, str) -> str - return os.path.join(base_path, '.esphome', '{}.json'.format(config_filename)) + return os.path.join(base_path, '.esphome', f'{config_filename}.json') def esphome_storage_path(base_path): # type: (str) -> str @@ -35,7 +33,7 @@ def trash_storage_path(base_path): # type: (str) -> str # pylint: disable=too-many-instance-attributes -class StorageJSON(object): +class StorageJSON: def __init__(self, storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations): @@ -85,7 +83,7 @@ class StorageJSON(object): } def to_json(self): - return json.dumps(self.as_dict(), indent=2) + u'\n' + return json.dumps(self.as_dict(), indent=2) + '\n' def save(self, path): write_file_if_changed(path, self.to_json()) @@ -156,7 +154,7 @@ class StorageJSON(object): return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() -class EsphomeStorageJSON(object): +class EsphomeStorageJSON: def __init__(self, storage_version, cookie_secret, last_update_check, remote_version): # Version of the storage JSON schema @@ -189,7 +187,7 @@ class EsphomeStorageJSON(object): self.last_update_check_str = new.strftime("%Y-%m-%dT%H:%M:%S") def to_json(self): # type: () -> dict - return json.dumps(self.as_dict(), indent=2) + u'\n' + return json.dumps(self.as_dict(), indent=2) + '\n' def save(self, path): # type: (str) -> None write_file_if_changed(path, self.to_json()) @@ -216,7 +214,7 @@ class EsphomeStorageJSON(object): def get_default(): # type: () -> EsphomeStorageJSON return EsphomeStorageJSON( storage_version=1, - cookie_secret=text_type(binascii.hexlify(os.urandom(64))), + cookie_secret=binascii.hexlify(os.urandom(64)).decode(), last_update_check=None, remote_version=None, ) diff --git a/esphome/util.py b/esphome/util.py index b8e65cd576..6677946b01 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import collections import io import logging @@ -9,12 +7,11 @@ import subprocess import sys from esphome import const -from esphome.py_compat import IS_PY2, decode_text, text_type _LOGGER = logging.getLogger(__name__) -class RegistryEntry(object): +class RegistryEntry: def __init__(self, name, fun, type_id, schema): self.name = name self.fun = fun @@ -34,7 +31,7 @@ class RegistryEntry(object): class Registry(dict): def __init__(self, base_schema=None, type_id_key=None): - super(Registry, self).__init__() + super().__init__() self.base_schema = base_schema or {} self.type_id_key = type_id_key @@ -81,17 +78,17 @@ def safe_print(message=""): def shlex_quote(s): if not s: - return u"''" + return "''" if re.search(r'[^\w@%+=:,./-]', s) is None: return s - return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + return "'" + s.replace("'", "'\"'\"'") + "'" ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]') -class RedirectText(object): +class RedirectText: def __init__(self, out, filter_lines=None): self._out = out if filter_lines is None: @@ -116,13 +113,12 @@ class RedirectText(object): self._out.write(s) def write(self, s): - # s is usually a text_type already (self._out is of type TextIOWrapper) + # s is usually a str already (self._out is of type TextIOWrapper) # However, s is sometimes also a bytes object in python3. Let's make sure it's a - # text_type + # str # If the conversion fails, we will create an exception, which is okay because we won't # be able to print it anyway. - text = decode_text(s) - assert isinstance(text, text_type) + text = s.decode() if self._filter_pattern is not None: self._line_buffer += text @@ -160,8 +156,8 @@ def run_external_command(func, *cmd, **kwargs): orig_argv = sys.argv orig_exit = sys.exit # mock sys.exit - full_cmd = u' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info(u"Running: %s", full_cmd) + full_cmd = ' '.join(shlex_quote(x) for x in cmd) + _LOGGER.info("Running: %s", full_cmd) filter_lines = kwargs.get('filter_lines') orig_stdout = sys.stdout @@ -182,8 +178,8 @@ def run_external_command(func, *cmd, **kwargs): except SystemExit as err: return err.args[0] except Exception as err: # pylint: disable=broad-except - _LOGGER.error(u"Running command failed: %s", err) - _LOGGER.error(u"Please try running %s locally.", full_cmd) + _LOGGER.error("Running command failed: %s", err) + _LOGGER.error("Please try running %s locally.", full_cmd) return 1 finally: sys.argv = orig_argv @@ -198,8 +194,8 @@ def run_external_command(func, *cmd, **kwargs): def run_external_process(*cmd, **kwargs): - full_cmd = u' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info(u"Running: %s", full_cmd) + full_cmd = ' '.join(shlex_quote(x) for x in cmd) + _LOGGER.info("Running: %s", full_cmd) filter_lines = kwargs.get('filter_lines') capture_stdout = kwargs.get('capture_stdout', False) @@ -215,8 +211,8 @@ def run_external_process(*cmd, **kwargs): stdout=sub_stdout, stderr=sub_stderr) except Exception as err: # pylint: disable=broad-except - _LOGGER.error(u"Running command failed: %s", err) - _LOGGER.error(u"Please try running %s locally.", full_cmd) + _LOGGER.error("Running command failed: %s", err) + _LOGGER.error("Please try running %s locally.", full_cmd) return 1 finally: if capture_stdout: @@ -233,29 +229,6 @@ class OrderedDict(collections.OrderedDict): def __repr__(self): return dict(self).__repr__() - def move_to_end(self, key, last=True): - if IS_PY2: - if len(self) == 1: - return - if last: - # When moving to end, just pop and re-add - val = self.pop(key) - self[key] = val - else: - # When moving to front, use internals here - # https://stackoverflow.com/a/16664932 - root = self._OrderedDict__root # pylint: disable=no-member - first = root[1] - link = self._OrderedDict__map[key] # pylint: disable=no-member - link_prev, link_next, _ = link - link_prev[1] = link_next - link_next[0] = link_prev - link[0] = root - link[1] = first - root[1] = first[0] = link - else: - super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member - def list_yaml_files(folder): files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 09fa1a6756..8193c1317c 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -3,8 +3,6 @@ import itertools import voluptuous as vol -from esphome.py_compat import string_types - class ExtraKeysInvalid(vol.Invalid): def __init__(self, *arg, **kwargs): @@ -22,14 +20,14 @@ def ensure_multiple_invalid(err): class _Schema(vol.Schema): """Custom cv.Schema that prints similar keys on error.""" def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None): - super(_Schema, self).__init__(schema, required=required, extra=extra) + super().__init__(schema, required=required, extra=extra) # List of extra schemas to apply after validation # Should be used sparingly, as it's not a very voluptuous-way/clean way of # doing things. self._extra_schemas = extra_schemas or [] def __call__(self, data): - res = super(_Schema, self).__call__(data) + res = super().__call__(data) for extra in self._extra_schemas: try: res = extra(res) @@ -51,10 +49,10 @@ class _Schema(vol.Schema): raise ValueError("All schema keys must be wrapped in cv.Required or cv.Optional") # Keys that may be required - all_required_keys = set(key for key in schema if isinstance(key, vol.Required)) + all_required_keys = {key for key in schema if isinstance(key, vol.Required)} # Keys that may have defaults - all_default_keys = set(key for key in schema if isinstance(key, vol.Optional)) + all_default_keys = {key for key in schema if isinstance(key, vol.Optional)} # Recursively compile schema _compiled_schema = {} @@ -84,9 +82,9 @@ class _Schema(vol.Schema): key_names = [] for skey in schema: - if isinstance(skey, string_types): + if isinstance(skey, str): key_names.append(skey) - elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str): key_names.append(skey.schema) def validate_mapping(path, iterable, out): @@ -156,7 +154,7 @@ class _Schema(vol.Schema): if self.extra == vol.ALLOW_EXTRA: out[key] = value elif self.extra != vol.REMOVE_EXTRA: - if isinstance(key, string_types) and key_names: + if isinstance(key, str) and key_names: matches = difflib.get_close_matches(key, key_names) errors.append(ExtraKeysInvalid('extra keys not allowed', key_path, candidates=matches)) @@ -195,5 +193,5 @@ class _Schema(vol.Schema): schema = schemas[0] if isinstance(schema, vol.Schema): schema = schema.schema - ret = super(_Schema, self).extend(schema, extra=extra) + ret = super().extend(schema, extra=extra) return _Schema(ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas) diff --git a/esphome/vscode.py b/esphome/vscode.py index 6b35d4bd7a..e8c0b106f7 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -1,19 +1,17 @@ -from __future__ import print_function - import json import os +# pylint: disable=unused-import from esphome.config import load_config, _format_vol_invalid, Config from esphome.core import CORE, DocumentRange -from esphome.py_compat import text_type, safe_input +import esphome.config_validation as cv # pylint: disable=unused-import, wrong-import-order -import voluptuous as vol from typing import Optional def _get_invalid_range(res, invalid): - # type: (Config, vol.Invalid) -> Optional[DocumentRange] + # type: (Config, cv.Invalid) -> Optional[DocumentRange] return res.get_deepest_document_range_for_path(invalid.path) @@ -30,7 +28,7 @@ def _dump_range(range): } -class VSCodeResult(object): +class VSCodeResult: def __init__(self): self.yaml_errors = [] self.validation_errors = [] @@ -57,7 +55,7 @@ class VSCodeResult(object): def read_config(args): while True: CORE.reset() - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'validate' CORE.vscode = True CORE.ace = args.ace @@ -70,7 +68,7 @@ def read_config(args): try: res = load_config() except Exception as err: # pylint: disable=broad-except - vs.add_yaml_error(text_type(err)) + vs.add_yaml_error(str(err)) else: for err in res.errors: try: diff --git a/esphome/wizard.py b/esphome/wizard.py index 8cc759f934..b00f6d2b01 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os import random import string @@ -11,7 +9,6 @@ import esphome.config_validation as cv from esphome.helpers import color, get_bool_env, write_file # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS -from esphome.py_compat import safe_input, text_type from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print @@ -44,7 +41,7 @@ OTA_BIG = r""" ____ _______ \____/ |_/_/ \_\\ """ -BASE_CONFIG = u"""esphome: +BASE_CONFIG = """esphome: name: {name} platform: {platform} board: {board} @@ -75,7 +72,7 @@ def sanitize_double_quotes(value): def wizard_file(**kwargs): letters = string.ascii_letters + string.digits ap_name_base = kwargs['name'].replace('_', ' ').title() - ap_name = "{} Fallback Hotspot".format(ap_name_base) + ap_name = f"{ap_name_base} Fallback Hotspot" if len(ap_name) > 32: ap_name = ap_name_base kwargs['fallback_name'] = ap_name @@ -84,9 +81,9 @@ def wizard_file(**kwargs): config = BASE_CONFIG.format(**kwargs) if kwargs['password']: - config += u' password: "{0}"\n\nota:\n password: "{0}"\n'.format(kwargs['password']) + config += ' password: "{0}"\n\nota:\n password: "{0}"\n'.format(kwargs['password']) else: - config += u"\nota:\n" + config += "\nota:\n" return config @@ -119,7 +116,7 @@ else: def safe_print_step(step, big): safe_print() safe_print() - safe_print("============= STEP {} =============".format(step)) + safe_print(f"============= STEP {step} =============") safe_print(big) safe_print("===================================") sleep(0.25) @@ -127,24 +124,24 @@ def safe_print_step(step, big): def default_input(text, default): safe_print() - safe_print(u"Press ENTER for default ({})".format(default)) - return safe_input(text.format(default)) or default + safe_print(f"Press ENTER for default ({default})") + return input(text.format(default)) or default # From https://stackoverflow.com/a/518232/8924614 def strip_accents(value): - return u''.join(c for c in unicodedata.normalize('NFD', text_type(value)) - if unicodedata.category(c) != 'Mn') + return ''.join(c for c in unicodedata.normalize('NFD', str(value)) + if unicodedata.category(c) != 'Mn') def wizard(path): if not path.endswith('.yaml') and not path.endswith('.yml'): - safe_print(u"Please make your configuration file {} have the extension .yaml or .yml" - u"".format(color('cyan', path))) + safe_print("Please make your configuration file {} have the extension .yaml or .yml" + "".format(color('cyan', path))) return 1 if os.path.exists(path): - safe_print(u"Uh oh, it seems like {} already exists, please delete that file first " - u"or chose another configuration file.".format(color('cyan', path))) + safe_print("Uh oh, it seems like {} already exists, please delete that file first " + "or chose another configuration file.".format(color('cyan', path))) return 1 safe_print("Hi there!") sleep(1.5) @@ -164,21 +161,21 @@ def wizard(path): color('bold_white', "livingroom"))) safe_print() sleep(1) - name = safe_input(color("bold_white", "(name): ")) + name = input(color("bold_white", "(name): ")) while True: try: name = cv.valid_name(name) break except vol.Invalid: - safe_print(color("red", u"Oh noes, \"{}\" isn't a valid name. Names can only include " - u"numbers, letters and underscores.".format(name))) + safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " + "numbers, letters and underscores.".format(name))) name = strip_accents(name).replace(' ', '_') - name = u''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) - safe_print(u"Shall I use \"{}\" as the name instead?".format(color('cyan', name))) + name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) + safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) sleep(0.5) - name = default_input(u"(name [{}]): ", name) + name = default_input("(name [{}]): ", name) - safe_print(u"Great! Your node is now called \"{}\".".format(color('cyan', name))) + safe_print("Great! Your node is now called \"{}\".".format(color('cyan', name))) sleep(1) safe_print_step(2, ESP_BIG) safe_print("Now I'd like to know what microcontroller you're using so that I can compile " @@ -189,14 +186,14 @@ def wizard(path): sleep(0.5) safe_print() safe_print("Please enter either ESP32 or ESP8266.") - platform = safe_input(color("bold_white", "(ESP32/ESP8266): ")) + platform = input(color("bold_white", "(ESP32/ESP8266): ")) try: platform = vol.All(vol.Upper, vol.Any('ESP32', 'ESP8266'))(platform) break except vol.Invalid: - safe_print(u"Unfortunately, I can't find an espressif microcontroller called " - u"\"{}\". Please try again.".format(platform)) - safe_print(u"Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) + safe_print("Unfortunately, I can't find an espressif microcontroller called " + "\"{}\". Please try again.".format(platform)) + safe_print("Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) safe_print() sleep(1) @@ -221,17 +218,17 @@ def wizard(path): safe_print("Options: {}".format(', '.join(sorted(boards)))) while True: - board = safe_input(color("bold_white", "(board): ")) + board = input(color("bold_white", "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: - safe_print(color('red', "Sorry, I don't think the board \"{}\" exists.".format(board))) + safe_print(color('red', f"Sorry, I don't think the board \"{board}\" exists.")) safe_print() sleep(0.25) safe_print() - safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board))) + safe_print("Way to go! You've chosen {} as your board.".format(color('cyan', board))) safe_print() sleep(1) @@ -241,22 +238,22 @@ def wizard(path): safe_print() sleep(1) safe_print("First, what's the " + color('green', 'SSID') + - u" (the name) of the WiFi network {} I should connect to?".format(name)) + f" (the name) of the WiFi network {name} I should connect to?") sleep(1.5) safe_print("For example \"{}\".".format(color('bold_white', "Abraham Linksys"))) while True: - ssid = safe_input(color('bold_white', "(ssid): ")) + ssid = input(color('bold_white', "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: - safe_print(color('red', u"Unfortunately, \"{}\" doesn't seem to be a valid SSID. " - u"Please try again.".format(ssid))) + safe_print(color('red', "Unfortunately, \"{}\" doesn't seem to be a valid SSID. " + "Please try again.".format(ssid))) safe_print() sleep(1) - safe_print(u"Thank you very much! You've just chosen \"{}\" as your SSID." - u"".format(color('cyan', ssid))) + safe_print("Thank you very much! You've just chosen \"{}\" as your SSID." + "".format(color('cyan', ssid))) safe_print() sleep(0.75) @@ -265,7 +262,7 @@ def wizard(path): safe_print() safe_print("For example \"{}\"".format(color('bold_white', 'PASSWORD42'))) sleep(0.5) - psk = safe_input(color('bold_white', '(PSK): ')) + psk = input(color('bold_white', '(PSK): ')) safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).") sleep(1.5) @@ -277,7 +274,7 @@ def wizard(path): safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color('bold_white', '(password): ')) + password = input(color('bold_white', '(password): ')) wizard_write(path=path, name=name, platform=platform, board=board, ssid=ssid, psk=psk, password=password) diff --git a/esphome/writer.py b/esphome/writer.py index b036413a66..b3a60c5de9 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import logging import os import re @@ -14,19 +12,19 @@ from esphome.storage_json import StorageJSON, storage_path _LOGGER = logging.getLogger(__name__) -CPP_AUTO_GENERATE_BEGIN = u'// ========== AUTO GENERATED CODE BEGIN ===========' -CPP_AUTO_GENERATE_END = u'// =========== AUTO GENERATED CODE END ============' -CPP_INCLUDE_BEGIN = u'// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========' -CPP_INCLUDE_END = u'// ========== AUTO GENERATED INCLUDE BLOCK END ===========' -INI_AUTO_GENERATE_BEGIN = u'; ========== AUTO GENERATED CODE BEGIN ===========' -INI_AUTO_GENERATE_END = u'; =========== AUTO GENERATED CODE END ============' +CPP_AUTO_GENERATE_BEGIN = '// ========== AUTO GENERATED CODE BEGIN ===========' +CPP_AUTO_GENERATE_END = '// =========== AUTO GENERATED CODE END ============' +CPP_INCLUDE_BEGIN = '// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========' +CPP_INCLUDE_END = '// ========== AUTO GENERATED INCLUDE BLOCK END ===========' +INI_AUTO_GENERATE_BEGIN = '; ========== AUTO GENERATED CODE BEGIN ===========' +INI_AUTO_GENERATE_END = '; =========== AUTO GENERATED CODE END ============' -CPP_BASE_FORMAT = (u"""// Auto generated code by esphome -""", u"""" +CPP_BASE_FORMAT = ("""// Auto generated code by esphome +""", """" void setup() { // ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== - """, u""" + """, """ // ========= YOU CAN EDIT AFTER THIS LINE ========= App.setup(); } @@ -36,7 +34,7 @@ void loop() { } """) -INI_BASE_FORMAT = (u"""; Auto generated code by esphome +INI_BASE_FORMAT = ("""; Auto generated code by esphome [common] lib_deps = @@ -44,7 +42,7 @@ build_flags = upload_flags = ; ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== -""", u""" +""", """ ; ========= YOU CAN EDIT AFTER THIS LINE ========= """) @@ -62,8 +60,8 @@ def get_flags(key): def get_include_text(): - include_text = u'#include "esphome.h"\n' \ - u'using namespace esphome;\n' + include_text = '#include "esphome.h"\n' \ + 'using namespace esphome;\n' for _, component, conf in iter_components(CORE.config): if not hasattr(component, 'includes'): continue @@ -106,7 +104,7 @@ def migrate_src_version_0_to_1(): if CPP_INCLUDE_BEGIN not in content: content, count = replace_file_content(content, r'#include "esphomelib/application.h"', - CPP_INCLUDE_BEGIN + u'\n' + CPP_INCLUDE_END) + CPP_INCLUDE_BEGIN + '\n' + CPP_INCLUDE_END) if count == 0: _LOGGER.error("Migration failed. ESPHome 1.10.0 needs to have a new auto-generated " "include section in the %s file. Please remove %s and let it be " @@ -160,14 +158,14 @@ def update_storage_json(): def format_ini(data): - content = u'' + content = '' for key, value in sorted(data.items()): if isinstance(value, (list, set, tuple)): - content += u'{} =\n'.format(key) + content += f'{key} =\n' for x in value: - content += u' {}\n'.format(x) + content += f' {x}\n' else: - content += u'{} = {}\n'.format(key, value) + content += f'{key} = {value}\n' return content @@ -216,7 +214,7 @@ def get_ini_content(): # data['lib_ldf_mode'] = 'chain' data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) - content = u'[env:{}]\n'.format(CORE.name) + content = f'[env:{CORE.name}]\n' content += format_ini(data) return content @@ -225,18 +223,18 @@ def get_ini_content(): def find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: - raise EsphomeError(u"Could not find auto generated code begin in file, either " - u"delete the main sketch file or insert the comment again.") + raise EsphomeError("Could not find auto generated code begin in file, either " + "delete the main sketch file or insert the comment again.") if text.find(begin_s, begin_index + 1) != -1: - raise EsphomeError(u"Found multiple auto generate code begins, don't know " - u"which to chose, please remove one of them.") + raise EsphomeError("Found multiple auto generate code begins, don't know " + "which to chose, please remove one of them.") end_index = text.find(end_s) if end_index == -1: - raise EsphomeError(u"Could not find auto generated code end in file, either " - u"delete the main sketch file or insert the comment again.") + raise EsphomeError("Could not find auto generated code end in file, either " + "delete the main sketch file or insert the comment again.") if text.find(end_s, end_index + 1) != -1: - raise EsphomeError(u"Found multiple auto generate code endings, don't know " - u"which to chose, please remove one of them.") + raise EsphomeError("Found multiple auto generate code endings, don't know " + "which to chose, please remove one of them.") return text[:begin_index], text[(end_index + len(end_s)):] @@ -263,17 +261,17 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = u"""\ +DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once {} """ -VERSION_H_FORMAT = u"""\ +VERSION_H_FORMAT = """\ #pragma once #define ESPHOME_VERSION "{}" """ DEFINES_H_TARGET = 'esphome/core/defines.h' VERSION_H_TARGET = 'esphome/core/version.h' -ESPHOME_README_TXT = u""" +ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY ESPHome automatically populates the esphome/ directory, and any @@ -298,9 +296,9 @@ def copy_src_tree(): include_l = [] for target, path in source_files_l: if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS: - include_l.append(u'#include "{}"'.format(target)) - include_l.append(u'') - include_s = u'\n'.join(include_l) + include_l.append(f'#include "{target}"') + include_l.append('') + include_s = '\n'.join(include_l) source_files_copy = source_files.copy() source_files_copy.pop(DEFINES_H_TARGET) @@ -340,7 +338,7 @@ def copy_src_tree(): def generate_defines_h(): define_content_l = [x.as_macro for x in CORE.defines] define_content_l.sort() - return DEFINES_H_FORMAT.format(u'\n'.join(define_content_l)) + return DEFINES_H_FORMAT.format('\n'.join(define_content_l)) def write_cpp(code_s): @@ -354,11 +352,11 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() - global_s = u'#include "esphome.h"\n' + global_s = '#include "esphome.h"\n' global_s += CORE.cpp_global_section - full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END - full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END + full_file = code_format[0] + CPP_INCLUDE_BEGIN + '\n' + global_s + CPP_INCLUDE_END + full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + '\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] write_file_if_changed(path, full_file) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 0e5b4593e9..053fba6274 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import fnmatch import functools import inspect @@ -15,7 +13,6 @@ from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.helpers import add_class_to_obj -from esphome.py_compat import text_type, IS_PY2 from esphome.util import OrderedDict, filter_yaml_files _LOGGER = logging.getLogger(__name__) @@ -23,12 +20,12 @@ _LOGGER = logging.getLogger(__name__) # Mostly copied from Home Assistant because that code works fine and # let's not reinvent the wheel here -SECRET_YAML = u'secrets.yaml' +SECRET_YAML = 'secrets.yaml' _SECRET_CACHE = {} _SECRET_VALUES = {} -class ESPHomeDataBase(object): +class ESPHomeDataBase: @property def esp_range(self): return getattr(self, '_esp_range', None) @@ -38,7 +35,7 @@ class ESPHomeDataBase(object): self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) -class ESPForceValue(object): +class ESPForceValue: pass @@ -74,27 +71,27 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_yaml_int(self, node): - return super(ESPHomeLoader, self).construct_yaml_int(node) + return super().construct_yaml_int(node) @_add_data_ref def construct_yaml_float(self, node): - return super(ESPHomeLoader, self).construct_yaml_float(node) + return super().construct_yaml_float(node) @_add_data_ref def construct_yaml_binary(self, node): - return super(ESPHomeLoader, self).construct_yaml_binary(node) + return super().construct_yaml_binary(node) @_add_data_ref def construct_yaml_omap(self, node): - return super(ESPHomeLoader, self).construct_yaml_omap(node) + return super().construct_yaml_omap(node) @_add_data_ref def construct_yaml_str(self, node): - return super(ESPHomeLoader, self).construct_yaml_str(node) + return super().construct_yaml_str(node) @_add_data_ref def construct_yaml_seq(self, node): - return super(ESPHomeLoader, self).construct_yaml_seq(node) + return super().construct_yaml_seq(node) @_add_data_ref def construct_yaml_map(self, node): @@ -130,12 +127,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors hash(key) except TypeError: raise yaml.constructor.ConstructorError( - 'Invalid key "{}" (not hashable)'.format(key), key_node.start_mark) + f'Invalid key "{key}" (not hashable)', key_node.start_mark) # Check if it is a duplicate key if key in seen_keys: raise yaml.constructor.ConstructorError( - 'Duplicate key "{}"'.format(key), key_node.start_mark, + f'Duplicate key "{key}"', key_node.start_mark, 'NOTE: Previous declaration here:', seen_keys[key], ) seen_keys[key] = key_node.start_mark @@ -194,11 +191,11 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors args = node.value.split() # Check for a default value if len(args) > 1: - return os.getenv(args[0], u' '.join(args[1:])) + return os.getenv(args[0], ' '.join(args[1:])) if args[0] in os.environ: return os.environ[args[0]] raise yaml.MarkedYAMLError( - u"Environment variable '{}' not defined".format(node.value), node.start_mark + f"Environment variable '{node.value}' not defined", node.start_mark ) @property @@ -213,10 +210,10 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) if node.value not in secrets: raise yaml.MarkedYAMLError( - u"Secret '{}' not defined".format(node.value), node.start_mark + f"Secret '{node.value}' not defined", node.start_mark ) val = secrets[node.value] - _SECRET_VALUES[text_type(val)] = node.value + _SECRET_VALUES[str(val)] = node.value return val @_add_data_ref @@ -259,7 +256,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_lambda(self, node): - return Lambda(text_type(node.value)) + return Lambda(str(node.value)) @_add_data_ref def construct_force(self, node): @@ -267,13 +264,13 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return add_class_to_obj(obj, ESPForceValue) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:omap', ESPHomeLoader.construct_yaml_omap) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:str', ESPHomeLoader.construct_yaml_str) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:seq', ESPHomeLoader.construct_yaml_seq) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:map', ESPHomeLoader.construct_yaml_map) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:omap', ESPHomeLoader.construct_yaml_omap) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:str', ESPHomeLoader.construct_yaml_str) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:seq', ESPHomeLoader.construct_yaml_seq) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:map', ESPHomeLoader.construct_yaml_map) ESPHomeLoader.add_constructor('!env_var', ESPHomeLoader.construct_env_var) ESPHomeLoader.add_constructor('!secret', ESPHomeLoader.construct_secret) ESPHomeLoader.add_constructor('!include', ESPHomeLoader.construct_include) @@ -313,7 +310,7 @@ def dump(dict_): def _is_file_valid(name): """Decide if a file is valid.""" - return not name.startswith(u'.') + return not name.startswith('.') def _find_files(directory, pattern): @@ -328,7 +325,7 @@ def _find_files(directory, pattern): def is_secret(value): try: - return _SECRET_VALUES[text_type(value)] + return _SECRET_VALUES[str(value)] except (KeyError, ValueError): return None @@ -358,31 +355,31 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return node def represent_secret(self, value): - return self.represent_scalar(tag=u'!secret', value=_SECRET_VALUES[text_type(value)]) + return self.represent_scalar(tag='!secret', value=_SECRET_VALUES[str(value)]) def represent_stringify(self, value): if is_secret(value): return self.represent_secret(value) - return self.represent_scalar(tag=u'tag:yaml.org,2002:str', value=text_type(value)) + return self.represent_scalar(tag='tag:yaml.org,2002:str', value=str(value)) # pylint: disable=arguments-differ def represent_bool(self, value): - return self.represent_scalar(u'tag:yaml.org,2002:bool', u'true' if value else u'false') + return self.represent_scalar('tag:yaml.org,2002:bool', 'true' if value else 'false') def represent_int(self, value): if is_secret(value): return self.represent_secret(value) - return self.represent_scalar(tag=u'tag:yaml.org,2002:int', value=text_type(value)) + return self.represent_scalar(tag='tag:yaml.org,2002:int', value=str(value)) def represent_float(self, value): if is_secret(value): return self.represent_secret(value) if math.isnan(value): - value = u'.nan' + value = '.nan' elif math.isinf(value): - value = u'.inf' if value > 0 else u'-.inf' + value = '.inf' if value > 0 else '-.inf' else: - value = text_type(repr(value)).lower() + value = str(repr(value)).lower() # Note that in some cases `repr(data)` represents a float number # without the decimal parts. For instance: # >>> repr(1e17) @@ -390,9 +387,9 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors # Unfortunately, this is not a valid float representation according # to the definition of the `!!float` tag. We fix this by adding # '.0' before the 'e' symbol. - if u'.' not in value and u'e' in value: - value = value.replace(u'e', u'.0e', 1) - return self.represent_scalar(tag=u'tag:yaml.org,2002:float', value=value) + if '.' not in value and 'e' in value: + value = value.replace('e', '.0e', 1) + return self.represent_scalar(tag='tag:yaml.org,2002:float', value=value) def represent_lambda(self, value): if is_secret(value.value): @@ -417,9 +414,6 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool) ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) -if IS_PY2: - ESPHomeDumper.add_multi_representer(unicode, ESPHomeDumper.represent_stringify) - ESPHomeDumper.add_multi_representer(long, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 9c8f8eab77..a8ca5b3c53 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -12,8 +12,6 @@ import time import ifaddr -from esphome.py_compat import indexbytes, text_type - log = logging.getLogger(__name__) # Some timing constants @@ -83,7 +81,7 @@ class IncomingDecodeError(Error): # pylint: disable=no-init -class QuietLogger(object): +class QuietLogger: _seen_logs = {} @classmethod @@ -112,7 +110,7 @@ class QuietLogger(object): logger(*args) -class DNSEntry(object): +class DNSEntry: """A DNS entry""" def __init__(self, name, type_, class_): @@ -281,7 +279,7 @@ class DNSIncoming(QuietLogger): def read_utf(self, offset, length): """Reads a UTF-8 string of a given length from the packet""" - return text_type(self.data[offset:offset + length], 'utf-8', 'replace') + return str(self.data[offset:offset + length], 'utf-8', 'replace') def read_name(self): """Reads a domain name from the packet""" @@ -291,7 +289,7 @@ class DNSIncoming(QuietLogger): first = off while True: - length = indexbytes(self.data, off) + length = self.data[off] off += 1 if length == 0: break @@ -302,13 +300,13 @@ class DNSIncoming(QuietLogger): elif t == 0xC0: if next_ < 0: next_ = off + 1 - off = ((length & 0x3F) << 8) | indexbytes(self.data, off) + off = ((length & 0x3F) << 8) | self.data[off] if off >= first: raise IncomingDecodeError( - "Bad domain name (circular) at %s" % (off,)) + f"Bad domain name (circular) at {off}") first = off else: - raise IncomingDecodeError("Bad domain name at %s" % (off,)) + raise IncomingDecodeError(f"Bad domain name at {off}") if next_ >= 0: self.offset = next_ @@ -318,7 +316,7 @@ class DNSIncoming(QuietLogger): return result -class DNSOutgoing(object): +class DNSOutgoing: """Object representation of an outgoing packet""" def __init__(self, flags): @@ -461,7 +459,7 @@ class Engine(threading.Thread): if reader: reader.handle_read(socket_) - except (select.error, socket.error) as e: + except OSError as e: # If the socket was closed by another thread, during # shutdown, ignore it and exit if e.args[0] != socket.EBADF or not self.zc.done: @@ -500,7 +498,7 @@ class Listener(QuietLogger): self.zc.handle_response(msg) -class RecordUpdateListener(object): +class RecordUpdateListener: def update_record(self, zc, now, record): raise NotImplementedError() @@ -578,7 +576,7 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): self.on_update({key: self.host_status(key) for key in self.key_to_host}) def request_query(self, hosts): - self.query_hosts = set(host for host in hosts.values()) + self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() @@ -605,12 +603,12 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): def get_all_addresses(): - return list(set( + return list({ addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4 and addr.network_prefix != 32 # Host only netmask 255.255.255.255 - )) + }) def new_socket(): @@ -631,7 +629,7 @@ def new_socket(): else: try: s.setsockopt(socket.SOL_SOCKET, reuseport, 1) - except (OSError, socket.error) as err: + except OSError as err: # OSError on python 3, socket.error on python 2 if err.errno != errno.ENOPROTOOPT: raise @@ -662,7 +660,7 @@ class Zeroconf(QuietLogger): _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i) self._listen_socket.setsockopt( socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value) - except socket.error as e: + except OSError as e: _errno = e.args[0] if _errno == errno.EADDRINUSE: log.info( diff --git a/pylintrc b/pylintrc index 89cc73656f..c65a9a7cd9 100644 --- a/pylintrc +++ b/pylintrc @@ -25,9 +25,3 @@ disable= stop-iteration-return, no-self-use, import-outside-toplevel, - - -additional-builtins= - unicode, - long, - raw_input diff --git a/requirements.txt b/requirements.txt index b8af2a605b..8c763ef9a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ PyYAML==5.2 paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 -typing>=3.6.6;python_version<"3.5" protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 diff --git a/requirements_test.txt b/requirements_test.txt index ff02badf80..7711b3867a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,6 @@ PyYAML==5.2 paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 -typing>=3.6.6;python_version<"3.5" protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py index 52cbbde678..e690a2c5d7 100644 --- a/script/api_protobuf/api_options_pb2.py +++ b/script/api_protobuf/api_options_pb2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: api_options.proto diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 8373e0bd66..2ecaec10bd 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -39,12 +39,12 @@ content = prot.read_bytes() d = descriptor.FileDescriptorSet.FromString(content) -def indent_list(text, padding=u' '): +def indent_list(text, padding=' '): return [padding + line for line in text.splitlines()] -def indent(text, padding=u' '): - return u'\n'.join(indent_list(text, padding)) +def indent(text, padding=' '): + return '\n'.join(indent_list(text, padding)) def camel_to_snake(name): @@ -432,7 +432,7 @@ class SInt64Type(TypeInfo): class RepeatedTypeInfo(TypeInfo): def __init__(self, field): - super(RepeatedTypeInfo, self).__init__(field) + super().__init__(field) self._ti = TYPE_INFO[field.type](field) @property diff --git a/script/build_compile_commands.py b/script/build_compile_commands.py index 31e4c2fa56..f0fc48ad98 100755 --- a/script/build_compile_commands.py +++ b/script/build_compile_commands.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys import os.path diff --git a/script/ci-custom.py b/script/ci-custom.py index 51b7b9d9b5..b2b838cb5b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -from __future__ import print_function +#!/usr/bin/env python3 import codecs import collections @@ -105,7 +104,7 @@ def lint_re_check(regex, **kwargs): err = func(fname, match) if err is None: continue - errors.append("{} See line {}.".format(err, lineno)) + errors.append(f"{err} See line {lineno}.") return errors return decor(new_func) return decorator @@ -134,7 +133,7 @@ def lint_ino(fname): return "This file extension (.ino) is not allowed. Please use either .cpp or .h" -@lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [ +@lint_file_check(exclude=[f'*{f}' for f in file_types] + [ '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc', 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*', ]) @@ -177,7 +176,7 @@ CPP_RE_EOL = r'\s*?(?://.*?)?$' def highlight(s): - return '\033[36m{}\033[0m'.format(s) + return f'\033[36m{s}\033[0m' @lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, @@ -268,7 +267,7 @@ def lint_constants_usage(): def relative_cpp_search_text(fname, content): parts = fname.split('/') integration = parts[2] - return '#include "esphome/components/{}'.format(integration) + return f'#include "esphome/components/{integration}' @lint_content_find_check(relative_cpp_search_text, include=['esphome/components/*.cpp']) @@ -284,7 +283,7 @@ def lint_relative_cpp_import(fname): def relative_py_search_text(fname, content): parts = fname.split('/') integration = parts[2] - return 'esphome.components.{}'.format(integration) + return f'esphome.components.{integration}' @lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'], @@ -303,7 +302,7 @@ def lint_relative_py_import(fname): def lint_namespace(fname, content): expected_name = re.match(r'^esphome/components/([^/]+)/.*', fname.replace(os.path.sep, '/')).group(1) - search = 'namespace {}'.format(expected_name) + search = f'namespace {expected_name}' if search in content: return None return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ @@ -380,7 +379,7 @@ for fname in files: run_checks(LINT_POST_CHECKS, 'POST') for f, errs in sorted(errors.items()): - print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) + print(f"\033[0;32m************* File \033[1;32m{f}\033[0m") for err in errs: print(err) print() diff --git a/script/clang-format b/script/clang-format index 89a5acd746..e9c3692bb8 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function diff --git a/script/clang-tidy b/script/clang-tidy index f178e036b1..1005d15580 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function diff --git a/script/helpers.py b/script/helpers.py index 243dfde49e..c9bf5224b1 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -12,11 +12,11 @@ temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp') def shlex_quote(s): if not s: - return u"''" + return "''" if re.search(r'[^\w@%+=:,./-]', s) is None: return s - return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + return "'" + s.replace("'", "'\"'\"'") + "'" def build_all_include(): @@ -29,7 +29,7 @@ def build_all_include(): if ext in filetypes: path = os.path.relpath(path, root_path) include_p = path.replace(os.path.sep, '/') - headers.append('#include "{}"'.format(include_p)) + headers.append(f'#include "{include_p}"') headers.sort() headers.append('') content = '\n'.join(headers) @@ -47,7 +47,7 @@ def build_compile_commands(): gcc_flags = json.load(f) exec_path = gcc_flags['execPath'] include_paths = gcc_flags['gccIncludePaths'].split(',') - includes = ['-I{}'.format(p) for p in include_paths] + includes = [f'-I{p}' for p in include_paths] cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ') defines = [flag for flag in cpp_flags if flag.startswith('-D')] command = [exec_path] @@ -102,7 +102,7 @@ def splitlines_no_ends(string): def changed_files(): for remote in ('upstream', 'origin'): - command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD'] + command = ['git', 'merge-base', f'{remote}/dev', 'HEAD'] try: merge_base = splitlines_no_ends(get_output(*command))[0] break @@ -124,7 +124,7 @@ def filter_changed(files): if not files: print(" No changed files!") for c in files: - print(" {}".format(c)) + print(f" {c}") return files diff --git a/script/lint-python b/script/lint-python index 3fbc329ab0..4915115262 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function @@ -61,7 +61,7 @@ def main(): continue file_ = line[0] linno = line[1] - msg = (u':'.join(line[3:])).strip() + msg = (':'.join(line[3:])).strip() print_error(file_, linno, msg) errors += 1 @@ -74,7 +74,7 @@ def main(): continue file_ = line[0] linno = line[1] - msg = (u':'.join(line[2:])).strip() + msg = (':'.join(line[2:])).strip() print_error(file_, linno, msg) errors += 1 diff --git a/setup.cfg b/setup.cfg index bef998fb37..ab43acfbc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ Topic :: Home Automation [flake8] max-line-length = 120 -builtins = unicode, long, raw_input, basestring exclude = api_pb2.py [bdist_wheel] diff --git a/setup.py b/setup.py index 1d4fb3a4d0..c520b949fc 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """esphome setup script.""" from setuptools import setup, find_packages import os @@ -28,7 +28,6 @@ REQUIRES = [ 'paho-mqtt==1.5.0', 'colorlog==4.0.2', 'tornado==5.1.1', - 'typing>=3.6.6;python_version<"3.6"', 'protobuf==3.11.1', 'tzlocal==2.0.0', 'pytz==2019.3', @@ -69,7 +68,7 @@ setup( zip_safe=False, platforms='any', test_suite='tests', - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4.0', + python_requires='>=3.6,<4.0', install_requires=REQUIRES, keywords=['home', 'automation'], entry_points={ From ae784dc74c4c51516f3851615888b4a09c472af7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 7 Dec 2019 18:53:20 +0100 Subject: [PATCH 125/412] Fix CI --- esphome/core/component.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 6297013247..f4151a14fc 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -139,19 +139,19 @@ float Component::get_actual_setup_priority() const { } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpmf-conversions" bool Component::has_overridden_loop() const { #ifdef CLANG_TIDY bool loop_overridden = true; bool call_loop_overridden = true; #else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpmf-conversions" bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); +#pragma GCC diagnostic pop #endif return loop_overridden || call_loop_overridden; } -#pragma GCC diagnostic pop PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} From f5b7cc81d8c75fdb6ec045eeca38f619849d280b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 10 Dec 2019 23:09:35 +1300 Subject: [PATCH 126/412] Add RFBridge component (#896) * Add RFBridge component * Fix format issues * Rename methods * More formatting * Fix line length * Apply suggestions from code review Co-Authored-By: Otto Winter * Check uart settings on dump * Make receiving local to the loop * FIx code order and schema * Add rf_bridge to test file * Apply suggestions from code review Co-Authored-By: Otto Winter --- esphome/components/rf_bridge/__init__.py | 75 ++++++++++++++++ esphome/components/rf_bridge/rf_bridge.cpp | 100 +++++++++++++++++++++ esphome/components/rf_bridge/rf_bridge.h | 95 ++++++++++++++++++++ tests/test3.yaml | 17 +++- 4 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 esphome/components/rf_bridge/__init__.py create mode 100644 esphome/components/rf_bridge/rf_bridge.cpp create mode 100644 esphome/components/rf_bridge/rf_bridge.h diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py new file mode 100644 index 0000000000..1fd4fbc7bd --- /dev/null +++ b/esphome/components/rf_bridge/__init__.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SYNC, CONF_HIGH +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') +RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) + +RFBridgeData = rf_bridge_ns.struct('RFBridgeData') + +RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_('RFBridgeReceivedCodeTrigger', + automation.Trigger.template(RFBridgeData)) + +RFBridgeSendCodeAction = rf_bridge_ns.class_('RFBridgeSendCodeAction', automation.Action) +RFBridgeLearnAction = rf_bridge_ns.class_('RFBridgeLearnAction', automation.Action) + + +CONF_ON_CODE_RECEIVED = 'on_code_received' + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(RFBridgeComponent), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RFBridgeReceivedCodeTrigger), + }), +}).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_CODE_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(RFBridgeData, 'data')], conf) + + +RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_SYNC): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_LOW): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_HIGH): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_CODE): cv.templatable(cv.hex_uint32_t) +}) + + +@automation.register_action('rf_bridge.send_code', RFBridgeSendCodeAction, + RFBRIDGE_SEND_CODE_SCHEMA) +def rf_bridge_send_code_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = yield cg.templatable(config[CONF_SYNC], args, cg.uint16) + cg.add(var.set_sync(template_)) + template_ = yield cg.templatable(config[CONF_LOW], args, cg.uint16) + cg.add(var.set_low(template_)) + template_ = yield cg.templatable(config[CONF_HIGH], args, cg.uint16) + cg.add(var.set_high(template_)) + template_ = yield cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_code(template_)) + yield var + + +RFBRIDGE_LEARN_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(RFBridgeComponent) +}) + + +@automation.register_action('rf_bridge.learn', RFBridgeLearnAction, RFBRIDGE_LEARN_SCHEMA) +def rf_bridge_learnx_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + yield var diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp new file mode 100644 index 0000000000..f1537cdc87 --- /dev/null +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -0,0 +1,100 @@ +#include "rf_bridge.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace rf_bridge { + +static const char *TAG = "rf_bridge"; + +void RFBridgeComponent::ack_() { + ESP_LOGV(TAG, "Sending ACK"); + this->write(RF_CODE_START); + this->write(RF_CODE_ACK); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::decode_() { + uint8_t action = uartbuf_[0]; + RFBridgeData data{}; + + switch (action) { + case RF_CODE_ACK: + ESP_LOGD(TAG, "Action OK"); + break; + case RF_CODE_LEARN_KO: + this->ack_(); + ESP_LOGD(TAG, "Learn timeout"); + break; + case RF_CODE_LEARN_OK: + ESP_LOGD(TAG, "Learn started"); + case RF_CODE_RFIN: + this->ack_(); + + data.sync = (uartbuf_[1] << 8) | uartbuf_[2]; + data.low = (uartbuf_[3] << 8) | uartbuf_[4]; + data.high = (uartbuf_[5] << 8) | uartbuf_[6]; + data.code = (uartbuf_[7] << 16) | (uartbuf_[8] << 8) | uartbuf_[9]; + + ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, + data.high, data.code); + this->callback_.call(data); + break; + default: + ESP_LOGD(TAG, "Unknown action: 0x%02X", action); + break; + } + this->last_ = millis(); +} + +void RFBridgeComponent::loop() { + bool receiving = false; + if (this->last_ != 0 && millis() - this->last_ > RF_DEBOUNCE) { + this->last_ = 0; + } + + while (this->available()) { + uint8_t c = this->read(); + if (receiving) { + if (c == RF_CODE_STOP && (this->uartpos_ == 1 || this->uartpos_ == RF_MESSAGE_SIZE + 1)) { + this->decode_(); + receiving = false; + } else if (this->uartpos_ <= RF_MESSAGE_SIZE) { + this->uartbuf_[uartpos_++] = c; + } else { + receiving = false; + } + } else if (c == RF_CODE_START) { + this->uartpos_ = 0; + receiving = true; + } + } +} + +void RFBridgeComponent::send_code(RFBridgeData data) { + ESP_LOGD(TAG, "Sending code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, + data.code); + this->write(RF_CODE_START); + this->write(RF_CODE_RFOUT); + this->write(data.sync); + this->write(data.low); + this->write(data.high); + this->write(data.code); + this->write(RF_CODE_STOP); +} + +void RFBridgeComponent::learn() { + ESP_LOGD(TAG, "Learning mode"); + this->write(RF_CODE_START); + this->write(RF_CODE_LEARN); + this->write(RF_CODE_STOP); +} + +void RFBridgeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "RF_Bridge:"); + this->check_uart_settings(19200); +} + +} // namespace rf_bridge +} // namespace esphome diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h new file mode 100644 index 0000000000..86713b8a5c --- /dev/null +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace rf_bridge { + +static const uint8_t RF_MESSAGE_SIZE = 9; +static const uint8_t RF_CODE_START = 0xAA; +static const uint8_t RF_CODE_ACK = 0xA0; +static const uint8_t RF_CODE_LEARN = 0xA1; +static const uint8_t RF_CODE_LEARN_KO = 0xA2; +static const uint8_t RF_CODE_LEARN_OK = 0xA3; +static const uint8_t RF_CODE_RFIN = 0xA4; +static const uint8_t RF_CODE_RFOUT = 0xA5; +static const uint8_t RF_CODE_SNIFFING_ON = 0xA6; +static const uint8_t RF_CODE_SNIFFING_OFF = 0xA7; +static const uint8_t RF_CODE_RFOUT_NEW = 0xA8; +static const uint8_t RF_CODE_LEARN_NEW = 0xA9; +static const uint8_t RF_CODE_LEARN_KO_NEW = 0xAA; +static const uint8_t RF_CODE_LEARN_OK_NEW = 0xAB; +static const uint8_t RF_CODE_RFOUT_BUCKET = 0xB0; +static const uint8_t RF_CODE_STOP = 0x55; +static const uint8_t RF_DEBOUNCE = 200; + +struct RFBridgeData { + uint16_t sync; + uint16_t low; + uint16_t high; + uint32_t code; +}; + +class RFBridgeComponent : public uart::UARTDevice, public Component { + public: + void loop() override; + void dump_config() override; + void add_on_code_received_callback(std::function callback) { + this->callback_.add(std::move(callback)); + } + void send_code(RFBridgeData data); + void learn(); + + protected: + void ack_(); + void decode_(); + + unsigned long last_ = 0; + unsigned char uartbuf_[RF_MESSAGE_SIZE + 3] = {0}; + unsigned char uartpos_ = 0; + + CallbackManager callback_; +}; + +class RFBridgeReceivedCodeTrigger : public Trigger { + public: + explicit RFBridgeReceivedCodeTrigger(RFBridgeComponent *parent) { + parent->add_on_code_received_callback([this](RFBridgeData data) { this->trigger(data); }); + } +}; + +template class RFBridgeSendCodeAction : public Action { + public: + RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint16_t, sync) + TEMPLATABLE_VALUE(uint16_t, low) + TEMPLATABLE_VALUE(uint16_t, high) + TEMPLATABLE_VALUE(uint32_t, code) + + void play(Ts... x) { + RFBridgeData data{}; + data.sync = this->sync_.value(x...); + data.low = this->low_.value(x...); + data.high = this->high_.value(x...); + data.code = this->code_.value(x...); + this->parent_->send_code(data); + } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeLearnAction : public Action { + public: + RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->learn(); } + + protected: + RFBridgeComponent *parent_; +}; + +} // namespace rf_bridge +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 2703621752..77a0fa631c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -623,7 +623,7 @@ output: mcp23017: id: mcp23017_hub - + mcp23008: id: mcp23008_hub @@ -666,8 +666,21 @@ dfplayer: dfplayer.is_playing then: logger.log: 'Playback finished event' - tm1651: id: tm1651_battery clk_pin: D6 dio_pin: D5 +rf_bridge: + on_code_received: + - lambda: |- + uint32_t test; + test = data.sync; + test = data.low; + test = data.high; + test = data.code; + - rf_bridge.send_code: + sync: 0x1234 + low: 0x1234 + high: 0x1234 + code: 0x123456 + - rf_bridge.learn From c8ccb06f11e28c035f2d340444cfb3b3e49110ff Mon Sep 17 00:00:00 2001 From: Andrew Zaborowski Date: Tue, 17 Dec 2019 12:08:37 +0100 Subject: [PATCH 127/412] ct_clamp: Check sample() return value is not NaN (#921) Don't try to update CT clamp's state with NaN values returned from the underlaying sensor. A single IO error in the sensor code will cause a NaN to be returned and if we use that in CTClampSensor's floating point maths both sample_sum_ and offset_ will become NaN and from there every future calculation will use the NaN offset_ and return NaN too. --- esphome/components/ct_clamp/ct_clamp_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 674cc0ae98..c1e3bec486 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -64,6 +64,8 @@ void CTClampSensor::loop() { // Perform a single sample float value = this->source_->sample(); + if (isnan(value)) + return; if (this->is_calibrating_offset_) { this->sample_sum_ += value; From eea78531a10109cba691d8f3c08478d3ec9220ff Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 30 Dec 2019 22:02:55 -0300 Subject: [PATCH 128/412] Climate Mitsubishi (#725) * add climate * Mitsubishi updates * refactor mitsubishi to use climate_ir * lint --- esphome/components/climate_ir/climate_ir.cpp | 1 + esphome/components/climate_ir/climate_ir.h | 3 + esphome/components/mitsubishi/__init__.py | 0 esphome/components/mitsubishi/climate.py | 18 +++++ esphome/components/mitsubishi/mitsubishi.cpp | 84 ++++++++++++++++++++ esphome/components/mitsubishi/mitsubishi.h | 22 +++++ 6 files changed, 128 insertions(+) create mode 100644 esphome/components/mitsubishi/__init__.py create mode 100644 esphome/components/mitsubishi/climate.py create mode 100644 esphome/components/mitsubishi/mitsubishi.cpp create mode 100644 esphome/components/mitsubishi/mitsubishi.h diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 8f06ff2214..92a5b2423a 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -116,6 +116,7 @@ void ClimateIR::dump_config() { ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); } +bool ClimateIR::on_receive(remote_base::RemoteReceiveData data) { return false; } } // namespace climate_ir } // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 7a69b19786..82bf4247b0 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -63,6 +63,9 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; + + /// Handle received IR Buffer, so it is optional to implement + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace climate_ir diff --git a/esphome/components/mitsubishi/__init__.py b/esphome/components/mitsubishi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py new file mode 100644 index 0000000000..933e53baf0 --- /dev/null +++ b/esphome/components/mitsubishi/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +mitsubishi_ns = cg.esphome_ns.namespace('mitsubishi') +MitsubishiClimate = mitsubishi_ns.class_('MitsubishiClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MitsubishiClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp new file mode 100644 index 0000000000..b70aa6d394 --- /dev/null +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -0,0 +1,84 @@ +#include "mitsubishi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mitsubishi { + +static const char *TAG = "mitsubishi.climate"; + +const uint32_t MITSUBISHI_OFF = 0x00; + +const uint8_t MITSUBISHI_COOL = 0x18; +const uint8_t MITSUBISHI_DRY = 0x10; +const uint8_t MITSUBISHI_AUTO = 0x20; +const uint8_t MITSUBISHI_HEAT = 0x08; +const uint8_t MITSUBISHI_FAN_AUTO = 0x00; + +// Pulse parameters in usec +const uint16_t MITSUBISHI_BIT_MARK = 430; +const uint16_t MITSUBISHI_ONE_SPACE = 1250; +const uint16_t MITSUBISHI_ZERO_SPACE = 390; +const uint16_t MITSUBISHI_HEADER_MARK = 3500; +const uint16_t MITSUBISHI_HEADER_SPACE = 1700; +const uint16_t MITSUBISHI_MIN_GAP = 17500; + +void MitsubishiClimate::transmit_state() { + uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x48, 0x00, 0x30, + 0x58, 0x61, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00}; + + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state[6] = MITSUBISHI_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[6] = MITSUBISHI_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state[6] = MITSUBISHI_AUTO; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state[5] = MITSUBISHI_OFF; + break; + } + + remote_state[7] = + (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN); + + ESP_LOGV(TAG, "Sending Mitsubishi target temp: %.1f state: %02X mode: %02X temp: %02X", this->target_temperature, + remote_state[5], remote_state[6], remote_state[7]); + + // Checksum + for (int i = 0; i < 17; i++) { + remote_state[17] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + // repeat twice + for (uint16_t r = 0; r < 2; r++) { + // Header + data->mark(MITSUBISHI_HEADER_MARK); + data->space(MITSUBISHI_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(MITSUBISHI_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? MITSUBISHI_ONE_SPACE : MITSUBISHI_ZERO_SPACE); + } + // Footer + if (r == 0) { + data->mark(MITSUBISHI_BIT_MARK); + data->space(MITSUBISHI_MIN_GAP); // Pause before repeating + } + } + data->mark(MITSUBISHI_BIT_MARK); + + transmit.perform(); +} + +} // namespace mitsubishi +} // namespace esphome diff --git a/esphome/components/mitsubishi/mitsubishi.h b/esphome/components/mitsubishi/mitsubishi.h new file mode 100644 index 0000000000..e6bd7b8ebe --- /dev/null +++ b/esphome/components/mitsubishi/mitsubishi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace mitsubishi { + +// Temperature +const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius +const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius + +class MitsubishiClimate : public climate_ir::ClimateIR { + public: + MitsubishiClimate() : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) {} + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; +}; + +} // namespace mitsubishi +} // namespace esphome From 828e2915384e8e0b861bbe814d20104568f9e24d Mon Sep 17 00:00:00 2001 From: Wilmar den Ouden Date: Tue, 31 Dec 2019 03:23:03 +0100 Subject: [PATCH 129/412] fix: only decode when not str already (#923) Signed-off-by: wilmardo --- esphome/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/util.py b/esphome/util.py index 6677946b01..10c8d4e581 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -118,10 +118,11 @@ class RedirectText: # str # If the conversion fails, we will create an exception, which is okay because we won't # be able to print it anyway. - text = s.decode() + if not isinstance(s, str): + s = s.decode() if self._filter_pattern is not None: - self._line_buffer += text + self._line_buffer += s lines = self._line_buffer.splitlines(True) for line in lines: if '\n' not in line and '\r' not in line: @@ -138,7 +139,7 @@ class RedirectText: self._write_color_replace(line) else: - self._write_color_replace(text) + self._write_color_replace(s) # write() returns the number of characters written # Let's print the number of characters of the original string in order to not confuse From 420c86042443daff4a075b8fedde80c65f37cc03 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 31 Dec 2019 00:20:11 -0300 Subject: [PATCH 130/412] fix climate-ir bad merge (#935) * fix climate-ir bad merge * add mitshubishi test --- esphome/components/climate_ir/climate_ir.cpp | 1 - esphome/components/climate_ir/climate_ir.h | 3 --- tests/test1.yaml | 3 ++- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 92a5b2423a..8f06ff2214 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -116,7 +116,6 @@ void ClimateIR::dump_config() { ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); } -bool ClimateIR::on_receive(remote_base::RemoteReceiveData data) { return false; } } // namespace climate_ir } // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 82bf4247b0..7a69b19786 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -63,9 +63,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; - - /// Handle received IR Buffer, so it is optional to implement - bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace climate_ir diff --git a/tests/test1.yaml b/tests/test1.yaml index 1080339b67..043fafc9c3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1192,7 +1192,8 @@ climate: name: Fujitsu General Climate - platform: yashima name: Yashima Climate - + - platform: mitsubishi + name: Mitsubishi switch: - platform: gpio From 7c870556c6204f5acfa52486727e5a708fd61631 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Tue, 31 Dec 2019 14:40:13 +0300 Subject: [PATCH 131/412] http_request: fix memory allocation (#916) --- esphome/components/http_request/http_request.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 4f772d6826..d26899c0db 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -28,6 +28,7 @@ class HttpRequestComponent : public Component { #ifdef ARDUINO_ARCH_ESP8266 this->wifi_client_ = new BearSSL::WiFiClientSecure(); this->wifi_client_->setInsecure(); + this->wifi_client_->setBufferSizes(512, 512); #endif } void dump_config() override; From 05f9dede702f959892493bed9d8ae75797710587 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Tue, 31 Dec 2019 14:40:20 +0300 Subject: [PATCH 132/412] http_request version fix (#917) --- esphome/components/http_request/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 897440a454..ea12b0657d 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -4,7 +4,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ - CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_0 + CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_1 from esphome.core import CORE, Lambda from esphome.core_config import PLATFORMIO_ESP8266_LUT @@ -35,8 +35,8 @@ def validate_framework(config): return config framework = PLATFORMIO_ESP8266_LUT[version] if version in PLATFORMIO_ESP8266_LUT else version - if framework < ARDUINO_VERSION_ESP8266_2_5_0: - raise cv.Invalid('This component is not supported on arduino framework version below 2.5.0') + if framework < ARDUINO_VERSION_ESP8266_2_5_1: + raise cv.Invalid('This component is not supported on arduino framework version below 2.5.1') return config From a6d31f05ee900eeddddd82be940af43c40e33268 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 4 Jan 2020 12:43:11 +0100 Subject: [PATCH 133/412] PID Climate (#885) * PID Climate * Add sensor for debugging PID output value * Add dump_config, use percent * Add more observable values * Update * Set target temperature * Add autotuner * Add algorithm explanation * Add autotuner action, update controller * Add simulator * Format * Change defaults * Updates --- esphome/components/pid/__init__.py | 0 esphome/components/pid/climate.py | 79 ++++ esphome/components/pid/pid_autotuner.cpp | 358 ++++++++++++++++++ esphome/components/pid/pid_autotuner.h | 110 ++++++ esphome/components/pid/pid_climate.cpp | 152 ++++++++ esphome/components/pid/pid_climate.h | 94 +++++ esphome/components/pid/pid_controller.h | 79 ++++ esphome/components/pid/pid_simulator.h | 75 ++++ esphome/components/pid/sensor/__init__.py | 36 ++ .../pid/sensor/pid_climate_sensor.cpp | 47 +++ .../pid/sensor/pid_climate_sensor.h | 34 ++ 11 files changed, 1064 insertions(+) create mode 100644 esphome/components/pid/__init__.py create mode 100644 esphome/components/pid/climate.py create mode 100644 esphome/components/pid/pid_autotuner.cpp create mode 100644 esphome/components/pid/pid_autotuner.h create mode 100644 esphome/components/pid/pid_climate.cpp create mode 100644 esphome/components/pid/pid_climate.h create mode 100644 esphome/components/pid/pid_controller.h create mode 100644 esphome/components/pid/pid_simulator.h create mode 100644 esphome/components/pid/sensor/__init__.py create mode 100644 esphome/components/pid/sensor/pid_climate_sensor.cpp create mode 100644 esphome/components/pid/sensor/pid_climate_sensor.h diff --git a/esphome/components/pid/__init__.py b/esphome/components/pid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py new file mode 100644 index 0000000000..a3e2299296 --- /dev/null +++ b/esphome/components/pid/climate.py @@ -0,0 +1,79 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor, output +from esphome.const import CONF_ID, CONF_SENSOR + +pid_ns = cg.esphome_ns.namespace('pid') +PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) +PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) + +CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' + +CONF_KP = 'kp' +CONF_KI = 'ki' +CONF_KD = 'kd' +CONF_CONTROL_PARAMETERS = 'control_parameters' +CONF_COOL_OUTPUT = 'cool_output' +CONF_HEAT_OUTPUT = 'heat_output' +CONF_NOISEBAND = 'noiseband' +CONF_POSITIVE_OUTPUT = 'positive_output' +CONF_NEGATIVE_OUTPUT = 'negative_output' +CONF_MIN_INTEGRAL = 'min_integral' +CONF_MAX_INTEGRAL = 'max_integral' + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PIDClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, + cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), + cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema({ + cv.Required(CONF_KP): cv.float_, + cv.Optional(CONF_KI, default=0.0): cv.float_, + cv.Optional(CONF_KD, default=0.0): cv.float_, + cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, + cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, + }), +}), cv.has_at_least_one_key(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + + if CONF_COOL_OUTPUT in config: + out = yield cg.get_variable(config[CONF_COOL_OUTPUT]) + cg.add(var.set_cool_output(out)) + if CONF_HEAT_OUTPUT in config: + out = yield cg.get_variable(config[CONF_HEAT_OUTPUT]) + cg.add(var.set_heat_output(out)) + params = config[CONF_CONTROL_PARAMETERS] + cg.add(var.set_kp(params[CONF_KP])) + cg.add(var.set_ki(params[CONF_KI])) + cg.add(var.set_kd(params[CONF_KD])) + if CONF_MIN_INTEGRAL in params: + cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL])) + if CONF_MAX_INTEGRAL in params: + cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL])) + + cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) + + +@automation.register_action('climate.pid.autotune', PIDAutotuneAction, automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, + cv.Optional(CONF_POSITIVE_OUTPUT, default=1.0): cv.possibly_negative_percentage, + cv.Optional(CONF_NEGATIVE_OUTPUT, default=-1.0): cv.possibly_negative_percentage, +})) +def esp8266_set_frequency_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_noiseband(config[CONF_NOISEBAND])) + cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT])) + cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT])) + yield var diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp new file mode 100644 index 0000000000..e8b006b8d7 --- /dev/null +++ b/esphome/components/pid/pid_autotuner.cpp @@ -0,0 +1,358 @@ +#include "pid_autotuner.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.autotune"; + +/* + * # PID Autotuner + * + * Autotuning of PID parameters is a very interesting topic. There has been + * a lot of research over the years to create algorithms that can efficiently determine + * suitable starting PID parameters. + * + * The most basic approach is the Ziegler-Nichols method, which can determine good PID parameters + * in a manual process: + * - Set ki, kd to zero. + * - Increase kp until the output oscillates *around* the setpoint. This value kp is called the + * "ultimate gain" K_u. + * - Additionally, record the period of the observed oscillation as P_u (also called T_u). + * - suitable PID parameters are then: kp=0.6*K_u, ki=1.2*K_u/P_u, kd=0.075*K_u*P_u (additional variants of + * these "magic" factors exist as well [2]). + * + * Now we'd like to automate that process to get K_u and P_u without the user. So we'd like to somehow + * make the observed variable oscillate. One observation is that in many applications of PID controllers + * the observed variable has some amount of "delay" to the output value (think heating an object, it will + * take a few seconds before the sensor can sense the change of temperature) [3]. + * + * It turns out one way to induce such an oscillation is by using a really dumb heating controller: + * When the observed value is below the setpoint, heat at 100%. If it's below, cool at 100% (or disable heating). + * We call this the "RelayFunction" - the class is responsible for making the observed value oscillate around the + * setpoint. We actually use a hysteresis filter (like the bang bang controller) to make the process immune to + * noise in the input data, but the math is the same [1]. + * + * Next, now that we have induced an oscillation, we want to measure the frequency (or period) of oscillation. + * This is what "OscillationFrequencyDetector" is for: it records zerocrossing events (when the observed value + * crosses the setpoint). From that data, we can determine the average oscillating period. This is the P_u of the + * ZN-method. + * + * Finally, we need to determine K_u, the ultimate gain. It turns out we can calculate this based on the amplitude of + * oscillation ("induced amplitude `a`) as described in [1]: + * K_u = (4d) / (πa) + * where d is the magnitude of the relay function (in range -d to +d). + * To measure `a`, we look at the current phase the relay function is in - if it's in the "heating" phase, then we + * expect the lowest temperature (=highest error) to be found in the phase because the peak will always happen slightly + * after the relay function has changed state (assuming a delay-dominated process). + * + * Finally, we use some heuristics to determine if the data we've received so far is good: + * - First, of course we must have enough data to calculate the values. + * - The ZC events need to happen at a relatively periodic rate. If the heating/cooling speeds are very different, + * I've observed the ZN parameters are not very useful. + * - The induced amplitude should not deviate too much. If the amplitudes deviate too much this means there has + * been some outside influence (or noise) on the system, and the measured amplitude values are not reliable. + * + * There are many ways this method can be improved, but on my simulation data the current method already produces very + * good results. Some ideas for future improvements: + * - Relay Function improvements: + * - Integrator, Preload, Saturation Relay ([1]) + * - Use phase of measured signal relative to relay function. + * - Apply PID parameters from ZN, but continuously tweak them in a second step. + * + * [1]: https://warwick.ac.uk/fac/cross_fac/iatl/reinvention/archive/volume5issue2/hornsey/ + * [2]: http://www.mstarlabs.com/control/znrule.html + * [3]: https://www.academia.edu/38620114/SEBORG_3rd_Edition_Process_Dynamics_and_Control + */ + +PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float process_variable) { + PIDAutotuner::PIDAutotuneResult res; + if (this->state_ == AUTOTUNE_SUCCEEDED) { + res.result_params = this->get_ziegler_nichols_pid_(); + return res; + } + + if (!isnan(this->setpoint_) && this->setpoint_ != setpoint) { + ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!"); + } + this->setpoint_ = setpoint; + + float error = setpoint - process_variable; + const uint32_t now = millis(); + + float output = this->relay_function_.update(error); + this->frequency_detector_.update(now, error); + this->amplitude_detector_.update(error, this->relay_function_.state); + res.output = output; + + if (!this->frequency_detector_.has_enough_data() || !this->amplitude_detector_.has_enough_data()) { + // not enough data for calculation yet + ESP_LOGV(TAG, " Not enough data yet for aututuner"); + return res; + } + + bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + if (!zc_symmetrical || !amplitude_convergent) { + // The frequency/amplitude is not fully accurate yet, try to wait + // until the fault clears, or terminate after a while anyway + if (zc_symmetrical) { + ESP_LOGVV(TAG, " ZC is not symmetrical"); + } + if (amplitude_convergent) { + ESP_LOGVV(TAG, " Amplitude is not convergent"); + } + uint32_t phase = this->relay_function_.phase_count; + ESP_LOGVV(TAG, " Phase %u, enough=%u", phase, enough_data_phase_); + + if (this->enough_data_phase_ == 0) { + this->enough_data_phase_ = phase; + } else if (phase - this->enough_data_phase_ <= 6) { + // keep trying for at least 6 more phases + return res; + } else { + // proceed to calculating PID parameters + // warning will be shown in "Checks" section + } + } + + ESP_LOGI(TAG, "PID Autotune finished!"); + + float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude(); + float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f; + ESP_LOGVV(TAG, " Relay magnitude: %f", d); + this->ku_ = 4.0f * d / float(M_PI * osc_ampl); + this->pu_ = this->frequency_detector_.get_mean_oscillation_period(); + + this->state_ = AUTOTUNE_SUCCEEDED; + res.result_params = this->get_ziegler_nichols_pid_(); + this->dump_config(); + + return res; +} +void PIDAutotuner::dump_config() { + ESP_LOGI(TAG, "PID Autotune:"); + if (this->state_ == AUTOTUNE_SUCCEEDED) { + ESP_LOGI(TAG, " State: Succeeded!"); + bool has_issue = false; + if (!this->amplitude_detector_.is_amplitude_convergent()) { + ESP_LOGW(TAG, " Could not reliable determine oscillation amplitude, PID parameters may be inaccurate!"); + ESP_LOGW(TAG, " Please make sure you eliminate all outside influences on the measured temperature."); + has_issue = true; + } + if (!this->frequency_detector_.is_increase_decrease_symmetrical()) { + ESP_LOGW(TAG, " Oscillation Frequency is not symmetrical. PID parameters may be inaccurate!"); + ESP_LOGW( + TAG, + " This is usually because the heat and cool processes do not change the temperature at the same rate."); + ESP_LOGW(TAG, + " Please try reducing the positive_output value (or increase negative_output in case of a cooler)"); + has_issue = true; + } + if (!has_issue) { + ESP_LOGI(TAG, " All checks passed!"); + } + + auto fac = get_ziegler_nichols_pid_(); + ESP_LOGI(TAG, " Calculated PID parameters (\"Ziegler-Nichols PID\" rule):"); + ESP_LOGI(TAG, " "); + ESP_LOGI(TAG, " control_parameters:"); + ESP_LOGI(TAG, " kp: %.5f", fac.kp); + ESP_LOGI(TAG, " ki: %.5f", fac.ki); + ESP_LOGI(TAG, " kd: %.5f", fac.kd); + ESP_LOGI(TAG, " "); + ESP_LOGI(TAG, " Please copy these values into your YAML configuration! They will reset on the next reboot."); + + ESP_LOGV(TAG, " Oscillation Period: %f", this->frequency_detector_.get_mean_oscillation_period()); + ESP_LOGV(TAG, " Oscillation Amplitude: %f", this->amplitude_detector_.get_mean_oscillation_amplitude()); + ESP_LOGV(TAG, " Ku: %f, Pu: %f", this->ku_, this->pu_); + + ESP_LOGD(TAG, " Alternative Rules:"); + // http://www.mstarlabs.com/control/znrule.html + print_rule_("Ziegler-Nichols PI", 0.45f, 0.54f, 0.0f); + print_rule_("Pessen Integral PID", 0.7f, 1.75f, 0.105f); + print_rule_("Some Overshoot PID", 0.333f, 0.667f, 0.111f); + print_rule_("No Overshoot PID", 0.2f, 0.4f, 0.0625f); + } + + if (this->state_ == AUTOTUNE_RUNNING) { + ESP_LOGI(TAG, " Autotune is still running!"); + ESP_LOGD(TAG, " Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error()); + ESP_LOGD(TAG, " Stats so far:"); + ESP_LOGD(TAG, " Phases: %u", relay_function_.phase_count); + ESP_LOGD(TAG, " Detected %u zero-crossings", frequency_detector_.zerocrossing_intervals.size()); // NOLINT + ESP_LOGD(TAG, " Current Phase Min: %.2f, Max: %.2f", amplitude_detector_.phase_min, + amplitude_detector_.phase_max); + } +} +PIDAutotuner::PIDResult PIDAutotuner::calculate_pid_(float kp_factor, float ki_factor, float kd_factor) { + float kp = kp_factor * ku_; + float ki = ki_factor * ku_ / pu_; + float kd = kd_factor * ku_ * pu_; + return { + .kp = kp, + .ki = ki, + .kd = kd, + }; +} +void PIDAutotuner::print_rule_(const char *name, float kp_factor, float ki_factor, float kd_factor) { + auto fac = calculate_pid_(kp_factor, ki_factor, kd_factor); + ESP_LOGD(TAG, " Rule '%s':", name); + ESP_LOGD(TAG, " kp: %.5f, ki: %.5f, kd: %.5f", fac.kp, fac.ki, fac.kd); +} + +// ================== RelayFunction ================== +float PIDAutotuner::RelayFunction::update(float error) { + if (this->state == RELAY_FUNCTION_INIT) { + bool pos = error > this->noiseband; + state = pos ? RELAY_FUNCTION_POSITIVE : RELAY_FUNCTION_NEGATIVE; + } + bool change = false; + if (this->state == RELAY_FUNCTION_POSITIVE && error < -this->noiseband) { + // Positive hysteresis reached, change direction + this->state = RELAY_FUNCTION_NEGATIVE; + change = true; + } else if (this->state == RELAY_FUNCTION_NEGATIVE && error > this->noiseband) { + // Negative hysteresis reached, change direction + this->state = RELAY_FUNCTION_POSITIVE; + change = true; + } + + float output = state == RELAY_FUNCTION_POSITIVE ? output_positive : output_negative; + if (change) { + this->phase_count++; + ESP_LOGV(TAG, "Autotune: Turning output to %.1f%%", output * 100); + } + + return output; +} + +// ================== OscillationFrequencyDetector ================== +void PIDAutotuner::OscillationFrequencyDetector::update(uint32_t now, float error) { + if (this->state == FREQUENCY_DETECTOR_INIT) { + bool pos = error > this->noiseband; + state = pos ? FREQUENCY_DETECTOR_POSITIVE : FREQUENCY_DETECTOR_NEGATIVE; + } + + bool had_crossing = false; + if (this->state == FREQUENCY_DETECTOR_POSITIVE && error < -this->noiseband) { + this->state = FREQUENCY_DETECTOR_NEGATIVE; + had_crossing = true; + } else if (this->state == FREQUENCY_DETECTOR_NEGATIVE && error > this->noiseband) { + this->state = FREQUENCY_DETECTOR_POSITIVE; + had_crossing = true; + } + + if (had_crossing) { + // Had crossing above hysteresis threshold, record + ESP_LOGV(TAG, "Autotune: Detected Zero-Cross at %u", now); + if (this->last_zerocross != 0) { + uint32_t dt = now - this->last_zerocross; + ESP_LOGV(TAG, " dt: %u", dt); + this->zerocrossing_intervals.push_back(dt); + } + this->last_zerocross = now; + } +} +bool PIDAutotuner::OscillationFrequencyDetector::has_enough_data() const { + // Do we have enough data in this detector to generate PID values? + return this->zerocrossing_intervals.size() >= 2; +} +float PIDAutotuner::OscillationFrequencyDetector::get_mean_oscillation_period() const { + // Get the mean oscillation period in seconds + // Only call if has_enough_data() has returned true. + float sum = 0.0f; + for (uint32_t v : this->zerocrossing_intervals) + sum += v; + // zerocrossings are each half-period, multiply by 2 + float mean_value = sum / this->zerocrossing_intervals.size(); + // divide by 1000 to get seconds, multiply by two because zc happens two times per period + float mean_period = mean_value / 1000 * 2; + return mean_period; +} +bool PIDAutotuner::OscillationFrequencyDetector::is_increase_decrease_symmetrical() const { + // Check if increase/decrease of process value was symmetrical + // If the process value increases much faster than it decreases, the generated PID values will + // not be very good and the function output values need to be adjusted + // Happens for example with a well-insulated heating element. + // We calculate this based on the zerocrossing interval. + if (zerocrossing_intervals.empty()) + return false; + uint32_t max_interval = zerocrossing_intervals[0]; + uint32_t min_interval = zerocrossing_intervals[0]; + for (uint32_t interval : zerocrossing_intervals) { + max_interval = std::max(max_interval, interval); + min_interval = std::min(min_interval, interval); + } + float ratio = min_interval / float(max_interval); + return ratio >= 0.66; +} + +// ================== OscillationAmplitudeDetector ================== +void PIDAutotuner::OscillationAmplitudeDetector::update(float error, + PIDAutotuner::RelayFunction::RelayFunctionState relay_state) { + if (relay_state != last_relay_state) { + if (last_relay_state == RelayFunction::RELAY_FUNCTION_POSITIVE) { + // Transitioned from positive error to negative error. + // The positive error peak must have been in previous segment (180° shifted) + // record phase_max + this->phase_maxs.push_back(phase_max); + ESP_LOGV(TAG, "Autotune: Phase Max: %f", phase_max); + } else if (last_relay_state == RelayFunction::RELAY_FUNCTION_NEGATIVE) { + // Transitioned from negative error to positive error. + // The negative error peak must have been in previous segment (180° shifted) + // record phase_min + this->phase_mins.push_back(phase_min); + ESP_LOGV(TAG, "Autotune: Phase Min: %f", phase_min); + } + // reset phase values for next phase + this->phase_min = error; + this->phase_max = error; + } + this->last_relay_state = relay_state; + + this->phase_min = std::min(this->phase_min, error); + this->phase_max = std::max(this->phase_max, error); + + // Check arrays sizes, we keep at most 7 items (6 datapoints is enough, and data at beginning might not + // have been stabilized) + if (this->phase_maxs.size() > 7) + this->phase_maxs.erase(this->phase_maxs.begin()); + if (this->phase_mins.size() > 7) + this->phase_mins.erase(this->phase_mins.begin()); +} +bool PIDAutotuner::OscillationAmplitudeDetector::has_enough_data() const { + // Return if we have enough data to generate PID parameters + // The first phase is not very useful if the setpoint is not set to the starting process value + // So discard first phase. Otherwise we need at least two phases. + return std::min(phase_mins.size(), phase_maxs.size()) >= 3; +} +float PIDAutotuner::OscillationAmplitudeDetector::get_mean_oscillation_amplitude() const { + float total_amplitudes = 0; + size_t total_amplitudes_n = 0; + for (int i = 1; i < std::min(phase_mins.size(), phase_maxs.size()) - 1; i++) { + total_amplitudes += std::abs(phase_maxs[i] - phase_mins[i + 1]); + total_amplitudes_n++; + } + float mean_amplitude = total_amplitudes / total_amplitudes_n; + // Amplitude is measured from center, divide by 2 + return mean_amplitude / 2.0f; +} +bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const { + // Check if oscillation amplitude is convergent + // We implement this by checking global extrema against average amplitude + if (this->phase_mins.empty() || this->phase_maxs.empty()) + return false; + + float global_max = phase_maxs[0], global_min = phase_mins[0]; + for (auto v : this->phase_mins) + global_min = std::min(global_min, v); + for (auto v : this->phase_maxs) + global_max = std::min(global_max, v); + float global_amplitude = (global_max - global_min) / 2.0f; + float mean_amplitude = this->get_mean_oscillation_amplitude(); + return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; +} + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h new file mode 100644 index 0000000000..7dfe0c056d --- /dev/null +++ b/esphome/components/pid/pid_autotuner.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "pid_controller.h" +#include "pid_simulator.h" + +namespace esphome { +namespace pid { + +class PIDAutotuner { + public: + struct PIDResult { + float kp; + float ki; + float kd; + }; + struct PIDAutotuneResult { + float output; + optional result_params; + }; + + void config(float output_min, float output_max) { + relay_function_.output_negative = std::max(relay_function_.output_negative, output_min); + relay_function_.output_positive = std::min(relay_function_.output_positive, output_max); + } + PIDAutotuneResult update(float setpoint, float process_variable); + bool is_finished() const { return state_ != AUTOTUNE_RUNNING; } + + void dump_config(); + + void set_noiseband(float noiseband) { + relay_function_.noiseband = noiseband; + // ZC detector uses 1/4 the noiseband of relay function (noise suppression) + frequency_detector_.noiseband = noiseband / 4; + } + void set_output_positive(float output_positive) { relay_function_.output_positive = output_positive; } + void set_output_negative(float output_negative) { relay_function_.output_negative = output_negative; } + + protected: + struct RelayFunction { + float update(float error); + + float current_target_error() const { + if (state == RELAY_FUNCTION_INIT) + return 0; + if (state == RELAY_FUNCTION_POSITIVE) + return -noiseband; + return noiseband; + } + + enum RelayFunctionState { + RELAY_FUNCTION_INIT, + RELAY_FUNCTION_POSITIVE, + RELAY_FUNCTION_NEGATIVE, + } state = RELAY_FUNCTION_INIT; + float noiseband = 0.5; + float output_positive = 1; + float output_negative = -1; + uint32_t phase_count = 0; + } relay_function_; + struct OscillationFrequencyDetector { + void update(uint32_t now, float error); + + bool has_enough_data() const; + + float get_mean_oscillation_period() const; + + bool is_increase_decrease_symmetrical() const; + + enum FrequencyDetectorState { + FREQUENCY_DETECTOR_INIT, + FREQUENCY_DETECTOR_POSITIVE, + FREQUENCY_DETECTOR_NEGATIVE, + } state; + float noiseband = 0.05; + uint32_t last_zerocross{0}; + std::vector zerocrossing_intervals; + } frequency_detector_; + struct OscillationAmplitudeDetector { + void update(float error, RelayFunction::RelayFunctionState relay_state); + + bool has_enough_data() const; + + float get_mean_oscillation_amplitude() const; + + bool is_amplitude_convergent() const; + + float phase_min = NAN; + float phase_max = NAN; + std::vector phase_mins; + std::vector phase_maxs; + RelayFunction::RelayFunctionState last_relay_state = RelayFunction::RELAY_FUNCTION_INIT; + } amplitude_detector_; + PIDResult calculate_pid_(float kp_factor, float ki_factor, float kd_factor); + void print_rule_(const char *name, float kp_factor, float ki_factor, float kd_factor); + PIDResult get_ziegler_nichols_pid_() { return calculate_pid_(0.6f, 1.2f, 0.075f); } + + uint32_t enough_data_phase_ = 0; + float setpoint_ = NAN; + enum State { + AUTOTUNE_RUNNING, + AUTOTUNE_SUCCEEDED, + } state_ = AUTOTUNE_RUNNING; + float ku_; + float pu_; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp new file mode 100644 index 0000000000..0c777ffd8b --- /dev/null +++ b/esphome/components/pid/pid_climate.cpp @@ -0,0 +1,152 @@ +#include "pid_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.climate"; + +void PIDClimate::setup() { + this->sensor_->add_on_state_callback([this](float state) { + // only publish if state/current temperature has changed in two digits of precision + this->do_publish_ = roundf(state * 100) != roundf(this->current_temperature * 100); + this->current_temperature = state; + this->update_pid_(); + }); + this->current_temperature = this->sensor_->state; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles those for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->target_temperature = this->default_target_temperature_; + } +} +void PIDClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + // If switching to non-auto mode, set output immediately + if (this->mode != climate::CLIMATE_MODE_AUTO) + this->handle_non_auto_mode_(); + + this->publish_state(); +} +climate::ClimateTraits PIDClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(true); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_cool_mode(this->supports_cool_()); + traits.set_supports_heat_mode(this->supports_heat_()); + traits.set_supports_action(true); + return traits; +} +void PIDClimate::dump_config() { + LOG_CLIMATE("", "PID Climate", this); + ESP_LOGCONFIG(TAG, " Control Parameters:"); + ESP_LOGCONFIG(TAG, " kp: %.5f, ki: %.5f, kd: %.5f", controller_.kp, controller_.ki, controller_.kd); + + if (this->autotuner_ != nullptr) { + this->autotuner_->dump_config(); + } +} +void PIDClimate::write_output_(float value) { + this->output_value_ = value; + + // first ensure outputs are off (both outputs not active at the same time) + if (this->supports_cool_() && value >= 0) + this->cool_output_->set_level(0.0f); + if (this->supports_heat_() && value <= 0) + this->heat_output_->set_level(0.0f); + + // value < 0 means cool, > 0 means heat + if (this->supports_cool_() && value < 0) + this->cool_output_->set_level(std::min(1.0f, -value)); + if (this->supports_heat_() && value > 0) + this->heat_output_->set_level(std::min(1.0f, value)); + + // Update action variable for user feedback what's happening + climate::ClimateAction new_action; + if (this->supports_cool_() && value < 0) + new_action = climate::CLIMATE_ACTION_COOLING; + else if (this->supports_heat_() && value > 0) + new_action = climate::CLIMATE_ACTION_HEATING; + else if (this->mode == climate::CLIMATE_MODE_OFF) + new_action = climate::CLIMATE_ACTION_OFF; + else + new_action = climate::CLIMATE_ACTION_IDLE; + + if (new_action != this->action) { + this->action = new_action; + this->do_publish_ = true; + } + this->pid_computed_callback_.call(); +} +void PIDClimate::handle_non_auto_mode_() { + // in non-auto mode, switch directly to appropriate action + // - HEAT mode / COOL mode -> Output at ±100% + // - OFF mode -> Output at 0% + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->write_output_(1.0); + } else if (this->mode == climate::CLIMATE_MODE_COOL) { + this->write_output_(-1.0); + } else if (this->mode == climate::CLIMATE_MODE_OFF) { + this->write_output_(0.0); + } else { + assert(false); + } +} +void PIDClimate::update_pid_() { + float value; + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, turn off all outputs + value = 0.0; + } else { + // Update PID controller irrespective of current mode, to not mess up D/I terms + // In non-auto mode, we just discard the output value + value = this->controller_.update(this->target_temperature, this->current_temperature); + + // Check autotuner + if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) { + auto res = this->autotuner_->update(this->target_temperature, this->current_temperature); + if (res.result_params.has_value()) { + this->controller_.kp = res.result_params->kp; + this->controller_.ki = res.result_params->ki; + this->controller_.kd = res.result_params->kd; + // keep autotuner instance so that subsequent dump_configs will print the long result message. + } else { + value = res.output; + if (mode != climate::CLIMATE_MODE_AUTO) { + ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); + } + } + } + } + + if (this->mode != climate::CLIMATE_MODE_AUTO) { + this->handle_non_auto_mode_(); + } else { + this->write_output_(value); + } + + if (this->do_publish_) + this->publish_state(); +} +void PIDClimate::start_autotune(std::unique_ptr &&autotune) { + this->autotuner_ = std::move(autotune); + float min_value = this->supports_cool_() ? -1.0f : 0.0f; + float max_value = this->supports_heat_() ? 1.0f : 0.0f; + this->autotuner_->config(min_value, max_value); + this->set_interval("autotune-progress", 10000, [this]() { + if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) + this->autotuner_->dump_config(); + }); +} + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h new file mode 100644 index 0000000000..8f379c47b4 --- /dev/null +++ b/esphome/components/pid/pid_climate.h @@ -0,0 +1,94 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/output/float_output.h" +#include "pid_controller.h" +#include "pid_autotuner.h" + +namespace esphome { +namespace pid { + +class PIDClimate : public climate::Climate, public Component { + public: + PIDClimate() = default; + void setup() override; + void dump_config() override; + + void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } + void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } + void set_kp(float kp) { controller_.kp = kp; } + void set_ki(float ki) { controller_.ki = ki; } + void set_kd(float kd) { controller_.kd = kd; } + void set_min_integral(float min_integral) { controller_.min_integral = min_integral; } + void set_max_integral(float max_integral) { controller_.max_integral = max_integral; } + + float get_output_value() const { return output_value_; } + float get_error_value() const { return controller_.error; } + float get_proportional_term() const { return controller_.proportional_term; } + float get_integral_term() const { return controller_.integral_term; } + float get_derivative_term() const { return controller_.derivative_term; } + void add_on_pid_computed_callback(std::function &&callback) { + pid_computed_callback_.add(std::move(callback)); + } + void set_default_target_temperature(float default_target_temperature) { + default_target_temperature_ = default_target_temperature; + } + void start_autotune(std::unique_ptr &&autotune); + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + void update_pid_(); + + bool supports_cool_() const { return this->cool_output_ != nullptr; } + bool supports_heat_() const { return this->heat_output_ != nullptr; } + + void write_output_(float value); + void handle_non_auto_mode_(); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_; + output::FloatOutput *cool_output_ = nullptr; + output::FloatOutput *heat_output_ = nullptr; + PIDController controller_; + /// Output value as reported by the PID controller, for PIDClimateSensor + float output_value_; + CallbackManager pid_computed_callback_; + float default_target_temperature_; + std::unique_ptr autotuner_; + bool do_publish_ = false; +}; + +template class PIDAutotuneAction : public Action { + public: + PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { + auto tuner = make_unique(); + tuner->set_noiseband(this->noiseband_); + tuner->set_output_negative(this->negative_output_); + tuner->set_output_positive(this->positive_output_); + this->parent_->start_autotune(std::move(tuner)); + } + + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + + protected: + float noiseband_; + float positive_output_; + float negative_output_; + PIDClimate *parent_; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h new file mode 100644 index 0000000000..7ec7724e15 --- /dev/null +++ b/esphome/components/pid/pid_controller.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/esphal.h" + +namespace esphome { +namespace pid { + +struct PIDController { + float update(float setpoint, float process_value) { + // e(t) ... error at timestamp t + // r(t) ... setpoint + // y(t) ... process value (sensor reading) + // u(t) ... output value + + float dt = calculate_relative_time_(); + + // e(t) := r(t) - y(t) + error = setpoint - process_value; + + // p(t) := K_p * e(t) + proportional_term = kp * error; + + // i(t) := K_i * \int_{0}^{t} e(t) dt + accumulated_integral_ += error * dt * ki; + // constrain accumulated integral value + if (!isnan(min_integral) && accumulated_integral_ < min_integral) + accumulated_integral_ = min_integral; + if (!isnan(max_integral) && accumulated_integral_ > max_integral) + accumulated_integral_ = max_integral; + integral_term = accumulated_integral_; + + // d(t) := K_d * de(t)/dt + float derivative = 0.0f; + if (dt != 0.0f) + derivative = (error - previous_error_) / dt; + previous_error_ = error; + derivative_term = kd * derivative; + + // u(t) := p(t) + i(t) + d(t) + return proportional_term + integral_term + derivative_term; + } + + /// Proportional gain K_p. + float kp = 0; + /// Integral gain K_i. + float ki = 0; + /// Differential gain K_d. + float kd = 0; + + float min_integral = NAN; + float max_integral = NAN; + + // Store computed values in struct so that values can be monitored through sensors + float error; + float proportional_term; + float integral_term; + float derivative_term; + + protected: + float calculate_relative_time_() { + uint32_t now = millis(); + uint32_t dt = now - this->last_time_; + if (last_time_ == 0) { + last_time_ = now; + return 0.0f; + } + last_time_ = now; + return dt / 1000.0f; + } + + /// Error from previous update used for derivative term + float previous_error_ = 0; + /// Accumulated integral value + float accumulated_integral_ = 0; + uint32_t last_time_ = 0; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h new file mode 100644 index 0000000000..fe145b7330 --- /dev/null +++ b/esphome/components/pid/pid_simulator.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace pid { + +class PIDSimulator : public PollingComponent, public output::FloatOutput { + public: + PIDSimulator() : PollingComponent(1000) {} + + float surface = 1; /// surface area in m² + float mass = 3; /// mass of simulated object in kg + float temperature = 21; /// current temperature of object in °C + float efficiency = 0.98; /// heating efficiency, 1 is 100% efficient + float thermal_conductivity = 15; /// thermal conductivity of surface are in W/(m*K), here: steel + float specific_heat_capacity = 4.182; /// specific heat capacity of mass in kJ/(kg*K), here: water + float heat_power = 500; /// Heating power in W + float ambient_temperature = 20; /// Ambient temperature in °C + float update_interval = 1; /// The simulated updated interval in seconds + std::vector delayed_temps; /// storage of past temperatures for delaying temperature reading + size_t delay_cycles = 15; /// how many update cycles to delay the output + float output_value = 0.0; /// Current output value of heating element + sensor::Sensor *sensor = new sensor::Sensor(); + + float delta_t(float power) { + // P = Q / t + // Q = c * m * 𝚫t + // 𝚫t = (P*t) / (c*m) + float c = this->specific_heat_capacity; + float t = this->update_interval; + float p = power / 1000; // in kW + float m = this->mass; + return (p * t) / (c * m); + } + + float update_temp() { + float value = clamp(output_value, 0.0f, 1.0f); + + // Heat + float power = value * heat_power * efficiency; + temperature += this->delta_t(power); + + // Cool + // Q = k_w * A * (T_mass - T_ambient) + // P = Q / t + float dt = temperature - ambient_temperature; + float cool_power = (thermal_conductivity * surface * dt) / update_interval; + temperature -= this->delta_t(cool_power); + + // Delay temperature readings + delayed_temps.push_back(temperature); + if (delayed_temps.size() > delay_cycles) + delayed_temps.erase(delayed_temps.begin()); + float prev_temp = this->delayed_temps[0]; + float alpha = 0.1f; + float ret = (1 - alpha) * prev_temp + alpha * prev_temp; + return ret; + } + + void setup() override { sensor->publish_state(this->temperature); } + void update() override { + float new_temp = this->update_temp(); + sensor->publish_state(new_temp); + } + + protected: + void write_state(float state) override { this->output_value = state; } +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py new file mode 100644 index 0000000000..cfab23d204 --- /dev/null +++ b/esphome/components/pid/sensor/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, UNIT_PERCENT, ICON_GAUGE, CONF_TYPE +from ..climate import pid_ns, PIDClimate + +PIDClimateSensor = pid_ns.class_('PIDClimateSensor', sensor.Sensor, cg.Component) +PIDClimateSensorType = pid_ns.enum('PIDClimateSensorType') + +PID_CLIMATE_SENSOR_TYPES = { + 'RESULT': PIDClimateSensorType.PID_SENSOR_TYPE_RESULT, + 'ERROR': PIDClimateSensorType.PID_SENSOR_TYPE_ERROR, + 'PROPORTIONAL': PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL, + 'INTEGRAL': PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL, + 'DERIVATIVE': PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, + 'HEAT': PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, + 'COOL': PIDClimateSensorType.PID_SENSOR_TYPE_COOL, +} + +CONF_CLIMATE_ID = 'climate_id' +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_GAUGE, 1).extend({ + cv.GenerateID(): cv.declare_id(PIDClimateSensor), + cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate), + + cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_CLIMATE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + yield sensor.register_sensor(var, config) + yield cg.register_component(var, config) + + cg.add(var.set_parent(parent)) + cg.add(var.set_type(config[CONF_TYPE])) diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp new file mode 100644 index 0000000000..6241a139f6 --- /dev/null +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -0,0 +1,47 @@ +#include "pid_climate_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.sensor"; + +void PIDClimateSensor::setup() { + this->parent_->add_on_pid_computed_callback([this]() { this->update_from_parent_(); }); + this->update_from_parent_(); +} +void PIDClimateSensor::update_from_parent_() { + float value; + switch (this->type_) { + case PID_SENSOR_TYPE_RESULT: + value = this->parent_->get_output_value(); + break; + case PID_SENSOR_TYPE_ERROR: + value = this->parent_->get_error_value(); + break; + case PID_SENSOR_TYPE_PROPORTIONAL: + value = this->parent_->get_proportional_term(); + break; + case PID_SENSOR_TYPE_INTEGRAL: + value = this->parent_->get_integral_term(); + break; + case PID_SENSOR_TYPE_DERIVATIVE: + value = this->parent_->get_derivative_term(); + break; + case PID_SENSOR_TYPE_HEAT: + value = clamp(this->parent_->get_output_value(), 0.0f, 1.0f); + break; + case PID_SENSOR_TYPE_COOL: + value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f); + break; + default: + value = NAN; + break; + } + this->publish_state(value * 100.0f); +} +void PIDClimateSensor::dump_config() { LOG_SENSOR("", "PID Climate Sensor", this); } + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h new file mode 100644 index 0000000000..85759f1eaf --- /dev/null +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pid/pid_climate.h" + +namespace esphome { +namespace pid { + +enum PIDClimateSensorType { + PID_SENSOR_TYPE_RESULT, + PID_SENSOR_TYPE_ERROR, + PID_SENSOR_TYPE_PROPORTIONAL, + PID_SENSOR_TYPE_INTEGRAL, + PID_SENSOR_TYPE_DERIVATIVE, + PID_SENSOR_TYPE_HEAT, + PID_SENSOR_TYPE_COOL, +}; + +class PIDClimateSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void set_parent(PIDClimate *parent) { parent_ = parent; } + void set_type(PIDClimateSensorType type) { type_ = type; } + + void dump_config() override; + + protected: + void update_from_parent_(); + PIDClimate *parent_; + PIDClimateSensorType type_; +}; + +} // namespace pid +} // namespace esphome From 45630d74f33b8106496989562a9222de09164695 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Fri, 10 Jan 2020 08:23:25 +1100 Subject: [PATCH 134/412] Use b''.decode() instead of str(b'') (#941) Handling of request arguments in WizardRequestHandler is not decoding bytes and rather just doing a str conversion resulting in a value of "b''" being supplied to the wizard code. --- esphome/dashboard/dashboard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 8aea841247..4f2d63d545 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -325,7 +325,10 @@ class WizardRequestHandler(BaseHandler): def post(self): from esphome import wizard - kwargs = {k: ''.join(str(x) for x in v) for k, v in self.request.arguments.items()} + kwargs = { + k: ''.join(x.decode() for x in v) + for k, v in self.request.arguments.items() + } destination = settings.rel_path(kwargs['name'] + '.yaml') wizard.wizard_write(path=destination, **kwargs) self.redirect('./?begin=True') From a73fd55fc2e3ec20c7a80bb13cd73324b8636167 Mon Sep 17 00:00:00 2001 From: Vc <37367415+Valcob@users.noreply.github.com> Date: Thu, 9 Jan 2020 23:25:35 +0200 Subject: [PATCH 135/412] Adding the espressif 2.6.3 (#944) --- esphome/core_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core_config.py b/esphome/core_config.py index 5cfff6c4d9..35f2fa1f80 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -45,6 +45,7 @@ def validate_board(value): validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) PLATFORMIO_ESP8266_LUT = { + '2.6.3': 'espressif8266@2.3.2', '2.6.2': 'espressif8266@2.3.1', '2.6.1': 'espressif8266@2.3.0', '2.5.2': 'espressif8266@2.2.3', From 8a754421fe18c0c0de9d8389d9aa48c7998acd5e Mon Sep 17 00:00:00 2001 From: gitolicious Date: Thu, 9 Jan 2020 22:27:39 +0100 Subject: [PATCH 136/412] extract and use current version of python 3 (#938) --- .gitpod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.yml b/.gitpod.yml index 1859ef44e4..2ff20a0366 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,5 +2,5 @@ ports: - port: 6052 onOpen: open-preview tasks: -- before: script/setup +- before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup command: python -m esphome config dashboard From e21dbc4b60a37237995757a434f5cc314b6d9147 Mon Sep 17 00:00:00 2001 From: voibit Date: Sun, 12 Jan 2020 15:16:25 +0100 Subject: [PATCH 137/412] Inverted output in neopixelbus (#895) * Added inverted output * Added support for inverted output in neopixelbus * Update esphome/components/neopixelbus/light.py Co-Authored-By: Otto Winter * Update light.py * corrected lint errors Co-authored-by: Otto Winter --- esphome/components/neopixelbus/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index ea1b67f8ce..fb83e4740d 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import light from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, CONF_NUM_LEDS, CONF_PIN, \ - CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID + CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID, CONF_INVERT from esphome.core import CORE neopixelbus_ns = cg.esphome_ns.namespace('neopixelbus') @@ -120,6 +120,13 @@ ESP32_METHODS = { def format_method(config): variant = VARIANTS[config[CONF_VARIANT]] method = config[CONF_METHOD] + + if config[CONF_INVERT]: + if method == 'ESP8266_DMA': + variant = 'Inverted' + variant + else: + variant += 'Inverted' + if CORE.is_esp8266: return ESP8266_METHODS[method].format(variant) if CORE.is_esp32: @@ -145,6 +152,7 @@ CONFIG_SCHEMA = cv.All(light.ADDRESSABLE_LIGHT_SCHEMA.extend({ cv.Optional(CONF_TYPE, default='GRB'): validate_type, cv.Optional(CONF_VARIANT, default='800KBPS'): validate_variant, cv.Optional(CONF_METHOD, default=None): validate_method, + cv.Optional(CONF_INVERT, default='no'): cv.boolean, cv.Optional(CONF_PIN): pins.output_pin, cv.Optional(CONF_CLOCK_PIN): pins.output_pin, cv.Optional(CONF_DATA_PIN): pins.output_pin, From d33a1585851724777957a8a983260f38cd8e9181 Mon Sep 17 00:00:00 2001 From: Luar Roji Date: Sun, 12 Jan 2020 11:18:30 -0300 Subject: [PATCH 138/412] Added degree symbol for MAX7219 7-segment display. (#764) The ascii char to use it is "~" (0x7E). Disclaimer: I didn't test this yet. --- esphome/components/max7219/max7219.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index db43ff19f6..6af8982c33 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -14,7 +14,7 @@ static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[94] PROGMEM = { +const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 @@ -109,6 +109,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[94] PROGMEM = { 0b00110001, // '{', ord 0x7B 0b00000110, // '|', ord 0x7C 0b00000111, // '}', ord 0x7D + 0b01100011, // '~', ord 0x7E (degree symbol) }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } @@ -166,7 +167,7 @@ uint8_t MAX7219Component::print(uint8_t start_pos, const char *str) { uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = MAX7219_UNKNOWN_CHAR; - if (*str >= ' ' && *str <= '}') + if (*str >= ' ' && *str <= '~') data = pgm_read_byte(&MAX7219_ASCII_TO_RAW[*str - ' ']); if (data == MAX7219_UNKNOWN_CHAR) { From a30d2f291c47c79159c4a1eef8445ca50bcfc5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=C3=ADr=C3=B3?= <1202136+andrasbiro@users.noreply.github.com> Date: Sun, 12 Jan 2020 16:25:32 +0100 Subject: [PATCH 139/412] Fix dump/tx of 64 bit codes (#940) * Fix dump/tx of 64 bit codes * fixed source format --- esphome/components/remote_base/rc_switch_protocol.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 754b2fae49..b2ff22eb2a 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -56,7 +56,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); for (int16_t i = len - 1; i >= 0; i--) { - if (code & (1 << i)) + if (code & ((uint64_t) 1 << i)) this->one(dst); else this->zero(dst); @@ -237,7 +237,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) { char buffer[65]; for (uint8_t j = 0; j < out_nbits; j++) - buffer[j] = (out_data & (1 << (out_nbits - j - 1))) ? '1' : '0'; + buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0'; buffer[out_nbits] = '\0'; ESP_LOGD(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); From d7a2816c58f46dd7b40cd18c535e6c0c5fd9d92c Mon Sep 17 00:00:00 2001 From: dmkif Date: Sun, 12 Jan 2020 16:38:40 +0100 Subject: [PATCH 140/412] Update hdc1080.cpp (#887) * Update hdc1080.cpp increase waittime, to fix reading errors * Fix: Update HDC1080.cpp i fixed the my change on write_bytes --- esphome/components/hdc1080/hdc1080.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 4041c0c464..915c44b155 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -36,7 +36,7 @@ void HDC1080Component::dump_config() { } void HDC1080Component::update() { uint16_t raw_temp; - if (!this->read_byte_16(HDC1080_CMD_TEMPERATURE, &raw_temp, 9)) { + if (!this->read_byte_16(HDC1080_CMD_TEMPERATURE, &raw_temp, 20)) { this->status_set_warning(); return; } @@ -44,7 +44,7 @@ void HDC1080Component::update() { this->temperature_->publish_state(temp); uint16_t raw_humidity; - if (!this->read_byte_16(HDC1080_CMD_HUMIDITY, &raw_humidity, 9)) { + if (!this->read_byte_16(HDC1080_CMD_HUMIDITY, &raw_humidity, 20)) { this->status_set_warning(); return; } From 92d93d658c85afea6845bc4fb5eaa008cb7e8117 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 12 Jan 2020 12:39:23 -0300 Subject: [PATCH 141/412] add tcl112 support for dry, fan and swing (#939) --- esphome/components/tcl112/tcl112.cpp | 71 ++++++++++++++++++++++++++-- esphome/components/tcl112/tcl112.h | 6 ++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index 2907ae1743..3e7eb7ec9a 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -15,6 +15,12 @@ const uint8_t TCL112_COOL = 3; const uint8_t TCL112_FAN = 7; const uint8_t TCL112_AUTO = 8; +const uint8_t TCL112_FAN_AUTO = 0; +const uint8_t TCL112_FAN_LOW = 2; +const uint8_t TCL112_FAN_MED = 3; +const uint8_t TCL112_FAN_HIGH = 5; + +const uint8_t TCL112_VSWING_MASK = 0x38; const uint8_t TCL112_POWER_MASK = 0x04; const uint8_t TCL112_HALF_DEGREE = 0b00100000; @@ -53,6 +59,14 @@ void Tcl112Climate::transmit_state() { remote_state[6] &= 0xF0; remote_state[6] |= TCL112_HEAT; break; + case climate::CLIMATE_MODE_DRY: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_FAN; + break; case climate::CLIMATE_MODE_OFF: default: remote_state[5] &= ~TCL112_POWER_MASK; @@ -72,6 +86,29 @@ void Tcl112Climate::transmit_state() { remote_state[7] &= 0xF0; // Clear temp bits. remote_state[7] |= ((uint8_t) TCL112_TEMP_MAX - half_degrees / 2); + // Set fan + uint8_t selected_fan; + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + selected_fan = TCL112_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + selected_fan = TCL112_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + selected_fan = TCL112_FAN_LOW; + break; + case climate::CLIMATE_FAN_AUTO: + default: + selected_fan = TCL112_FAN_AUTO; + } + remote_state[8] |= selected_fan; + + // Set swing + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) { + remote_state[8] |= TCL112_VSWING_MASK; + } + // Calculate & set the checksum for the current internal state of the remote. // Stored the checksum value in the last byte. for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) @@ -107,7 +144,7 @@ void Tcl112Climate::transmit_state() { bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { // Validate header if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header fail"); + ESP_LOGVV(TAG, "Header fail"); return false; } @@ -119,14 +156,14 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) remote_state[i] |= 1 << j; else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { - ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } } // Validate footer if (!data.expect_mark(TCL112_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); + ESP_LOGVV(TAG, "Footer fail"); return false; } @@ -136,7 +173,7 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) checksum += remote_state[checksum_byte]; if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { - ESP_LOGV(TAG, "Checksum fail"); + ESP_LOGVV(TAG, "Checksum fail"); return false; } @@ -161,7 +198,11 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_COOL; break; case TCL112_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; case TCL112_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; case TCL112_AUTO: this->mode = climate::CLIMATE_MODE_AUTO; break; @@ -171,6 +212,28 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { if (remote_state[12] & TCL112_HALF_DEGREE) temp += .5f; this->target_temperature = temp; + auto fan = remote_state[8] & 0x7; + switch (fan) { + case TCL112_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case TCL112_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case TCL112_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case TCL112_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + if ((remote_state[8] & TCL112_VSWING_MASK) == TCL112_VSWING_MASK) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + this->publish_state(); return true; } diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 273162662d..e982755d40 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -11,7 +11,11 @@ const float TCL112_TEMP_MIN = 16.0; class Tcl112Climate : public climate_ir::ClimateIR { public: - Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} + Tcl112Climate() + : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} protected: /// Transmit via IR the state of this climate controller. From 170d52e0db8ef1a83859b8bfbfd18715140f00b7 Mon Sep 17 00:00:00 2001 From: "Panuruj Khambanonda (PK)" Date: Sun, 12 Jan 2020 07:42:18 -0800 Subject: [PATCH 142/412] Fix SGP30 incorrect baseline reading/writing (#936) * Split the SGP30 baseline into 2 values - According to the SGP30 datasheet, each eCO2 and TVOC baseline is a 2-byte value (MSB first) - The current implementation ignores the MSB of each of the value - Update the schema to allow 2 different baseline values (optional, but both need to be specified for the baseline to apply) * Make both eCO2 and TVOC required if the optional baseline is defined * Make dump_config() looks better --- esphome/components/sgp30/sensor.py | 11 ++++++-- esphome/components/sgp30/sgp30.cpp | 40 +++++++++++++++++------------- esphome/components/sgp30/sgp30.h | 8 +++--- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 6329b122fd..a52811eb34 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -12,6 +12,8 @@ SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CD CONF_ECO2 = 'eco2' CONF_TVOC = 'tvoc' CONF_BASELINE = 'baseline' +CONF_ECO2_BASELINE = 'eco2_baseline' +CONF_TVOC_BASELINE = 'tvoc_baseline' CONF_UPTIME = 'uptime' CONF_COMPENSATION = 'compensation' CONF_HUMIDITY_SOURCE = 'humidity_source' @@ -22,7 +24,10 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), - cv.Optional(CONF_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_BASELINE): cv.Schema({ + cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, + cv.Required(CONF_TVOC_BASELINE): cv.hex_uint16_t, + }), cv.Optional(CONF_COMPENSATION): cv.Schema({ cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) @@ -44,7 +49,9 @@ def to_code(config): cg.add(var.set_tvoc_sensor(sens)) if CONF_BASELINE in config: - cg.add(var.set_baseline(config[CONF_BASELINE])) + baseline_config = config[CONF_BASELINE] + cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) + cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE])) if CONF_COMPENSATION in config: compensation_config = config[CONF_COMPENSATION] diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 9a73295447..8c148b8e83 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -74,9 +74,9 @@ void SGP30Component::setup() { } // Sensor baseline reliability timer - if (this->baseline_ > 0) { + if (this->eco2_baseline_ > 0 && this->tvoc_baseline_ > 0) { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; - this->write_iaq_baseline_(this->baseline_); + this->write_iaq_baseline_(this->eco2_baseline_, this->tvoc_baseline_); } else { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE; } @@ -106,10 +106,10 @@ void SGP30Component::read_iaq_baseline_() { return; } - uint8_t eco2baseline = (raw_data[0]); - uint8_t tvocbaseline = (raw_data[1]); + uint16_t eco2baseline = (raw_data[0]); + uint16_t tvocbaseline = (raw_data[1]); - ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF))); + ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); this->status_clear_warning(); }); } else { @@ -159,18 +159,19 @@ void SGP30Component::send_env_data_() { } } -void SGP30Component::write_iaq_baseline_(uint16_t baseline) { - uint8_t e_c_o2_baseline = baseline >> 8; - uint8_t tvoc_baseline = baseline & 0xFF; - uint8_t data[4]; +void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline) { + uint8_t data[7]; data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; - data[1] = e_c_o2_baseline; - data[2] = tvoc_baseline; - data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline); - if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) { - ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline); + data[1] = eco2_baseline >> 8; + data[2] = eco2_baseline & 0xFF; + data[3] = sht_crc_(data[1], data[2]); + data[4] = tvoc_baseline >> 8; + data[5] = tvoc_baseline & 0xFF; + data[6] = sht_crc_(data[4], data[5]); + if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { + ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); } else - ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline); + ESP_LOGI(TAG, "Initial eCO2 and TVOC baselines applied successfully!"); } void SGP30Component::dump_config() { @@ -196,8 +197,13 @@ void SGP30Component::dump_config() { } } else { ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); - ESP_LOGCONFIG(TAG, " Baseline: 0x%04X%s", this->baseline_, - ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)")); + if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { + ESP_LOGCONFIG(TAG, " Baseline:"); + ESP_LOGCONFIG(TAG, " eCO2 Baseline: 0x%04X", this->eco2_baseline_); + ESP_LOGCONFIG(TAG, " TVOC Baseline: 0x%04X", this->tvoc_baseline_); + } else { + ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); + } ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 2362d1bca6..27572e9c46 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -13,7 +13,8 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { public: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } - void set_baseline(uint16_t baseline) { baseline_ = baseline; } + void set_eco2_baseline(uint16_t eco2_baseline) { eco2_baseline_ = eco2_baseline; } + void set_tvoc_baseline(uint16_t tvoc_baseline) { tvoc_baseline_ = tvoc_baseline; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } @@ -28,7 +29,7 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { void send_env_data_(); void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); - void write_iaq_baseline_(uint16_t baseline); + void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; @@ -44,7 +45,8 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; - uint16_t baseline_{0x0000}; + uint16_t eco2_baseline_{0x0000}; + uint16_t tvoc_baseline_{0x0000}; /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; From 3b689ef39cb44f92deedf61de21ddc35c9e9819f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sun, 12 Jan 2020 17:08:48 +0100 Subject: [PATCH 143/412] Add register_*_effect to allow registering custom effects (#947) This allows to register custom effect from user components, allowing for bigger composability of source. --- esphome/components/light/effects.py | 95 ++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 005a66a5ad..d08e7f08f5 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -34,13 +34,10 @@ CONF_ADDRESSABLE_FIREWORKS = 'addressable_fireworks' CONF_ADDRESSABLE_FLICKER = 'addressable_flicker' CONF_AUTOMATION = 'automation' -BINARY_EFFECTS = ['lambda', 'automation', 'strobe'] -MONOCHROMATIC_EFFECTS = BINARY_EFFECTS + ['flicker'] -RGB_EFFECTS = MONOCHROMATIC_EFFECTS + ['random'] -ADDRESSABLE_EFFECTS = RGB_EFFECTS + [CONF_ADDRESSABLE_LAMBDA, CONF_ADDRESSABLE_RAINBOW, - CONF_ADDRESSABLE_COLOR_WIPE, CONF_ADDRESSABLE_SCAN, - CONF_ADDRESSABLE_TWINKLE, CONF_ADDRESSABLE_RANDOM_TWINKLE, - CONF_ADDRESSABLE_FIREWORKS, CONF_ADDRESSABLE_FLICKER] +BINARY_EFFECTS = [] +MONOCHROMATIC_EFFECTS = [] +RGB_EFFECTS = [] +ADDRESSABLE_EFFECTS = [] EFFECTS_REGISTRY = Registry() @@ -53,7 +50,41 @@ def register_effect(name, effect_type, default_name, schema, *extra_validators): return EFFECTS_REGISTRY.register(name, effect_type, validator) -@register_effect('lambda', LambdaLightEffect, "Lambda", { +def register_binary_effect(name, effect_type, default_name, schema, *extra_validators): + # binary effect can be used for all lights + BINARY_EFFECTS.append(name) + MONOCHROMATIC_EFFECTS.append(name) + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_monochromatic_effect(name, effect_type, default_name, schema, *extra_validators): + # monochromatic effect can be used for all lights expect binary + MONOCHROMATIC_EFFECTS.append(name) + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_rgb_effect(name, effect_type, default_name, schema, *extra_validators): + # RGB effect can be used for RGB and addressable lights + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_addressable_effect(name, effect_type, default_name, schema, *extra_validators): + # addressable effect can be used only in addressable + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +@register_binary_effect('lambda', LambdaLightEffect, "Lambda", { cv.Required(CONF_LAMBDA): cv.lambda_, cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.update_interval, }) @@ -63,7 +94,7 @@ def lambda_effect_to_code(config, effect_id): config[CONF_UPDATE_INTERVAL]) -@register_effect('automation', AutomationLightEffect, "Automation", { +@register_binary_effect('automation', AutomationLightEffect, "Automation", { cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True), }) def automation_effect_to_code(config, effect_id): @@ -72,7 +103,7 @@ def automation_effect_to_code(config, effect_id): yield var -@register_effect('random', RandomLightEffect, "Random", { +@register_rgb_effect('random', RandomLightEffect, "Random", { cv.Optional(CONF_TRANSITION_LENGTH, default='7.5s'): cv.positive_time_period_milliseconds, cv.Optional(CONF_UPDATE_INTERVAL, default='10s'): cv.positive_time_period_milliseconds, }) @@ -83,7 +114,7 @@ def random_effect_to_code(config, effect_id): yield effect -@register_effect('strobe', StrobeLightEffect, "Strobe", { +@register_binary_effect('strobe', StrobeLightEffect, "Strobe", { cv.Optional(CONF_COLORS, default=[ {CONF_STATE: True, CONF_DURATION: '0.5s'}, {CONF_STATE: False, CONF_DURATION: '0.5s'}, @@ -113,7 +144,7 @@ def strobe_effect_to_code(config, effect_id): yield var -@register_effect('flicker', FlickerLightEffect, "Flicker", { +@register_monochromatic_effect('flicker', FlickerLightEffect, "Flicker", { cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, }) @@ -124,10 +155,12 @@ def flicker_effect_to_code(config, effect_id): yield var -@register_effect('addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { - cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, -}) +@register_addressable_effect( + 'addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { + cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, + } +) def addressable_lambda_effect_to_code(config, effect_id): args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')] lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) @@ -136,7 +169,7 @@ def addressable_lambda_effect_to_code(config, effect_id): yield var -@register_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { +@register_addressable_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { cv.Optional(CONF_SPEED, default=10): cv.uint32_t, cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, }) @@ -147,7 +180,7 @@ def addressable_rainbow_effect_to_code(config, effect_id): yield var -@register_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { +@register_addressable_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { cv.Optional(CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]): cv.ensure_list({ cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, @@ -178,7 +211,7 @@ def addressable_color_wipe_effect_to_code(config, effect_id): yield var -@register_effect('addressable_scan', AddressableScanEffect, "Scan", { +@register_addressable_effect('addressable_scan', AddressableScanEffect, "Scan", { cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), }) @@ -189,7 +222,7 @@ def addressable_scan_effect_to_code(config, effect_id): yield var -@register_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { +@register_addressable_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, cv.Optional(CONF_PROGRESS_INTERVAL, default='4ms'): cv.positive_time_period_milliseconds, }) @@ -200,10 +233,12 @@ def addressable_twinkle_effect_to_code(config, effect_id): yield var -@register_effect('addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, -}) +@register_addressable_effect( + 'addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { + cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, + cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, + } +) def addressable_random_twinkle_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) @@ -211,7 +246,7 @@ def addressable_random_twinkle_effect_to_code(config, effect_id): yield var -@register_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { +@register_addressable_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { cv.Optional(CONF_UPDATE_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SPARK_PROBABILITY, default='10%'): cv.percentage, cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, @@ -226,10 +261,12 @@ def addressable_fireworks_effect_to_code(config, effect_id): yield var -@register_effect('addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { - cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, -}) +@register_addressable_effect( + 'addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { + cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, + } +) def addressable_flicker_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) From 30ecb58e061a71b445ca01b356f4a59eb926dc56 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Tue, 14 Jan 2020 09:35:55 +1100 Subject: [PATCH 144/412] Bugfix/normalize core comparisons (and Python 3 update fixes) (#952) * Correct implementation of comparisons to be Pythonic If a comparison cannot be made return NotImplemented, this allows the Python interpreter to try other comparisons (eg __ieq__) and either return False (in the case of __eq__) or raise a TypeError exception (eg in the case of __lt__). * Python 3 updates * Add a more helpful message in exception if platform is not defined * Added a basic pre-commit check --- .pre-commit-config.yaml | 11 ++++ esphome/core.py | 110 ++++++++++++++++++++-------------------- setup.cfg | 1 - setup.py | 1 - 4 files changed, 67 insertions(+), 56 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..d57da791fd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: flake8 diff --git a/esphome/core.py b/esphome/core.py index 96447e560d..b4bea49dbd 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -168,34 +168,34 @@ class TimePeriod: return self.days or 0 def __eq__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds == other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds == other.total_microseconds + return NotImplemented def __ne__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds != other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds != other.total_microseconds + return NotImplemented def __lt__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds < other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds < other.total_microseconds + return NotImplemented def __gt__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds > other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds > other.total_microseconds + return NotImplemented def __le__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds <= other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds <= other.total_microseconds + return NotImplemented def __ge__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds >= other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds >= other.total_microseconds + return NotImplemented class TimePeriodMicroseconds(TimePeriod): @@ -264,7 +264,7 @@ class ID: else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type = type # type: Optional['MockObjClass'] + self.type: Optional['MockObjClass'] = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -282,13 +282,13 @@ class ID: return self.id def __repr__(self): - return 'ID<{} declaration={}, type={}, manual={}>'.format( - self.id, self.is_declaration, self.type, self.is_manual) + return (f'ID<{self.id} declaration={self.is_declaration}, ' + f'type={self.type}, manual={self.is_manual}>') def __eq__(self, other): - if not isinstance(other, ID): - raise ValueError("other must be ID {} {}".format(type(other), other)) - return self.id == other.id + if isinstance(other, ID): + return self.id == other.id + return NotImplemented def __hash__(self): return hash(self.id) @@ -299,11 +299,10 @@ class ID: class DocumentLocation: - def __init__(self, document, line, column): - # type: (str, int, int) -> None - self.document = document # type: str - self.line = line # type: int - self.column = column # type: int + def __init__(self, document: str, line: int, column: int): + self.document: str = document + self.line: int = line + self.column: int = column @classmethod def from_mark(cls, mark): @@ -318,10 +317,9 @@ class DocumentLocation: class DocumentRange: - def __init__(self, start_mark, end_mark): - # type: (DocumentLocation, DocumentLocation) -> None - self.start_mark = start_mark # type: DocumentLocation - self.end_mark = end_mark # type: DocumentLocation + def __init__(self, start_mark: DocumentLocation, end_mark: DocumentLocation): + self.start_mark: DocumentLocation = start_mark + self.end_mark: DocumentLocation = end_mark @classmethod def from_marks(cls, start_mark, end_mark): @@ -359,7 +357,9 @@ class Define: return hash(self.as_tuple) def __eq__(self, other): - return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + if isinstance(other, Define): + return self.as_tuple == other.as_tuple + return NotImplemented class Library: @@ -381,7 +381,9 @@ class Library: return hash(self.as_tuple) def __eq__(self, other): - return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + if isinstance(other, Library): + return self.as_tuple == other.as_tuple + return NotImplemented def coroutine(func): @@ -462,19 +464,19 @@ class EsphomeCore: self.vscode = False self.ace = False # The name of the node - self.name = None # type: str + self.name: Optional[str] = None # The relative path to the configuration YAML - self.config_path = None # type: str + self.config_path: Optional[str] = None # The relative path to where all build files are stored - self.build_path = None # type: str + self.build_path: Optional[str] = None # The platform (ESP8266, ESP32) of this device - self.esp_platform = None # type: str + self.esp_platform: Optional[str] = None # The board that's used (for example nodemcuv2) - self.board = None # type: str + self.board: Optional[str] = None # The full raw configuration - self.raw_config = {} # type: ConfigType + self.raw_config: ConfigType = {} # The validated configuration, this is None until the config has been validated - self.config = {} # type: ConfigType + self.config: ConfigType = {} # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -482,20 +484,20 @@ class EsphomeCore: # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables = {} # type: Dict[str, 'MockObj'] + self.variables: Dict[str, 'MockObj'] = {} # A list of statements that go in the main setup() block - self.main_statements = [] # type: List['Statement'] + self.main_statements: List['Statement'] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements = [] # type: List['Statement'] + self.global_statements: List['Statement'] = [] # A set of platformio libraries to add to the project - self.libraries = [] # type: List[Library] + self.libraries: List[Library] = [] # A set of build flags to set in the platformio project - self.build_flags = set() # type: Set[str] + self.build_flags: Set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines = set() # type: Set['Define'] + self.defines: Set['Define'] = set() # A dictionary of started coroutines, used to warn when a coroutine was not # awaited. - self.active_coroutines = {} # type: Dict[int, Any] + self.active_coroutines: Dict[int, Any] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared @@ -525,7 +527,7 @@ class EsphomeCore: self.component_ids = set() @property - def address(self): # type: () -> str + def address(self) -> Optional[str]: if 'wifi' in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] @@ -535,7 +537,7 @@ class EsphomeCore: return None @property - def comment(self): # type: () -> str + def comment(self) -> Optional[str]: if CONF_COMMENT in self.config[CONF_ESPHOME]: return self.config[CONF_ESPHOME][CONF_COMMENT] @@ -548,7 +550,7 @@ class EsphomeCore: self.active_coroutines.pop(instance_id) @property - def arduino_version(self): # type: () -> str + def arduino_version(self) -> str: return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @property @@ -587,13 +589,13 @@ class EsphomeCore: @property def is_esp8266(self): if self.esp_platform is None: - raise ValueError + raise ValueError("No platform specified") return self.esp_platform == 'ESP8266' @property def is_esp32(self): if self.esp_platform is None: - raise ValueError + raise ValueError("No platform specified") return self.esp_platform == 'ESP32' def add_job(self, func, *args, **kwargs): diff --git a/setup.cfg b/setup.cfg index ab43acfbc5..32a60839a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ classifier = Intended Audience :: End Users/Desktop License :: OSI Approved :: MIT License Programming Language :: C++ - Programming Language :: Python :: 2 Programming Language :: Python :: 3 Topic :: Home Automation Topic :: Home Automation diff --git a/setup.py b/setup.py index c520b949fc..f78ec49f28 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,6 @@ CLASSIFIERS = [ 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: MIT License', 'Programming Language :: C++', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Home Automation', ] From 6a60f01753fabb3bdcf1aa504b151f15de912916 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 13 Jan 2020 16:39:17 -0600 Subject: [PATCH 145/412] Add transmit pioneer (#922) * Added pioneer_protocol to support transmit_pioneer --- esphome/components/remote_base/__init__.py | 40 ++++- .../remote_base/pioneer_protocol.cpp | 150 ++++++++++++++++++ .../components/remote_base/pioneer_protocol.h | 37 +++++ esphome/const.py | 2 + tests/test1.yaml | 8 + 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 esphome/components/remote_base/pioneer_protocol.cpp create mode 100644 esphome/components/remote_base/pioneer_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 3d57041e56..2c8b6be51c 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -5,7 +5,7 @@ from esphome.components import binary_sensor from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, \ CONF_COMMAND, CONF_CODE, CONF_PULSE_LENGTH, CONF_SYNC, CONF_ZERO, CONF_ONE, CONF_INVERTED, \ CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ - CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY + CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2 from esphome.core import coroutine from esphome.util import Registry, SimpleRegistry @@ -86,7 +86,7 @@ def validate_repeat(value): if isinstance(value, dict): return cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='10ms'): + cv.Optional(CONF_WAIT_TIME, default='25ms'): cv.templatable(cv.positive_time_period_microseconds), })(value) return validate_repeat({CONF_TIMES: value}) @@ -288,6 +288,42 @@ def nec_action(var, config, args): cg.add(var.set_command(template_)) +# Pioneer +(PioneerData, PioneerBinarySensor, PioneerTrigger, PioneerAction, + PioneerDumper) = declare_protocol('Pioneer') +PIONEER_SCHEMA = cv.Schema({ + cv.Required(CONF_RC_CODE_1): cv.hex_uint16_t, + cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint16_t, +}) + + +@register_binary_sensor('pioneer', PioneerBinarySensor, PIONEER_SCHEMA) +def pioneer_binary_sensor(var, config): + cg.add(var.set_data(cg.StructInitializer( + PioneerData, + ('rc_code_1', config[CONF_RC_CODE_1]), + ('rc_code_2', config[CONF_RC_CODE_2]), + ))) + + +@register_trigger('pioneer', PioneerTrigger, PioneerData) +def pioneer_trigger(var, config): + pass + + +@register_dumper('pioneer', PioneerDumper) +def pioneer_dumper(var, config): + pass + + +@register_action('pioneer', PioneerAction, PIONEER_SCHEMA) +def pioneer_action(var, config, args): + template_ = yield cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16) + cg.add(var.set_rc_code_1(template_)) + template_ = yield cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16) + cg.add(var.set_rc_code_2(template_)) + + # Sony SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol('Sony') SONY_SCHEMA = cv.Schema({ diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp new file mode 100644 index 0000000000..49a27e08e7 --- /dev/null +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -0,0 +1,150 @@ +#include "pioneer_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *TAG = "remote.pioneer"; + +static const uint32_t HEADER_HIGH_US = 9000; +static const uint32_t HEADER_LOW_US = 4500; +static const uint32_t BIT_HIGH_US = 560; +static const uint32_t BIT_ONE_LOW_US = 1690; +static const uint32_t BIT_ZERO_LOW_US = 560; +static const uint32_t TRAILER_SPACE_US = 25500; + +void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { + uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); + uint32_t address2 = ((data.rc_code_2 & 0xff00) | (~(data.rc_code_2 >> 8) & 0xff)); + uint32_t command1 = 0; + uint32_t command2 = 0; + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_1 >> bit) & 1) + command1 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_1 >> (bit + 4)) & 1) + command1 |= (1UL << (3 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_2 >> bit) & 1) + command2 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_2 >> (bit + 4)) & 1) + command2 |= (1UL << (3 - bit)); + } + + command1 = (command1 << 8) | ((~command1) & 0xff); + command2 = (command2 << 8) | ((~command2) & 0xff); + + if (data.rc_code_2 == 0) + dst->reserve(68); + else + dst->reserve((68 * 2) + 1); + + dst->set_carrier_frequency(40000); + + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (address1 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (command1 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + dst->mark(BIT_HIGH_US); + + if (data.rc_code_2 != 0) { + dst->space(TRAILER_SPACE_US); + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (address2 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (command2 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + dst->mark(BIT_HIGH_US); + } +} +optional PioneerProtocol::decode(RemoteReceiveData src) { + uint16_t address1 = 0; + uint16_t command1 = 0; + + PioneerData data{ + .rc_code_1 = 0, + .rc_code_2 = 0, + }; + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + address1 |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + address1 &= ~mask; + } else { + return {}; + } + } + + for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command1 |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command1 &= ~mask; + } else { + return {}; + } + } + + if (!src.expect_mark(BIT_HIGH_US)) + return {}; + + if ((address1 >> 8) != ((~address1) & 0xff)) + return {}; + + if ((command1 >> 8) != ((~command1) & 0xff)) + return {}; + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((~command1 >> bit) & 1) + data.rc_code_1 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((~command1 >> (bit + 4)) & 1) + data.rc_code_1 |= (1UL << (3 - bit)); + } + data.rc_code_1 |= address1 & 0xff00; + + return data; +} +void PioneerProtocol::dump(const PioneerData &data) { + if (data.rc_code_2 == 0) + ESP_LOGD(TAG, "Received Pioneer: rc_code_X=0x%04X", data.rc_code_1); + else + ESP_LOGD(TAG, "Received Pioneer: rc_code_1=0x%04X, rc_code_2=0x%04X", data.rc_code_1, data.rc_code_2); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h new file mode 100644 index 0000000000..f93e51a033 --- /dev/null +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -0,0 +1,37 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct PioneerData { + uint16_t rc_code_1; + uint16_t rc_code_2; + + bool operator==(const PioneerData &rhs) const { return rc_code_1 == rhs.rc_code_1 && rc_code_2 == rhs.rc_code_2; } +}; + +class PioneerProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const PioneerData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const PioneerData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Pioneer) + +template class PioneerAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, rc_code_1) + TEMPLATABLE_VALUE(uint16_t, rc_code_2) + void encode(RemoteTransmitData *dst, Ts... x) override { + PioneerData data{}; + data.rc_code_1 = this->rc_code_1_.value(x...); + data.rc_code_2 = this->rc_code_2_.value(x...); + PioneerProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 8a45f27880..f287a15734 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -356,6 +356,8 @@ CONF_RANGE_FROM = 'range_from' CONF_RANGE_TO = 'range_to' CONF_RATE = 'rate' CONF_RAW = 'raw' +CONF_RC_CODE_1 = 'rc_code_1' +CONF_RC_CODE_2 = 'rc_code_2' CONF_REBOOT_TIMEOUT = 'reboot_timeout' CONF_RECEIVE_TIMEOUT = 'receive_timeout' CONF_RED = 'red' diff --git a/tests/test1.yaml b/tests/test1.yaml index 043fafc9c3..f0de50d9b6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1239,6 +1239,14 @@ switch: remote_transmitter.transmit_panasonic: address: 0x4004 command: 0x1000BCD + - platform: template + name: Pioneer + turn_on_action: + - remote_transmitter.transmit_pioneer: + rc_code_1: 0xA556 + rc_code_2: 0xA506 + repeat: + times: 2 - platform: template name: RC Switch Raw turn_on_action: From 990a4d47746056786700655a253c93d1774f7634 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 13 Jan 2020 19:44:55 -0300 Subject: [PATCH 146/412] Display tm1637 (#946) * add TM1637 support --- esphome/components/tm1637/display.py | 35 +++ esphome/components/tm1637/tm1637.cpp | 299 ++++++++++++++++++++++++++ esphome/components/tm1637/tm1637.h | 70 ++++++ esphome/components/tm1651/__init__.py | 4 +- esphome/const.py | 1 + tests/test1.yaml | 6 + 6 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 esphome/components/tm1637/display.py create mode 100644 esphome/components/tm1637/tm1637.cpp create mode 100644 esphome/components/tm1637/tm1637.h diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py new file mode 100644 index 0000000000..211d78a9d9 --- /dev/null +++ b/esphome/components/tm1637/display.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY + +tm1637_ns = cg.esphome_ns.namespace('tm1637') +TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) +TM1637DisplayRef = TM1637Display.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TM1637Display), + + cv.Optional(CONF_INTENSITY, default=7): cv.All(cv.uint8_t, cv.Range(min=0, max=7)), + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield display.register_display(var, config) + + clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk)) + dio = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio)) + + cg.add(var.set_intensity(config[CONF_INTENSITY])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(TM1637DisplayRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp new file mode 100644 index 0000000000..ebf2fff9d6 --- /dev/null +++ b/esphome/components/tm1637/tm1637.cpp @@ -0,0 +1,299 @@ +#include "tm1637.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tm1637 { + +const char* 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_UNKNOWN_CHAR = 0b11111111; + +// +// A +// --- +// F | | B +// -G- +// E | | C +// --- +// D X +// XABCDEFG +const uint8_t TM1637_ASCII_TO_RAW[94] PROGMEM = { + 0b00000000, // ' ', ord 0x20 + 0b10110000, // '!', ord 0x21 + 0b00100010, // '"', ord 0x22 + TM1637_UNKNOWN_CHAR, // '#', ord 0x23 + TM1637_UNKNOWN_CHAR, // '$', ord 0x24 + 0b01001001, // '%', ord 0x25 + TM1637_UNKNOWN_CHAR, // '&', ord 0x26 + 0b00000010, // ''', ord 0x27 + 0b01001110, // '(', ord 0x28 + 0b01111000, // ')', ord 0x29 + 0b01000000, // '*', ord 0x2A + TM1637_UNKNOWN_CHAR, // '+', ord 0x2B + 0b00010000, // ',', ord 0x2C + 0b00000001, // '-', ord 0x2D + 0b10000000, // '.', ord 0x2E + TM1637_UNKNOWN_CHAR, // '/', ord 0x2F + 0b01111110, // '0', ord 0x30 + 0b00110000, // '1', ord 0x31 + 0b01101101, // '2', ord 0x32 + 0b01111001, // '3', ord 0x33 + 0b00110011, // '4', ord 0x34 + 0b01011011, // '5', ord 0x35 + 0b01011111, // '6', ord 0x36 + 0b01110000, // '7', ord 0x37 + 0b01111111, // '8', ord 0x38 + 0b01110011, // '9', ord 0x39 + 0b01001000, // ':', ord 0x3A + 0b01011000, // ';', ord 0x3B + TM1637_UNKNOWN_CHAR, // '<', ord 0x3C + TM1637_UNKNOWN_CHAR, // '=', ord 0x3D + TM1637_UNKNOWN_CHAR, // '>', ord 0x3E + 0b01100101, // '?', ord 0x3F + 0b01101111, // '@', ord 0x40 + 0b01110111, // 'A', ord 0x41 + 0b00011111, // 'B', ord 0x42 + 0b01001110, // 'C', ord 0x43 + 0b00111101, // 'D', ord 0x44 + 0b01001111, // 'E', ord 0x45 + 0b01000111, // 'F', ord 0x46 + 0b01011110, // 'G', ord 0x47 + 0b00110111, // 'H', ord 0x48 + 0b00110000, // 'I', ord 0x49 + 0b00111100, // 'J', ord 0x4A + TM1637_UNKNOWN_CHAR, // 'K', ord 0x4B + 0b00001110, // 'L', ord 0x4C + TM1637_UNKNOWN_CHAR, // 'M', ord 0x4D + 0b00010101, // 'N', ord 0x4E + 0b01111110, // 'O', ord 0x4F + 0b01100111, // 'P', ord 0x50 + 0b11111110, // 'Q', ord 0x51 + 0b00000101, // 'R', ord 0x52 + 0b01011011, // 'S', ord 0x53 + 0b00000111, // 'T', ord 0x54 + 0b00111110, // 'U', ord 0x55 + 0b00111110, // 'V', ord 0x56 + 0b00111111, // 'W', ord 0x57 + TM1637_UNKNOWN_CHAR, // 'X', ord 0x58 + 0b00100111, // 'Y', ord 0x59 + 0b01101101, // 'Z', ord 0x5A + 0b01001110, // '[', ord 0x5B + TM1637_UNKNOWN_CHAR, // '\', ord 0x5C + 0b01111000, // ']', ord 0x5D + TM1637_UNKNOWN_CHAR, // '^', ord 0x5E + 0b00001000, // '_', ord 0x5F + 0b00100000, // '`', ord 0x60 + 0b01110111, // 'a', ord 0x61 + 0b00011111, // 'b', ord 0x62 + 0b00001101, // 'c', ord 0x63 + 0b00111101, // 'd', ord 0x64 + 0b01001111, // 'e', ord 0x65 + 0b01000111, // 'f', ord 0x66 + 0b01011110, // 'g', ord 0x67 + 0b00010111, // 'h', ord 0x68 + 0b00010000, // 'i', ord 0x69 + 0b00111100, // 'j', ord 0x6A + TM1637_UNKNOWN_CHAR, // 'k', ord 0x6B + 0b00001110, // 'l', ord 0x6C + TM1637_UNKNOWN_CHAR, // 'm', ord 0x6D + 0b00010101, // 'n', ord 0x6E + 0b00011101, // 'o', ord 0x6F + 0b01100111, // 'p', ord 0x70 + TM1637_UNKNOWN_CHAR, // 'q', ord 0x71 + 0b00000101, // 'r', ord 0x72 + 0b01011011, // 's', ord 0x73 + 0b00000111, // 't', ord 0x74 + 0b00011100, // 'u', ord 0x75 + 0b00011100, // 'v', ord 0x76 + TM1637_UNKNOWN_CHAR, // 'w', ord 0x77 + TM1637_UNKNOWN_CHAR, // 'x', ord 0x78 + 0b00100111, // 'y', ord 0x79 + TM1637_UNKNOWN_CHAR, // 'z', ord 0x7A + 0b00110001, // '{', ord 0x7B + 0b00000110, // '|', ord 0x7C + 0b00000111, // '}', ord 0x7D +}; +void TM1637Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1637..."); + + this->clk_pin_->setup(); // OUTPUT + this->clk_pin_->digital_write(false); // LOW + this->dio_pin_->setup(); // OUTPUT + this->dio_pin_->digital_write(false); // LOW + + this->display(); +} +void TM1637Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1637:"); + ESP_LOGCONFIG(TAG, " INTENSITY: %d", this->intensity_); + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" DIO Pin: ", this->dio_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1637Display::update() { + for (uint8_t& i : this->buffer_) + i = 0; + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +float TM1637Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void TM1637Display::bit_delay_() { delayMicroseconds(100); } +void TM1637Display::start_() { + this->dio_pin_->pin_mode(OUTPUT); + this->bit_delay_(); +} + +void TM1637Display::stop_() { + this->dio_pin_->pin_mode(OUTPUT); + bit_delay_(); + this->clk_pin_->pin_mode(INPUT); + bit_delay_(); + this->dio_pin_->pin_mode(INPUT); + bit_delay_(); +} + +void TM1637Display::display() { + ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]); + + // Write COMM1 + this->start_(); + this->send_byte_(TM1637_I2C_COMM1); + this->stop_(); + + // Write COMM2 + first digit address + this->start_(); + this->send_byte_(TM1637_I2C_COMM2); + + // Write the data bytes + for (auto b : this->buffer_) { + this->send_byte_(b); + } + + this->stop_(); + + // Write COMM3 + brightness + this->start_(); + this->send_byte_(TM1637_I2C_COMM3 + ((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(OUTPUT); + this->bit_delay_(); + + // Set data bit + if (data & 0x01) + this->dio_pin_->pin_mode(INPUT); + else + this->dio_pin_->pin_mode(OUTPUT); + + this->bit_delay_(); + + // CLK high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + data = data >> 1; + } + + // Wait for acknowledge + // CLK to zero + this->clk_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(INPUT); + this->bit_delay_(); + + // CLK to high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + uint8_t ack = this->dio_pin_->digital_read(); + if (ack == 0) { + this->dio_pin_->pin_mode(OUTPUT); + } + + this->bit_delay_(); + this->clk_pin_->pin_mode(OUTPUT); + this->bit_delay_(); + + return ack; +} + +uint8_t TM1637Display::print(uint8_t start_pos, const char* 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; + if (*str >= ' ' && *str <= '}') + data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); + + if (data == TM1637_UNKNOWN_CHAR) { + ESP_LOGW(TAG, "Encountered character '%c' with no TM1637 representation while translating string!", *str); + } + // Remap segments, for compatibility with MAX7219 segment definition which is + // XABCDEFG, but TM1637 is // XGFEDCBA + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x1 : 0) | // A + ((data & 0x20) ? 0x2 : 0) | // B + ((data & 0x10) ? 0x4 : 0) | // C + ((data & 0x8) ? 0x8 : 0) | // D + ((data & 0x4) ? 0x10 : 0) | // E + ((data & 0x2) ? 0x20 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + if (*str == '.') { + if (pos != start_pos) + pos--; + this->buffer_[pos] |= 0b10000000; + } else { + if (pos >= 4) { + ESP_LOGE(TAG, "String is too long for the display!"); + break; + } + this->buffer_[pos] = data; + } + pos++; + } + return pos - start_pos; +} +uint8_t TM1637Display::print(const char* str) { return this->print(0, str); } +uint8_t TM1637Display::printf(uint8_t pos, const char* format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1637Display::printf(const char* format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t TM1637Display::strftime(uint8_t pos, const char* format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1637Display::strftime(const char* format, time::ESPTime time) { return this->strftime(0, format, time); } +#endif + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h new file mode 100644 index 0000000000..91e8ba66c0 --- /dev/null +++ b/esphome/components/tm1637/tm1637.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/esphal.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace tm1637 { + +class TM1637Display; + +using tm1637_writer_t = std::function; + +class TM1637Display : public PollingComponent { + public: + void set_writer(tm1637_writer_t &&writer) { this->writer_ = writer; } + + void setup() override; + + void dump_config() override; + + void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + + float get_setup_priority() const override; + + void update() override; + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t print(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t print(const char *str); + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } + + void display(); + +#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 + + protected: + void bit_delay_(); + void setup_pins_(); + bool send_byte_(uint8_t b); + void start_(); + void stop_(); + + GPIOPin *dio_pin_; + GPIOPin *clk_pin_; + uint8_t intensity_; + optional writer_{}; + uint8_t buffer_[4] = {0}; +}; + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 1c49287878..d83ef4b3b7 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_LEVEL, CONF_BRIGHTNESS +from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_DIO_PIN, CONF_LEVEL, CONF_BRIGHTNESS tm1651_ns = cg.esphome_ns.namespace('tm1651') TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) @@ -15,8 +15,6 @@ TM1651_BRIGHTNESS_OPTIONS = { 3: TM1651Display.TM1651_BRIGHTNESS_HIGH } -CONF_DIO_PIN = 'dio_pin' - CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(TM1651Display), cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, diff --git a/esphome/const.py b/esphome/const.py index f287a15734..0c5f286f30 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -125,6 +125,7 @@ CONF_DELTA = 'delta' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DIMENSIONS = 'dimensions' +CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' CONF_DISCOVERY = 'discovery' diff --git a/tests/test1.yaml b/tests/test1.yaml index f0de50d9b6..fb15bc0a6b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1426,6 +1426,12 @@ display: num_chips: 1 lambda: |- it.print("01234567"); +- platform: tm1637 + clk_pin: GPIO23 + dio_pin: GPIO25 + intensity: 3 + lambda: |- + it.print("1234"); - platform: nextion lambda: |- it.set_component_value("gauge", 50); From a55787f40caf11e8c7ed83aff76dd1205c5a50f6 Mon Sep 17 00:00:00 2001 From: Mario <4376789+mario-tux@users.noreply.github.com> Date: Tue, 14 Jan 2020 10:34:49 +0100 Subject: [PATCH 147/412] Support a further variant of Xiaomi CGG1 (#930) --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 18eaffed06..030ee73d4b 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -79,7 +79,7 @@ bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracke bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; - bool is_cgg1 = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; + bool is_cgg1 = ((raw[1] & 0x30) == 0x30 || (raw[1] & 0x20) == 0x20) && raw[2] == 0x47 && raw[3] == 0x03; if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); From 2d0d794a9dab9f538d65744fddbe6ecbee6c50e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Gim=C3=A9nez?= Date: Wed, 22 Jan 2020 23:38:04 +0100 Subject: [PATCH 148/412] Daikin climate ir component (#964) * Daikin ARC43XXX IR remote controller support * Format and lint fixes * Check temperature values against allowed min/max --- esphome/components/daikin/__init__.py | 0 esphome/components/daikin/climate.py | 18 ++++ esphome/components/daikin/daikin.cpp | 128 ++++++++++++++++++++++++++ esphome/components/daikin/daikin.h | 57 ++++++++++++ tests/test1.yaml | 2 + 5 files changed, 205 insertions(+) create mode 100644 esphome/components/daikin/__init__.py create mode 100644 esphome/components/daikin/climate.py create mode 100644 esphome/components/daikin/daikin.cpp create mode 100644 esphome/components/daikin/daikin.h diff --git a/esphome/components/daikin/__init__.py b/esphome/components/daikin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py new file mode 100644 index 0000000000..f1d5c7ed4a --- /dev/null +++ b/esphome/components/daikin/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +daikin_ns = cg.esphome_ns.namespace('daikin') +DaikinClimate = daikin_ns.class_('DaikinClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(DaikinClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp new file mode 100644 index 0000000000..eabbb96014 --- /dev/null +++ b/esphome/components/daikin/daikin.cpp @@ -0,0 +1,128 @@ +#include "daikin.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace daikin { + +static const char *TAG = "daikin.climate"; + +void DaikinClimate::transmit_state() { + uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00, + 0x42, 0x49, 0x05, 0xA2, 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00}; + + remote_state[21] = this->operation_mode_(); + remote_state[24] = this->fan_speed_(); + remote_state[22] = this->temperature_(); + + // Calculate checksum + for (int i = 16; i < 34; i++) { + remote_state[34] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_IR_FREQUENCY); + + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + for (int i = 0; i < 8; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(DAIKIN_MESSAGE_SPACE); + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (int i = 8; i < 16; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(DAIKIN_MESSAGE_SPACE); + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (int i = 16; i < 35; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_MODE_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + operating_mode |= DAIKIN_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t DaikinClimate::fan_speed_() { + uint8_t fan_speed; + switch (this->fan_mode) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_FAN_1; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_FAN_3; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_FAN_5; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DAIKIN_FAN_AUTO; + } + + // If swing is enabled switch first 4 bits to 1111 + return this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? fan_speed | 0xF : fan_speed; +} + +uint8_t DaikinClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + return 25; + case climate::CLIMATE_MODE_DRY: + return 0xc0; + default: + uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); + return temperature << 1; + } +} + +} // namespace daikin +} // namespace esphome diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h new file mode 100644 index 0000000000..ea69256701 --- /dev/null +++ b/esphome/components/daikin/daikin.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin { + +// Values for Daikin ARC43XXX IR Controllers +// Temperature +const uint8_t DAIKIN_TEMP_MIN = 10; // Celsius +const uint8_t DAIKIN_TEMP_MAX = 30; // Celsius + +// Modes +const uint8_t DAIKIN_MODE_AUTO = 0x00; +const uint8_t DAIKIN_MODE_COOL = 0x30; +const uint8_t DAIKIN_MODE_HEAT = 0x40; +const uint8_t DAIKIN_MODE_DRY = 0x20; +const uint8_t DAIKIN_MODE_FAN = 0x60; +const uint8_t DAIKIN_MODE_OFF = 0x00; +const uint8_t DAIKIN_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_FAN_AUTO = 0xA0; +const uint8_t DAIKIN_FAN_1 = 0x30; +const uint8_t DAIKIN_FAN_2 = 0x40; +const uint8_t DAIKIN_FAN_3 = 0x50; +const uint8_t DAIKIN_FAN_4 = 0x60; +const uint8_t DAIKIN_FAN_5 = 0x70; + +// IR Transmission +const uint32_t DAIKIN_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_HEADER_MARK = 3360; +const uint32_t DAIKIN_HEADER_SPACE = 1760; +const uint32_t DAIKIN_BIT_MARK = 360; +const uint32_t DAIKIN_ONE_SPACE = 1370; +const uint32_t DAIKIN_ZERO_SPACE = 520; +const uint32_t DAIKIN_MESSAGE_SPACE = 32300; + +class DaikinClimate : public climate_ir::ClimateIR { + public: + DaikinClimate() + : climate_ir::ClimateIR( + DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, + std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t operation_mode_(); + uint8_t fan_speed_(); + uint8_t temperature_(); +}; + +} // namespace daikin +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index fb15bc0a6b..0533063fb8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1190,6 +1190,8 @@ climate: name: Coolix Climate - platform: fujitsu_general name: Fujitsu General Climate + - platform: daikin + name: Daikin Climate - platform: yashima name: Yashima Climate - platform: mitsubishi From 499903bd3dd24915774942e70ae25aa4bf2c6283 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 4 Feb 2020 22:35:41 -0300 Subject: [PATCH 149/412] fix tm1637 missing __init__.py (#975) --- esphome/components/tm1637/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 esphome/components/tm1637/__init__.py diff --git a/esphome/components/tm1637/__init__.py b/esphome/components/tm1637/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From e6f21873c33703c8f82eddec080241c607c3f3c5 Mon Sep 17 00:00:00 2001 From: Andrzej Date: Sat, 8 Feb 2020 18:03:24 +0100 Subject: [PATCH 150/412] sim800l: Add support of roaming-registered SIM cards (#977) * Add support of roaming-registered cards * Change or to || --- esphome/components/sim800l/sim800l.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 1390ef8b49..9f8c733fa9 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -97,7 +97,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_CREGWAIT: { // Response: "+CREG: 0,1" -- the one there means registered ok // "+CREG: -,-" means not registered ok - bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1'; + bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); if (registered) { if (!this->registered_) ESP_LOGD(TAG, "Registered OK"); From 7721049ed787958437818da2690169a85b926d0d Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Sat, 8 Feb 2020 18:10:07 +0100 Subject: [PATCH 151/412] BME280: fix typos, use forced mode constant (#974) * Fix typo in BME280 chip ID error message * Use BME280 forced mode constant instead of literal --- esphome/components/bme280/bme280.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index b7c7f12f6f..6bb5ac9800 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -146,7 +146,7 @@ void BME280Component::dump_config() { ESP_LOGE(TAG, "Communication with BME280 failed!"); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BMP280?"); + ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); break; case NONE: default: @@ -172,7 +172,7 @@ void BME280Component::update() { uint8_t meas_register = 0; meas_register |= (this->temperature_oversampling_ & 0b111) << 5; meas_register |= (this->pressure_oversampling_ & 0b111) << 2; - meas_register |= 0b01; // Forced mode + meas_register |= BME280_MODE_FORCED; if (!this->write_byte(BME280_REGISTER_CONTROL, meas_register)) { this->status_set_warning(); return; From 1d136ab0df69ab08da266671f472a518fd7caae0 Mon Sep 17 00:00:00 2001 From: puuu Date: Sun, 9 Feb 2020 21:20:56 +0900 Subject: [PATCH 152/412] MQTT climate features (#913) * mqtt_climate: add action support * mqtt_climate: add fan and swing mode support * mqtt_climate: reduce length of discovery payload by using abbreviations https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mqtt/abbreviations.py --- esphome/components/mqtt/mqtt_climate.cpp | 168 +++++++++++++++++++++-- esphome/components/mqtt/mqtt_climate.h | 5 + 2 files changed, 162 insertions(+), 11 deletions(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 3f097d9c07..2a95ed2f64 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -14,12 +14,13 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - root["current_temperature_topic"] = this->get_current_temperature_state_topic(); + // current_temperature_topic + root["curr_temp_t"] = this->get_current_temperature_state_topic(); } // mode_command_topic - root["mode_command_topic"] = this->get_mode_command_topic(); + root["mode_cmd_t"] = this->get_mode_command_topic(); // mode_state_topic - root["mode_state_topic"] = this->get_mode_state_topic(); + root["mode_stat_t"] = this->get_mode_state_topic(); // modes JsonArray &modes = root.createNestedArray("modes"); // sort array for nice UI in HA @@ -37,18 +38,18 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic - root["temperature_low_command_topic"] = this->get_target_temperature_low_command_topic(); + root["temp_lo_cmd_t"] = this->get_target_temperature_low_command_topic(); // temperature_low_state_topic - root["temperature_low_state_topic"] = this->get_target_temperature_low_state_topic(); + root["temp_lo_stat_t"] = this->get_target_temperature_low_state_topic(); // temperature_high_command_topic - root["temperature_high_command_topic"] = this->get_target_temperature_high_command_topic(); + root["temp_hi_cmd_t"] = this->get_target_temperature_high_command_topic(); // temperature_high_state_topic - root["temperature_high_state_topic"] = this->get_target_temperature_high_state_topic(); + root["temp_hi_stat_t"] = this->get_target_temperature_high_state_topic(); } else { // temperature_command_topic - root["temperature_command_topic"] = this->get_target_temperature_command_topic(); + root["temp_cmd_t"] = this->get_target_temperature_command_topic(); // temperature_state_topic - root["temperature_state_topic"] = this->get_target_temperature_state_topic(); + root["temp_stat_t"] = this->get_target_temperature_state_topic(); } // min_temp @@ -60,10 +61,59 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC if (traits.get_supports_away()) { // away_mode_command_topic - root["away_mode_command_topic"] = this->get_away_command_topic(); + root["away_mode_cmd_t"] = this->get_away_command_topic(); // away_mode_state_topic - root["away_mode_state_topic"] = this->get_away_state_topic(); + root["away_mode_stat_t"] = this->get_away_state_topic(); } + if (traits.get_supports_action()) { + // action_topic + root["act_t"] = this->get_action_state_topic(); + } + + if (traits.get_supports_fan_modes()) { + // fan_mode_command_topic + root["fan_mode_cmd_t"] = this->get_fan_mode_command_topic(); + // fan_mode_state_topic + root["fan_mode_stat_t"] = this->get_fan_mode_state_topic(); + // fan_modes + JsonArray &fan_modes = root.createNestedArray("fan_modes"); + if (traits.supports_fan_mode(CLIMATE_FAN_ON)) + fan_modes.add("on"); + if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) + fan_modes.add("off"); + if (traits.supports_fan_mode(CLIMATE_FAN_AUTO)) + fan_modes.add("auto"); + if (traits.supports_fan_mode(CLIMATE_FAN_LOW)) + fan_modes.add("low"); + if (traits.supports_fan_mode(CLIMATE_FAN_MEDIUM)) + fan_modes.add("medium"); + if (traits.supports_fan_mode(CLIMATE_FAN_HIGH)) + fan_modes.add("high"); + if (traits.supports_fan_mode(CLIMATE_FAN_MIDDLE)) + fan_modes.add("middle"); + if (traits.supports_fan_mode(CLIMATE_FAN_FOCUS)) + fan_modes.add("focus"); + if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) + fan_modes.add("diffuse"); + } + + if (traits.get_supports_swing_modes()) { + // swing_mode_command_topic + root["swing_mode_cmd_t"] = this->get_swing_mode_command_topic(); + // swing_mode_state_topic + root["swing_mode_stat_t"] = this->get_swing_mode_state_topic(); + // swing_modes + JsonArray &swing_modes = root.createNestedArray("swing_modes"); + if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) + swing_modes.add("off"); + if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) + swing_modes.add("both"); + if (traits.supports_swing_mode(CLIMATE_SWING_VERTICAL)) + swing_modes.add("vertical"); + if (traits.supports_swing_mode(CLIMATE_SWING_HORIZONTAL)) + swing_modes.add("horizontal"); + } + config.state_topic = false; config.command_topic = false; } @@ -135,6 +185,22 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_fan_modes()) { + this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto call = this->device_->make_call(); + call.set_fan_mode(payload); + call.perform(); + }); + } + + if (traits.get_supports_swing_modes()) { + this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto call = this->device_->make_call(); + call.set_swing_mode(payload); + call.perform(); + }); + } + this->device_->add_on_state_callback([this]() { this->publish_state_(); }); } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} @@ -193,6 +259,86 @@ bool MQTTClimateComponent::publish_state_() { if (!this->publish(this->get_away_state_topic(), payload)) success = false; } + if (traits.get_supports_action()) { + const char *payload = "unknown"; + switch (this->device_->action) { + case CLIMATE_ACTION_OFF: + payload = "off"; + break; + case CLIMATE_ACTION_COOLING: + payload = "cooling"; + break; + case CLIMATE_ACTION_HEATING: + payload = "heating"; + break; + case CLIMATE_ACTION_IDLE: + payload = "idle"; + break; + case CLIMATE_ACTION_DRYING: + payload = "drying"; + break; + case CLIMATE_ACTION_FAN: + payload = "fan"; + break; + } + if (!this->publish(this->get_action_state_topic(), payload)) + success = false; + } + + if (traits.get_supports_fan_modes()) { + const char *payload = ""; + switch (this->device_->fan_mode) { + case CLIMATE_FAN_ON: + payload = "on"; + break; + case CLIMATE_FAN_OFF: + payload = "off"; + break; + case CLIMATE_FAN_AUTO: + payload = "auto"; + break; + case CLIMATE_FAN_LOW: + payload = "low"; + break; + case CLIMATE_FAN_MEDIUM: + payload = "medium"; + break; + case CLIMATE_FAN_HIGH: + payload = "high"; + break; + case CLIMATE_FAN_MIDDLE: + payload = "middle"; + break; + case CLIMATE_FAN_FOCUS: + payload = "focus"; + break; + case CLIMATE_FAN_DIFFUSE: + payload = "diffuse"; + break; + } + if (!this->publish(this->get_fan_mode_state_topic(), payload)) + success = false; + } + + if (traits.get_supports_swing_modes()) { + const char *payload = ""; + switch (this->device_->swing_mode) { + case CLIMATE_SWING_OFF: + payload = "off"; + break; + case CLIMATE_SWING_BOTH: + payload = "both"; + break; + case CLIMATE_SWING_VERTICAL: + payload = "vertical"; + break; + case CLIMATE_SWING_HORIZONTAL: + payload = "horizontal"; + break; + } + if (!this->publish(this->get_swing_mode_state_topic(), payload)) + success = false; + } return success; } diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 2d6fe3cc5e..8aea4feb26 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -30,6 +30,11 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) + MQTT_COMPONENT_CUSTOM_TOPIC(action, state) + MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, state) + MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, command) + MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, state) + MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, command) protected: std::string friendly_name() const override; From 6ae1efcf9fc00cb61eff0d9c28413d0f91525b88 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 16 Feb 2020 00:48:08 +0100 Subject: [PATCH 153/412] Revert ESP32 default upload speed to 115200 (#978) --- esphome/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/writer.py b/esphome/writer.py index b3a60c5de9..67b1332e8f 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -190,7 +190,7 @@ def get_ini_content(): 'framework': 'arduino', 'lib_deps': lib_deps + ['${common.lib_deps}'], 'build_flags': build_flags + ['${common.build_flags}'], - 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 460800), + 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 115200), } if CORE.is_esp32: From 4402a6eb4cc6db389b7bf9dffe86eb2a26a3804f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 16 Feb 2020 00:52:20 +0100 Subject: [PATCH 154/412] Add TM1651 simple level, turn on, turn off actions (#920) * Add TM1651 simple level action * fixed brightness validation * Updated lib, fixed import * Added turn_on, turn_off actions * Fixed after lint --- esphome/components/tm1651/__init__.py | 70 +++++++++++++++++++++++---- esphome/components/tm1651/tm1651.cpp | 26 ++++++++-- esphome/components/tm1651/tm1651.h | 27 +++++++++++ tests/test3.yaml | 15 ++++++ 4 files changed, 125 insertions(+), 13 deletions(-) diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index d83ef4b3b7..aa972552f4 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -1,13 +1,19 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation +from esphome.automation import maybe_simple_id from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_DIO_PIN, CONF_LEVEL, CONF_BRIGHTNESS tm1651_ns = cg.esphome_ns.namespace('tm1651') TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) + +SetLevelPercentAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) SetLevelAction = tm1651_ns.class_('SetLevelAction', automation.Action) SetBrightnessAction = tm1651_ns.class_('SetBrightnessAction', automation.Action) -validate_level = cv.All(cv.int_range(min=0, max=100)) +TurnOnAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) +TurnOffAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) + +CONF_LEVEL_PERCENT = 'level_percent' TM1651_BRIGHTNESS_OPTIONS = { 1: TM1651Display.TM1651_BRIGHTNESS_LOW, @@ -21,6 +27,10 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, }) +validate_level_percent = cv.All(cv.int_range(min=0, max=100)) +validate_level = cv.All(cv.int_range(min=0, max=7)) +validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) + def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -32,13 +42,50 @@ def to_code(config): cg.add(var.set_dio_pin(dio_pin)) # https://platformio.org/lib/show/6865/TM1651 - cg.add_library('6865', '1.0.0') + cg.add_library('6865', '1.0.1') -@automation.register_action('tm1651.set_level', SetLevelAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_LEVEL): cv.templatable(validate_level), -}, key=CONF_LEVEL)) +BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(TM1651Display), +}) + + +@automation.register_action('tm1651.turn_on', TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +def output_turn_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('tm1651.turn_off', TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA) +def output_turn_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action( + 'tm1651.set_level_percent', + SetLevelPercentAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), + }, key=CONF_LEVEL_PERCENT)) +def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) + cg.add(var.set_level_percent(template_)) + yield var + + +@automation.register_action( + 'tm1651.set_level', + SetLevelAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL): cv.templatable(validate_level), + }, key=CONF_LEVEL)) def tm1651_set_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) yield cg.register_parented(var, config[CONF_ID]) @@ -47,10 +94,13 @@ def tm1651_set_level_to_code(config, action_id, template_arg, args): yield var -@automation.register_action('tm1651.set_brightness', SetBrightnessAction, cv.maybe_simple_value({ - cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_level), -}, key=CONF_BRIGHTNESS)) +@automation.register_action( + 'tm1651.set_brightness', + SetBrightnessAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), + }, key=CONF_BRIGHTNESS)) def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) yield cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index 594ebe9db9..0417706327 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -5,8 +5,9 @@ namespace esphome { namespace tm1651 { static const char *TAG = "tm1651.display"; + +static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; static const uint8_t TM1651_MAX_LEVEL = 7; -static const uint8_t MAX_INPUT_LEVEL = 100; static const uint8_t TM1651_BRIGHTNESS_LOW = 0; static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2; @@ -29,17 +30,36 @@ void TM1651Display::dump_config() { LOG_PIN(" DIO: ", dio_pin_); } -void TM1651Display::set_level(uint8_t new_level) { +void TM1651Display::set_level_percent(uint8_t new_level) { this->level_ = calculate_level_(new_level); this->repaint_(); } +void TM1651Display::set_level(uint8_t new_level) { + this->level_ = new_level; + this->repaint_(); +} + void TM1651Display::set_brightness(uint8_t new_brightness) { this->brightness_ = calculate_brightness_(new_brightness); this->repaint_(); } +void TM1651Display::turn_on() { + this->is_on_ = true; + this->repaint_(); +} + +void TM1651Display::turn_off() { + this->is_on_ = false; + battery_display_->displayLevel(0); +} + void TM1651Display::repaint_() { + if (!this->is_on_) { + return; + } + battery_display_->set(this->brightness_); battery_display_->displayLevel(this->level_); } @@ -49,7 +69,7 @@ uint8_t TM1651Display::calculate_level_(uint8_t new_level) { return 0; } - float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL / (float) new_level); + float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL_PERCENT / (float) new_level); return (uint8_t) roundf(calculated_level); } diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index d75c2adb62..6eab24687c 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -17,13 +17,18 @@ class TM1651Display : public Component { void setup() override; void dump_config() override; + void set_level_percent(uint8_t); void set_level(uint8_t); void set_brightness(uint8_t); + void turn_on(); + void turn_off(); + protected: TM1651 *battery_display_; GPIOPin *clk_pin_; GPIOPin *dio_pin_; + bool is_on_ = true; uint8_t brightness_; uint8_t level_; @@ -34,9 +39,20 @@ class TM1651Display : public Component { uint8_t calculate_brightness_(uint8_t); }; +template class SetLevelPercentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level_percent) + + void play(Ts... x) override { + auto level_percent = this->level_percent_.value(x...); + this->parent_->set_level_percent(level_percent); + } +}; + template class SetLevelAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, level) + void play(Ts... x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); @@ -46,11 +62,22 @@ template class SetLevelAction : public Action, public Par template class SetBrightnessAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, brightness) + void play(Ts... x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } }; +template class TurnOnAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->turn_on(); } +}; + +template class TurnOffAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->turn_off(); } +}; + } // namespace tm1651 } // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 77a0fa631c..9c66242b4e 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -138,6 +138,13 @@ api: then: - dfplayer.random + - service: battery_level_percent + variables: + level_percent: int + then: + - tm1651.set_level_percent: + id: tm1651_battery + level_percent: !lambda 'return level_percent;' - service: battery_level variables: level: int @@ -152,6 +159,14 @@ api: - tm1651.set_brightness: id: tm1651_battery brightness: !lambda 'return brightness;' + - service: battery_turn_on + then: + - tm1651.turn_on: + id: tm1651_battery + - service: battery_turn_on + then: + - tm1651.turn_off: + id: tm1651_battery wifi: ssid: 'MySSID' From 67cbaabd99ef05b3b9f9510220766bdc3aa6625e Mon Sep 17 00:00:00 2001 From: Elkropac Date: Thu, 20 Feb 2020 13:05:10 +0100 Subject: [PATCH 155/412] Webserver - include css, js in index (#932) * add new config options * init variables in code * load css and js in python * update print inside webserver * fix indentation * fix indentation * indentation fix * fix condition in init * use cv.file_ instead of cv.string * do not import EsphomeError * support embedding js and css at the same time as defined in url * handle css as separate page * handle js as separate page * fix copy and paste error --- esphome/components/web_server/__init__.py | 12 ++- esphome/components/web_server/web_server.cpp | 77 ++++++++++++++++++-- esphome/components/web_server/web_server.h | 24 ++++++ esphome/const.py | 2 + 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 04f3cc5c04..2f0d179eba 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.const import ( - CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT, + CONF_CSS_INCLUDE, CONF_CSS_URL, CONF_ID, CONF_JS_INCLUDE, CONF_JS_URL, CONF_PORT, CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) from esphome.core import coroutine_with_priority @@ -16,7 +16,9 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(WebServer), cv.Optional(CONF_PORT, default=80): cv.port, cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string, + cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, + cv.Optional(CONF_JS_INCLUDE): cv.file_, cv.Optional(CONF_AUTH): cv.Schema({ cv.Required(CONF_USERNAME): cv.string_strict, cv.Required(CONF_PASSWORD): cv.string_strict, @@ -39,3 +41,11 @@ def to_code(config): if CONF_AUTH in config: cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) + if CONF_CSS_INCLUDE in config: + cg.add_define('WEBSERVER_CSS_INCLUDE') + with open(config[CONF_CSS_INCLUDE], "r") as myfile: + cg.add(var.set_css_include(myfile.read())) + if CONF_JS_INCLUDE in config: + cg.add_define('WEBSERVER_JS_INCLUDE') + with open(config[CONF_JS_INCLUDE], "r") as myfile: + cg.add(var.set_js_include(myfile.read())) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index fcd83297e2..c0708b763f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -60,7 +60,9 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) { } void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } +void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } +void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); @@ -133,9 +135,16 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { std::string title = App.get_name() + " Web Server"; stream->print(F("")); stream->print(title.c_str()); - stream->print(F("print(this->css_url_); - stream->print(F("\">

")); + stream->print(F("")); +#ifdef WEBSERVER_CSS_INCLUDE + stream->print(F("")); +#endif + if (strlen(this->css_url_) > 0) { + stream->print(F("print(this->css_url_); + stream->print(F("\">")); + } + stream->print(F("

")); stream->print(title.c_str()); stream->print(F("

States

")); // All content is controlled and created by user - so allowing all origins is fine here. @@ -175,14 +184,44 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { "REST API documentation.

" "

OTA Update

" - "

Debug Log

"
-                  ""));
+                  "

Debug Log

"));
+#ifdef WEBSERVER_JS_INCLUDE
+  if (this->js_include_ != nullptr) {
+    stream->print(F(""));
+  }
+#endif
+  if (strlen(this->js_url_) > 0) {
+    stream->print(F(""));
+  }
+  stream->print(F(""));
 
   request->send(stream);
 }
 
+#ifdef WEBSERVER_CSS_INCLUDE
+void WebServer::handle_css_request(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/css");
+  if (this->css_include_ != nullptr) {
+    stream->print(this->css_include_);
+  }
+
+  request->send(stream);
+}
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+void WebServer::handle_js_request(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/javascript");
+  if (this->js_include_ != nullptr) {
+    stream->print(this->js_include_);
+  }
+
+  request->send(stream);
+}
+#endif
+
 #ifdef USE_SENSOR
 void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
   this->events_.send(this->sensor_json(obj, state).c_str(), "state");
@@ -460,6 +499,16 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
   if (request->url() == "/")
     return true;
 
+#ifdef WEBSERVER_CSS_INCLUDE
+  if (request->url() == "/0.css")
+    return true;
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  if (request->url() == "/0.js")
+    return true;
+#endif
+
   UrlMatch match = match_url(request->url().c_str(), true);
   if (!match.valid)
     return false;
@@ -505,6 +554,20 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
     return;
   }
 
+#ifdef WEBSERVER_CSS_INCLUDE
+  if (request->url() == "/0.css") {
+    this->handle_css_request(request);
+    return;
+  }
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  if (request->url() == "/0.js") {
+    this->handle_js_request(request);
+    return;
+  }
+#endif
+
   UrlMatch match = match_url(request->url().c_str());
 #ifdef USE_SENSOR
   if (match.domain == "sensor") {
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 4dca8200cc..def1cac0ea 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -41,6 +41,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
    */
   void set_css_url(const char *css_url);
 
+  /** Set local path to the script that's embedded in the index page. Defaults to
+   *
+   * @param css_include Local path to web server script.
+   */
+  void set_css_include(const char *css_include);
+
   /** Set the URL to the script that's embedded in the index page. Defaults to
    * https://esphome.io/_static/webserver-v1.min.js
    *
@@ -48,6 +54,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
    */
   void set_js_url(const char *js_url);
 
+  /** Set local path to the script that's embedded in the index page. Defaults to
+   *
+   * @param js_include Local path to web server script.
+   */
+  void set_js_include(const char *js_include);
+
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   /// Setup the internal web server and register handlers.
@@ -61,6 +73,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   /// Handle an index request under '/'.
   void handle_index_request(AsyncWebServerRequest *request);
 
+#ifdef WEBSERVER_CSS_INCLUDE
+  /// Handle included css request under '/0.css'.
+  void handle_css_request(AsyncWebServerRequest *request);
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  /// Handle included js request under '/0.js'.
+  void handle_js_request(AsyncWebServerRequest *request);
+#endif
+
   bool using_auth() { return username_ != nullptr && password_ != nullptr; }
 
 #ifdef USE_SENSOR
@@ -135,7 +157,9 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   const char *username_{nullptr};
   const char *password_{nullptr};
   const char *css_url_{nullptr};
+  const char *css_include_{nullptr};
   const char *js_url_{nullptr};
+  const char *js_include_{nullptr};
 };
 
 }  // namespace web_server
diff --git a/esphome/const.py b/esphome/const.py
index 0c5f286f30..308dd2683f 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -103,6 +103,7 @@ CONF_COOL_ACTION = 'cool_action'
 CONF_COUNT_MODE = 'count_mode'
 CONF_CRON = 'cron'
 CONF_CS_PIN = 'cs_pin'
+CONF_CSS_INCLUDE = 'css_include'
 CONF_CSS_URL = 'css_url'
 CONF_CURRENT = 'current'
 CONF_CURRENT_OPERATION = 'current_operation'
@@ -210,6 +211,7 @@ CONF_INVALID_COOLDOWN = 'invalid_cooldown'
 CONF_INVERT = 'invert'
 CONF_INVERTED = 'inverted'
 CONF_IP_ADDRESS = 'ip_address'
+CONF_JS_INCLUDE = 'js_include'
 CONF_JS_URL = 'js_url'
 CONF_JVC = 'jvc'
 CONF_KEEP_ON_TIME = 'keep_on_time'

From eb895d2095861a4d51f1a5fcd582a97389c27b4f Mon Sep 17 00:00:00 2001
From: Erwin Kooi 
Date: Mon, 2 Mar 2020 23:56:25 +0100
Subject: [PATCH 156/412]  	Added equal symbol for MAX7219 7-segment
 display (#986)

---
 esphome/components/max7219/max7219.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp
index 6af8982c33..6616da1a58 100644
--- a/esphome/components/max7219/max7219.cpp
+++ b/esphome/components/max7219/max7219.cpp
@@ -44,7 +44,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
     0b01001000,            // ':', ord 0x3A
     0b01011000,            // ';', ord 0x3B
     MAX7219_UNKNOWN_CHAR,  // '<', ord 0x3C
-    MAX7219_UNKNOWN_CHAR,  // '=', ord 0x3D
+    0b00001001,            // '=', ord 0x3D
     MAX7219_UNKNOWN_CHAR,  // '>', ord 0x3E
     0b01100101,            // '?', ord 0x3F
     0b01101111,            // '@', ord 0x40

From a4ab52918b60410039b57751070c2788725ab6b8 Mon Sep 17 00:00:00 2001
From: Brandon Davidson 
Date: Wed, 11 Mar 2020 17:16:05 -0700
Subject: [PATCH 157/412] Output from platformio click command does not need to
 be decoded (#953)

---
 esphome/platformio_api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index 59f5bf20ae..29bfe7d2e6 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -98,7 +98,7 @@ def run_upload(config, verbose, port):
 
 def run_idedata(config):
     args = ['-t', 'idedata']
-    stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True).decode()
+    stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True)
     match = re.search(r'{\s*".*}', stdout)
     if match is None:
         _LOGGER.debug("Could not match IDEData for %s", stdout)

From 854d735ab3b72032d5163ad20ed5d763deff0dc2 Mon Sep 17 00:00:00 2001
From: Brandon Davidson 
Date: Wed, 11 Mar 2020 17:16:33 -0700
Subject: [PATCH 158/412] Allow custom lights to be addressable (#954)

---
 esphome/components/custom/light/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/custom/light/__init__.py b/esphome/components/custom/light/__init__.py
index 24b284941e..61dd74e661 100644
--- a/esphome/components/custom/light/__init__.py
+++ b/esphome/components/custom/light/__init__.py
@@ -10,7 +10,7 @@ CONF_LIGHTS = 'lights'
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(CustomLightOutputConstructor),
     cv.Required(CONF_LAMBDA): cv.returning_lambda,
-    cv.Required(CONF_LIGHTS): cv.ensure_list(light.RGB_LIGHT_SCHEMA),
+    cv.Required(CONF_LIGHTS): cv.ensure_list(light.ADDRESSABLE_LIGHT_SCHEMA),
 })
 
 

From 11069085e3819dac3adb62f3f41df8004e27c10d Mon Sep 17 00:00:00 2001
From: Paul Nicholls 
Date: Thu, 12 Mar 2020 13:17:29 +1300
Subject: [PATCH 159/412] Fix esphome/issues#947 - RGBW(W) white brightness
 (#925)

---
 esphome/components/light/light_color_values.h | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h
index cda86d3f1c..9ac8be1dd0 100644
--- a/esphome/components/light/light_color_values.h
+++ b/esphome/components/light/light_color_values.h
@@ -185,7 +185,7 @@ class LightColorValues {
   /// Convert these light color values to an RGBW representation and write them to red, green, blue, white.
   void as_rgbw(float *red, float *green, float *blue, float *white) const {
     this->as_rgb(red, green, blue);
-    *white = this->state_ * this->white_;
+    *white = this->state_ * this->brightness_ * this->white_;
   }
 
   /// Convert these light color values to an RGBWW representation with the given parameters.
@@ -196,8 +196,8 @@ class LightColorValues {
     const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw);
     const float cw_fraction = 1.0f - ww_fraction;
     const float max_cw_ww = std::max(ww_fraction, cw_fraction);
-    *cold_white = this->state_ * this->white_ * (cw_fraction / max_cw_ww);
-    *warm_white = this->state_ * this->white_ * (ww_fraction / max_cw_ww);
+    *cold_white = this->state_ * this->brightness_ * this->white_ * (cw_fraction / max_cw_ww);
+    *warm_white = this->state_ * this->brightness_ * this->white_ * (ww_fraction / max_cw_ww);
   }
 
   /// Convert these light color values to an CWWW representation with the given parameters.

From 7f2a6e74038beaf729c7acfc8f7a0aba9651d810 Mon Sep 17 00:00:00 2001
From: Thomas Klingbeil 
Date: Thu, 12 Mar 2020 01:19:01 +0100
Subject: [PATCH 160/412] Add support for TTGO epaper boards with B73 revision
 (#928)

* Add support for TTGO epaper boards with B73 revision
---
 .../components/waveshare_epaper/display.py    |  1 +
 .../waveshare_epaper/waveshare_epaper.cpp     | 31 +++++++++++++++++++
 .../waveshare_epaper/waveshare_epaper.h       |  1 +
 3 files changed, 33 insertions(+)

diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index 343059d0c1..77322cbb70 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -23,6 +23,7 @@ MODELS = {
     '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN),
     '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN),
     '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN),
+    '2.13in-ttgo-b73': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73),
     '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN),
     '2.70in': ('b', WaveshareEPaper2P7In),
     '2.90in-b': ('b', WaveshareEPaper2P9InB),
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index ff29df4444..fd869b46ec 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -18,6 +18,8 @@ static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {
     0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
 
 static const uint8_t LUT_SIZE_TTGO = 70;
+static const uint8_t LUT_SIZE_TTGO_B73 = 100;
+
 static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = {
     0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00,  // LUT0: BB:     VS 0 ~7
     0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00,  // LUT1: BW:     VS 0 ~7
@@ -33,6 +35,26 @@ static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = {
     0x00, 0x00, 0x00, 0x00, 0x00,              // TP6 A~D RP6
 };
 
+static const uint8_t FULL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = {
+    0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+
+    0x0F, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x03, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = {
+    0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+
+    0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
 static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = {
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT0: BB:     VS 0 ~7
     0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT1: BW:     VS 0 ~7
@@ -169,6 +191,9 @@ void WaveshareEPaperTypeA::dump_config() {
     case TTGO_EPAPER_2_13_IN:
       ESP_LOGCONFIG(TAG, "  Model: 2.13in (TTGO)");
       break;
+    case TTGO_EPAPER_2_13_IN_B73:
+      ESP_LOGCONFIG(TAG, "  Model: 2.13in (TTGO B73)");
+      break;
     case WAVESHARE_EPAPER_2_9_IN:
       ESP_LOGCONFIG(TAG, "  Model: 2.9in");
       break;
@@ -191,6 +216,8 @@ void HOT WaveshareEPaperTypeA::display() {
     if (full_update != prev_full_update) {
       if (this->model_ == TTGO_EPAPER_2_13_IN) {
         this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO);
+      } else if (this->model_ == TTGO_EPAPER_2_13_IN_B73) {
+        this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B73 : PARTIAL_UPDATE_LUT_TTGO_B73, LUT_SIZE_TTGO_B73);
       } else {
         this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE);
       }
@@ -247,6 +274,8 @@ int WaveshareEPaperTypeA::get_width_internal() {
       return 128;
     case TTGO_EPAPER_2_13_IN:
       return 128;
+    case TTGO_EPAPER_2_13_IN_B73:
+      return 128;
     case WAVESHARE_EPAPER_2_9_IN:
       return 128;
   }
@@ -260,6 +289,8 @@ int WaveshareEPaperTypeA::get_height_internal() {
       return 250;
     case TTGO_EPAPER_2_13_IN:
       return 250;
+    case TTGO_EPAPER_2_13_IN_B73:
+      return 250;
     case WAVESHARE_EPAPER_2_9_IN:
       return 296;
   }
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index 46fe465c5b..c21cabcaf4 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -68,6 +68,7 @@ enum WaveshareEPaperTypeAModel {
   WAVESHARE_EPAPER_2_13_IN,
   WAVESHARE_EPAPER_2_9_IN,
   TTGO_EPAPER_2_13_IN,
+  TTGO_EPAPER_2_13_IN_B73,
 };
 
 class WaveshareEPaperTypeA : public WaveshareEPaper {

From 3c68348868e8eafc0858df26cf2dd1a3dec89377 Mon Sep 17 00:00:00 2001
From: Niklas Wagner 
Date: Thu, 12 Mar 2020 01:20:27 +0100
Subject: [PATCH 161/412] Fix OTA updates getting killed by task_wdt (#959)

---
 esphome/components/ota/ota_component.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp
index 2041c688eb..b614139e07 100644
--- a/esphome/components/ota/ota_component.cpp
+++ b/esphome/components/ota/ota_component.cpp
@@ -241,6 +241,8 @@ void OTAComponent::handle_() {
       last_progress = now;
       float percentage = (total * 100.0f) / ota_size;
       ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
+      // slow down OTA update to avoid getting killed by task watchdog (task_wdt)
+      delay(10);
     }
   }
 

From aff4f1e9e2d613436af73a29299256a207655216 Mon Sep 17 00:00:00 2001
From: Tim Savage 
Date: Thu, 12 Mar 2020 11:22:45 +1100
Subject: [PATCH 162/412] Bugfix/1077 decode called on str fetching platformio
 stacktrace (#991)

* Remove decode from str result, add type annotations
---
 esphome/platformio_api.py |  6 ++++--
 esphome/util.py           | 20 +++++++++++++++++---
 2 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index 29bfe7d2e6..4866119902 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -1,4 +1,6 @@
 import json
+from typing import Union
+
 import logging
 import os
 import re
@@ -62,7 +64,7 @@ FILTER_PLATFORMIO_LINES = [
 ]
 
 
-def run_platformio_cli(*args, **kwargs):
+def run_platformio_cli(*args, **kwargs) -> Union[str, int]:
     os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
     os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path())
     os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath(CORE.relative_piolibdeps_path())
@@ -80,7 +82,7 @@ def run_platformio_cli(*args, **kwargs):
                                 *cmd, **kwargs)
 
 
-def run_platformio_cli_run(config, verbose, *args, **kwargs):
+def run_platformio_cli_run(config, verbose, *args, **kwargs) -> Union[str, int]:
     command = ['run', '-d', CORE.build_path]
     if verbose:
         command += ['-v']
diff --git a/esphome/util.py b/esphome/util.py
index 10c8d4e581..de6736096c 100644
--- a/esphome/util.py
+++ b/esphome/util.py
@@ -1,3 +1,5 @@
+from typing import Union
+
 import collections
 import io
 import logging
@@ -151,7 +153,21 @@ class RedirectText:
         return True
 
 
-def run_external_command(func, *cmd, **kwargs):
+def run_external_command(func, *cmd,
+                         capture_stdout: bool = False,
+                         filter_lines: str = None) -> Union[int, str]:
+    """
+    Run a function from an external package that acts like a main method.
+
+    Temporarily replaces stdin/stderr/stdout, sys.argv and sys.exit handler during the run.
+
+    :param func: Function to execute
+    :param cmd: Command to run as (eg first element of sys.argv)
+    :param capture_stdout: Capture text from stdout and return that.
+    :param filter_lines: Regular expression used to filter captured output.
+    :return: str if `capture_stdout` is set else int exit code.
+
+    """
     def mock_exit(return_code):
         raise SystemExit(return_code)
 
@@ -160,13 +176,11 @@ def run_external_command(func, *cmd, **kwargs):
     full_cmd = ' '.join(shlex_quote(x) for x in cmd)
     _LOGGER.info("Running:  %s", full_cmd)
 
-    filter_lines = kwargs.get('filter_lines')
     orig_stdout = sys.stdout
     sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
     orig_stderr = sys.stderr
     sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
 
-    capture_stdout = kwargs.get('capture_stdout', False)
     if capture_stdout:
         cap_stdout = sys.stdout = io.StringIO()
 

From 66083c5e973d15262fb9921bc84681ac524d342c Mon Sep 17 00:00:00 2001
From: buxtronix 
Date: Thu, 12 Mar 2020 11:24:05 +1100
Subject: [PATCH 163/412] Add support for Tuya ceiling fan controllers (#989)

* Add support for Tuya ceiling fan controllers
---
 esphome/components/tuya/fan/__init__.py   | 39 ++++++++++
 esphome/components/tuya/fan/tuya_fan.cpp  | 90 +++++++++++++++++++++++
 esphome/components/tuya/fan/tuya_fan.h    | 34 +++++++++
 esphome/components/tuya/light/__init__.py |  7 +-
 4 files changed, 167 insertions(+), 3 deletions(-)
 create mode 100644 esphome/components/tuya/fan/__init__.py
 create mode 100644 esphome/components/tuya/fan/tuya_fan.cpp
 create mode 100644 esphome/components/tuya/fan/tuya_fan.h

diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py
new file mode 100644
index 0000000000..8b4a0fa25f
--- /dev/null
+++ b/esphome/components/tuya/fan/__init__.py
@@ -0,0 +1,39 @@
+from esphome.components import fan
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import CONF_OUTPUT_ID
+from .. import tuya_ns, CONF_TUYA_ID, Tuya
+
+DEPENDENCIES = ['tuya']
+
+CONF_SPEED_DATAPOINT = "speed_datapoint"
+CONF_SWITCH_DATAPOINT = "switch_datapoint"
+CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint"
+
+TuyaFan = tuya_ns.class_('TuyaFan', cg.Component)
+
+CONFIG_SCHEMA = cv.All(fan.FAN_SCHEMA.extend({
+    cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
+    cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
+    cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
+    cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
+    cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
+}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(
+    CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
+    yield cg.register_component(var, config)
+
+    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    fan_ = yield fan.create_fan_state(config)
+    cg.add(var.set_tuya_parent(paren))
+    cg.add(var.set_fan(fan_))
+
+    if CONF_SPEED_DATAPOINT in config:
+        cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT]))
+    if CONF_SWITCH_DATAPOINT in config:
+        cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
+    if CONF_OSCILLATION_DATAPOINT in config:
+        cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT]))
diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp
new file mode 100644
index 0000000000..b9fe2c0829
--- /dev/null
+++ b/esphome/components/tuya/fan/tuya_fan.cpp
@@ -0,0 +1,90 @@
+#include "esphome/core/log.h"
+#include "tuya_fan.h"
+
+namespace esphome {
+namespace tuya {
+
+static const char *TAG = "tuya.fan";
+
+void TuyaFan::setup() {
+  auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value());
+  this->fan_->set_traits(traits);
+
+  if (this->speed_id_.has_value()) {
+    this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->fan_->make_call();
+      if (datapoint.value_enum == 0x0)
+        call.set_speed(fan::FAN_SPEED_LOW);
+      else if (datapoint.value_enum == 0x1)
+        call.set_speed(fan::FAN_SPEED_MEDIUM);
+      else if (datapoint.value_enum == 0x2)
+        call.set_speed(fan::FAN_SPEED_HIGH);
+      else
+        ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum);
+      ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum);
+      call.perform();
+    });
+  }
+  if (this->switch_id_.has_value()) {
+    this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->fan_->make_call();
+      call.set_state(datapoint.value_bool);
+      call.perform();
+      ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
+    });
+  }
+  if (this->oscillation_id_.has_value()) {
+    this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->fan_->make_call();
+      call.set_oscillating(datapoint.value_bool);
+      call.perform();
+      ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
+    });
+  }
+  this->fan_->add_on_state_callback([this]() { this->write_state(); });
+}
+
+void TuyaFan::dump_config() {
+  ESP_LOGCONFIG(TAG, "Tuya Fan:");
+  if (this->speed_id_.has_value())
+    ESP_LOGCONFIG(TAG, "  Speed has datapoint ID %u", *this->speed_id_);
+  if (this->switch_id_.has_value())
+    ESP_LOGCONFIG(TAG, "  Switch has datapoint ID %u", *this->switch_id_);
+  if (this->oscillation_id_.has_value())
+    ESP_LOGCONFIG(TAG, "  Oscillation has datapoint ID %u", *this->oscillation_id_);
+}
+
+void TuyaFan::write_state() {
+  if (this->switch_id_.has_value()) {
+    TuyaDatapoint datapoint{};
+    datapoint.id = *this->switch_id_;
+    datapoint.type = TuyaDatapointType::BOOLEAN;
+    datapoint.value_bool = this->fan_->state;
+    this->parent_->set_datapoint_value(datapoint);
+    ESP_LOGD(TAG, "Setting switch: %s", ONOFF(this->fan_->state));
+  }
+  if (this->oscillation_id_.has_value()) {
+    TuyaDatapoint datapoint{};
+    datapoint.id = *this->oscillation_id_;
+    datapoint.type = TuyaDatapointType::BOOLEAN;
+    datapoint.value_bool = this->fan_->oscillating;
+    this->parent_->set_datapoint_value(datapoint);
+    ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
+  }
+  if (this->speed_id_.has_value()) {
+    TuyaDatapoint datapoint{};
+    datapoint.id = *this->speed_id_;
+    datapoint.type = TuyaDatapointType::ENUM;
+    if (this->fan_->speed == fan::FAN_SPEED_LOW)
+      datapoint.value_enum = 0;
+    if (this->fan_->speed == fan::FAN_SPEED_MEDIUM)
+      datapoint.value_enum = 1;
+    if (this->fan_->speed == fan::FAN_SPEED_HIGH)
+      datapoint.value_enum = 2;
+    ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum);
+    this->parent_->set_datapoint_value(datapoint);
+  }
+}
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h
new file mode 100644
index 0000000000..d31d490e1a
--- /dev/null
+++ b/esphome/components/tuya/fan/tuya_fan.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/tuya/tuya.h"
+#include "esphome/components/fan/fan_state.h"
+
+namespace esphome {
+namespace tuya {
+
+class TuyaFan : public Component {
+ public:
+  void setup() override;
+  void dump_config() override;
+  void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
+  void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
+  void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; }
+  void set_fan(fan::FanState *fan) { this->fan_ = fan; }
+  void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
+  void write_state();
+
+ protected:
+  void update_speed_(uint32_t value);
+  void update_switch_(uint32_t value);
+  void update_oscillation_(uint32_t value);
+
+  Tuya *parent_;
+  optional speed_id_{};
+  optional switch_id_{};
+  optional oscillation_id_{};
+  fan::FanState *fan_;
+};
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py
index 605bdae32e..adaeb52531 100644
--- a/esphome/components/tuya/light/__init__.py
+++ b/esphome/components/tuya/light/__init__.py
@@ -12,10 +12,10 @@ CONF_SWITCH_DATAPOINT = "switch_datapoint"
 
 TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component)
 
-CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({
+CONFIG_SCHEMA = cv.All(light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({
     cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight),
     cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
-    cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t,
+    cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t,
     cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
     cv.Optional(CONF_MIN_VALUE): cv.int_,
     cv.Optional(CONF_MAX_VALUE): cv.int_,
@@ -24,7 +24,8 @@ CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({
     # The Tuya MCU handles transitions and gamma correction on its own.
     cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
     cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds,
-}).extend(cv.COMPONENT_SCHEMA)
+}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT,
+                                                        CONF_SWITCH_DATAPOINT))
 
 
 def to_code(config):

From 426e6a1b46a1e3a8cc1859cc7fb878c97077ee3b Mon Sep 17 00:00:00 2001
From: sekkr1 
Date: Thu, 12 Mar 2020 02:25:54 +0200
Subject: [PATCH 164/412] Fixed iBeacon struct and major and minor parsing
 (#987)

Co-authored-by: sekkr1 
---
 esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index 74bb7e5d10..5456adbfe5 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -46,14 +46,15 @@ class ESPBLEiBeacon {
   ESPBLEiBeacon(const uint8_t *data);
   static optional from_manufacturer_data(const ServiceData &data);
 
-  uint16_t get_major() { return reverse_bits_16(this->beacon_data_.major); }
-  uint16_t get_minor() { return reverse_bits_16(this->beacon_data_.minor); }
+  uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); }
+  uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); }
   int8_t get_signal_power() { return this->beacon_data_.signal_power; }
   ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); }
 
  protected:
   struct {
     uint8_t sub_type;
+    uint8_t length;
     uint8_t proximity_uuid[16];
     uint16_t major;
     uint16_t minor;

From e0b4226930e30fbed70930a6ec25a23693970b05 Mon Sep 17 00:00:00 2001
From: Nikolay Vasilchuk 
Date: Thu, 12 Mar 2020 03:27:05 +0300
Subject: [PATCH 165/412] http_request http fix (#980)

Co-authored-by: Nikolay Vasilchuk 
---
 esphome/components/http_request/__init__.py   |  5 ++--
 .../components/http_request/http_request.cpp  | 27 ++++++++++++++++---
 .../components/http_request/http_request.h    | 22 ++++++++-------
 esphome/const.py                              |  1 +
 4 files changed, 39 insertions(+), 16 deletions(-)

diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index ea12b0657d..e79df12a6c 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -4,7 +4,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \
-    CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_1
+    CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_1, CONF_URL
 from esphome.core import CORE, Lambda
 from esphome.core_config import PLATFORMIO_ESP8266_LUT
 
@@ -15,7 +15,6 @@ http_request_ns = cg.esphome_ns.namespace('http_request')
 HttpRequestComponent = http_request_ns.class_('HttpRequestComponent', cg.Component)
 HttpRequestSendAction = http_request_ns.class_('HttpRequestSendAction', automation.Action)
 
-CONF_URL = 'url'
 CONF_HEADERS = 'headers'
 CONF_USERAGENT = 'useragent'
 CONF_BODY = 'body'
@@ -121,7 +120,7 @@ def http_request_action_to_code(config, action_id, template_arg, args):
     paren = yield cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
 
-    template_ = yield cg.templatable(config[CONF_URL], args, cg.const_char_ptr)
+    template_ = yield cg.templatable(config[CONF_URL], args, cg.std_string)
     cg.add(var.set_url(template_))
     cg.add(var.set_method(config[CONF_METHOD]))
     if CONF_BODY in config:
diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp
index 9df7cf7913..f2fe0cff54 100644
--- a/esphome/components/http_request/http_request.cpp
+++ b/esphome/components/http_request/http_request.cpp
@@ -14,14 +14,15 @@ void HttpRequestComponent::dump_config() {
 
 void HttpRequestComponent::send() {
   bool begin_status = false;
+  this->client_.setReuse(true);
 #ifdef ARDUINO_ARCH_ESP32
   begin_status = this->client_.begin(this->url_);
 #endif
 #ifdef ARDUINO_ARCH_ESP8266
 #ifndef CLANG_TIDY
-  begin_status = this->client_.begin(*this->wifi_client_, this->url_);
   this->client_.setFollowRedirects(true);
   this->client_.setRedirectLimit(3);
+  begin_status = this->client_.begin(*this->get_wifi_client_(), this->url_);
 #endif
 #endif
 
@@ -41,8 +42,6 @@ void HttpRequestComponent::send() {
   }
 
   int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
-  this->client_.end();
-
   if (http_code < 0) {
     ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_, HTTPClient::errorToString(http_code).c_str());
     this->status_set_warning();
@@ -59,5 +58,27 @@ void HttpRequestComponent::send() {
   ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_, http_code);
 }
 
+#ifdef ARDUINO_ARCH_ESP8266
+WiFiClient *HttpRequestComponent::get_wifi_client_() {
+  if (this->secure_) {
+    if (this->wifi_client_secure_ == nullptr) {
+      this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
+      this->wifi_client_secure_->setInsecure();
+      this->wifi_client_secure_->setBufferSizes(512, 512);
+    }
+    return this->wifi_client_secure_;
+  }
+
+  if (this->wifi_client_ == nullptr) {
+    this->wifi_client_ = new WiFiClient();
+  }
+  return this->wifi_client_;
+}
+#endif
+
+void HttpRequestComponent::close() { this->client_.end(); }
+
+const char *HttpRequestComponent::get_string() { return this->client_.getString().c_str(); }
+
 }  // namespace http_request
 }  // namespace esphome
diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index d26899c0db..0c849f2ab0 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -24,41 +24,42 @@ struct Header {
 
 class HttpRequestComponent : public Component {
  public:
-  void setup() override {
-#ifdef ARDUINO_ARCH_ESP8266
-    this->wifi_client_ = new BearSSL::WiFiClientSecure();
-    this->wifi_client_->setInsecure();
-    this->wifi_client_->setBufferSizes(512, 512);
-#endif
-  }
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 
-  void set_url(const char *url) { this->url_ = url; }
+  void set_url(std::string url) {
+    this->url_ = url.c_str();
+    this->secure_ = url.compare(0, 6, "https:") == 0;
+  }
   void set_method(const char *method) { this->method_ = method; }
   void set_useragent(const char *useragent) { this->useragent_ = useragent; }
   void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
   void set_body(std::string body) { this->body_ = body; }
   void set_headers(std::list
headers) { this->headers_ = headers; } void send(); + void close(); + const char *get_string(); protected: HTTPClient client_{}; const char *url_; const char *method_; const char *useragent_{nullptr}; + bool secure_; uint16_t timeout_{5000}; std::string body_; std::list
headers_; #ifdef ARDUINO_ARCH_ESP8266 - BearSSL::WiFiClientSecure *wifi_client_; + WiFiClient *wifi_client_{nullptr}; + BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; + WiFiClient *get_wifi_client_(); #endif }; template class HttpRequestSendAction : public Action { public: HttpRequestSendAction(HttpRequestComponent *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(const char *, url) + TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) TEMPLATABLE_VALUE(const char *, useragent) @@ -102,6 +103,7 @@ template class HttpRequestSendAction : public Action { this->parent_->set_headers(headers); } this->parent_->send(); + this->parent_->close(); } protected: diff --git a/esphome/const.py b/esphome/const.py index 308dd2683f..e747371710 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -479,6 +479,7 @@ CONF_UNIQUE = 'unique' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UPDATE_INTERVAL = 'update_interval' CONF_UPDATE_ON_BOOT = 'update_on_boot' +CONF_URL = 'url' CONF_USE_ADDRESS = 'use_address' CONF_USERNAME = 'username' CONF_UUID = 'uuid' From 177617e6e34a54c095bbb15f7b62c33839869f19 Mon Sep 17 00:00:00 2001 From: Quinn Hosler Date: Wed, 11 Mar 2020 20:33:20 -0400 Subject: [PATCH 166/412] Rgbww color fix (#967) * RGBWW color vs white and brightness adjustments --- esphome/components/light/light_state.cpp | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 0ffb603818..5e9166795e 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -393,6 +393,36 @@ LightColorValues LightCall::validate_() { this->color_temperature_.reset(); } + // sets RGB to 100% if only White specified + if (this->white_.has_value()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + } + // White to 0% if (exclusively) setting any RGB value + else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->white_.has_value()) { + this->white_ = optional(0.0f); + } + } + // if changing Kelvin alone, change to white light + else if (this->color_temperature_.has_value()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + // if setting Kelvin from color (i.e. switching to white light), set White to 100% + auto cv = this->parent_->remote_values; + bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; + bool now_white = *this->red_ == 1.0f && *this->blue_ == 1.0f && *this->green_ == 1.0f; + if (!this->white_.has_value() && was_color && now_white) { + this->white_ = optional(1.0f); + } + } + #define VALIDATE_RANGE_(name_, upper_name) \ if (name_##_.has_value()) { \ auto val = *name_##_; \ From fcb2cc2471fc589d881154ce3147ae9662bb44b3 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 11 Mar 2020 21:35:01 -0300 Subject: [PATCH 167/412] add time cover assumed_state option (#979) --- esphome/components/time_based/cover.py | 4 +++- esphome/components/time_based/time_based_cover.cpp | 2 +- esphome/components/time_based/time_based_cover.h | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index 6a7c9b6835..dcb8d9505b 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import cover from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_ACTION, \ - CONF_OPEN_DURATION, CONF_STOP_ACTION + CONF_OPEN_DURATION, CONF_STOP_ACTION, CONF_ASSUMED_STATE time_based_ns = cg.esphome_ns.namespace('time_based') TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component) @@ -21,6 +21,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) @@ -38,3 +39,4 @@ def to_code(config): yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index bdb4e5379c..6d1de144f5 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,7 +51,7 @@ float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_position(true); - traits.set_is_assumed_state(true); + traits.set_is_assumed_state(this->assumed_state_); return traits; } void TimeBasedCover::control(const CoverCall &call) { diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index be3a55c546..6c48c26ed1 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -21,6 +21,7 @@ class TimeBasedCover : public cover::Cover, public Component { void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } + void set_assumed_state(bool value) { this->assumed_state_ = value; } protected: void control(const cover::CoverCall &call) override; @@ -43,6 +44,7 @@ class TimeBasedCover : public cover::Cover, public Component { uint32_t last_publish_time_{0}; float target_position_{0}; bool has_built_in_endstop_{false}; + bool assumed_state_{false}; }; } // namespace time_based From a1dfd355f703cf59764ea2e340710e6accc91a2d Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 12 Mar 2020 01:36:34 +0100 Subject: [PATCH 168/412] add on_rc_switch trigger (#983) --- esphome/components/remote_base/__init__.py | 7 +++++++ .../components/remote_base/rc_switch_protocol.cpp | 13 +++++++++++++ esphome/components/remote_base/rc_switch_protocol.h | 11 +++++++++++ 3 files changed, 31 insertions(+) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 2c8b6be51c..05a3e7e1aa 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -546,7 +546,9 @@ RC_SWITCH_TRANSMITTER = cv.Schema({ }) rc_switch_protocols = ns.rc_switch_protocols +RCSwitchData = ns.struct('RCSwitchData') RCSwitchBase = ns.class_('RCSwitchBase') +RCSwitchTrigger = ns.class_('RCSwitchTrigger', RemoteReceiverTrigger) RCSwitchDumper = ns.class_('RCSwitchDumper', RemoteTransmitterDumper) RCSwitchRawAction = ns.class_('RCSwitchRawAction', RemoteTransmitterActionBase) RCSwitchTypeAAction = ns.class_('RCSwitchTypeAAction', RemoteTransmitterActionBase) @@ -642,6 +644,11 @@ def rc_switch_type_d_action(var, config, args): cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) +@register_trigger('rc_switch', RCSwitchTrigger, RCSwitchData) +def rc_switch_trigger(var, config): + pass + + @register_dumper('rc_switch', RCSwitchDumper) def rc_switch_dumper(var, config): pass diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index b2ff22eb2a..91b22500e6 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -127,6 +127,19 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o } return true; } +optional RCSwitchBase::decode(RemoteReceiveData &src) const { + RCSwitchData out; + uint8_t out_nbits; + for (uint8_t i = 1; i <= 8; i++) { + src.reset(); + RCSwitchBase *protocol = &rc_switch_protocols[i]; + if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) { + out.protocol = i; + return out; + } + } + return {}; +} void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code) { *out_code = 0; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 0983da27ea..8362899cec 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -6,6 +6,13 @@ namespace esphome { namespace remote_base { +struct RCSwitchData { + uint64_t code; + uint8_t protocol; + + bool operator==(const RCSwitchData &rhs) const { return code == rhs.code && protocol == rhs.protocol; } +}; + class RCSwitchBase { public: RCSwitchBase() = default; @@ -28,6 +35,8 @@ class RCSwitchBase { bool decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const; + optional decode(RemoteReceiveData &src) const; + static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code); static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, @@ -204,5 +213,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase { bool dump(RemoteReceiveData src) override; }; +using RCSwitchTrigger = RemoteReceiverTrigger; + } // namespace remote_base } // namespace esphome From 11b727fdf7c8409c199d28816c6aff3cfb3ddbf8 Mon Sep 17 00:00:00 2001 From: Derek Hageman Date: Wed, 11 Mar 2020 18:39:40 -0600 Subject: [PATCH 169/412] SCD30 fixes and improvements (#962) * SCD30 improvements --- esphome/components/scd30/scd30.cpp | 43 +++++++++++++++++++++++++++--- esphome/components/scd30/scd30.h | 5 ++++ esphome/components/scd30/sensor.py | 17 ++++++++++++ tests/test1.yaml | 2 ++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 55ab07879e..195dfef5f6 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -8,21 +8,25 @@ static const char *TAG = "scd30"; static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; +static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; static const uint16_t SCD30_CMD_GET_DATA_READY_STATUS = 0x0202; static const uint16_t SCD30_CMD_READ_MEASUREMENT = 0x0300; /// Commands for future use static const uint16_t SCD30_CMD_STOP_MEASUREMENTS = 0x0104; static const uint16_t SCD30_CMD_MEASUREMENT_INTERVAL = 0x4600; -static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; static const uint16_t SCD30_CMD_FORCED_CALIBRATION = 0x5204; static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403; -static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; void SCD30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up scd30..."); +#ifdef ARDUINO_ARCH_ESP8266 + Wire.setClockStretchLimit(150000); +#endif + /// Firmware version identification if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) { this->error_code_ = COMMUNICATION_FAILED; @@ -40,12 +44,29 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); /// Sensor initialization - if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS)) { + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, 0)) { ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } + + // 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_)) { + ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } + + 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(); + return; + } } void SCD30Component::dump_config() { @@ -67,6 +88,12 @@ void SCD30Component::dump_config() { break; } } + if (this->altitude_compensation_ == 0xFFFF) { + ESP_LOGCONFIG(TAG, " Altitude compensation: OFF"); + } else { + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); @@ -130,6 +157,16 @@ bool SCD30Component::write_command_(uint16_t command) { 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_bytes_raw(raw, 5); +} + uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { uint8_t bit; uint8_t crc = 0xFF; diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 999e66414d..2c4ee51f8a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -13,6 +13,8 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } + void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } void setup() override; void update() override; @@ -21,6 +23,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { 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); @@ -30,6 +33,8 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { MEASUREMENT_INIT_FAILED, UNKNOWN } error_code_{UNKNOWN}; + bool enable_asc_{true}; + uint16_t altitude_compensation_{0xFFFF}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 7a60725276..b3de1b214a 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,3 +1,4 @@ +import re import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor @@ -10,12 +11,24 @@ DEPENDENCIES = ['i2c'] scd30_ns = cg.esphome_ns.namespace('scd30') SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) +CONF_AUTOMATIC_SELF_CALIBRATION = 'automatic_self_calibration' +CONF_ALTITUDE_COMPENSATION = 'altitude_compensation' + + +def remove_altitude_suffix(value): + return re.sub(r"\s*(?:m(?:\s+a\.s\.l)?)|(?:MAM?SL)$", '', value) + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SCD30Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, + cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(remove_altitude_suffix, + cv.int_range(min=0, max=0xFFFF, + max_included=False)), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) @@ -24,6 +37,10 @@ def to_code(config): yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + cg.add(var.set_automatic_self_calibration(config[CONF_AUTOMATIC_SELF_CALIBRATION])) + if CONF_ALTITUDE_COMPENSATION in config: + cg.add(var.set_altitude_compensation(config[CONF_ALTITUDE_COMPENSATION])) + if CONF_CO2 in config: sens = yield sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 0533063fb8..93e6638330 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -584,6 +584,8 @@ sensor: name: "Living Room Humidity 9" address: 0x61 update_interval: 15s + automatic_self_calibration: true + altitude_compensation: 10m - platform: sgp30 eco2: name: "Workshop eCO2" From c60989a7be75989e55af166d8496cb5ff27b4080 Mon Sep 17 00:00:00 2001 From: Pavel <205196+yekm@users.noreply.github.com> Date: Thu, 12 Mar 2020 23:37:57 +0300 Subject: [PATCH 170/412] pzemac total energy support (#933) * add energy support in pzemac sensor Co-authored-by: Sergio Mayoral Martinez Co-authored-by: t151602 Co-authored-by: Otto Winter --- esphome/components/pzemac/pzemac.cpp | 10 ++++++++-- esphome/components/pzemac/pzemac.h | 2 ++ esphome/components/pzemac/sensor.py | 8 +++++++- esphome/const.py | 2 ++ tests/test3.yaml | 2 ++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index f05ce15711..c79508d22f 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -19,6 +19,7 @@ void PZEMAC::on_modbus_data(const std::vector &data) { // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + // 0 2 6 10 14 16 auto pzem_get_16bit = [&](size_t i) -> uint16_t { return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); @@ -36,20 +37,24 @@ void PZEMAC::on_modbus_data(const std::vector &data) { uint32_t raw_active_power = pzem_get_32bit(6); float active_power = raw_active_power / 10.0f; // max 429496729.5 W + float active_energy = static_cast(pzem_get_32bit(10)); + uint16_t raw_frequency = pzem_get_16bit(14); float frequency = raw_frequency / 10.0f; uint16_t raw_power_factor = pzem_get_16bit(16); float power_factor = raw_power_factor / 100.0f; - ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, - power_factor); + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, E=%.1f Wh, F=%.1f Hz, PF=%.2f", voltage, current, active_power, + active_energy, frequency, power_factor); if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(voltage); if (this->current_sensor_ != nullptr) this->current_sensor_->publish_state(current); if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(active_power); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(active_energy); if (this->frequency_sensor_ != nullptr) this->frequency_sensor_->publish_state(frequency); if (this->power_factor_sensor_ != nullptr) @@ -63,6 +68,7 @@ void PZEMAC::dump_config() { LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); LOG_SENSOR("", "Frequency", this->frequency_sensor_); LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); } diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index d396b7cddf..07f661535f 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -12,6 +12,7 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } @@ -25,6 +26,7 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; sensor::Sensor *frequency_sensor_; sensor::Sensor *power_factor_sensor_; }; diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 54eb01e085..e3d2b90742 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -3,7 +3,8 @@ import esphome.config_validation as cv from esphome.components import sensor, modbus from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ - ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ, \ + CONF_ENERGY, UNIT_WATT_HOURS, ICON_COUNTER AUTO_LOAD = ['modbus'] @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), }).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) @@ -37,6 +39,10 @@ def to_code(config): conf = config[CONF_POWER] sens = yield sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) if CONF_FREQUENCY in config: conf = config[CONF_FREQUENCY] sens = yield sensor.new_sensor(conf) diff --git a/esphome/const.py b/esphome/const.py index e747371710..36ffbed254 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -145,6 +145,7 @@ CONF_EFFECTS = 'effects' CONF_ELSE = 'else' CONF_ENABLE_PIN = 'enable_pin' CONF_ENABLE_TIME = 'enable_time' +CONF_ENERGY = 'energy' CONF_ENTITY_ID = 'entity_id' CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' CONF_ESPHOME = 'esphome' @@ -580,6 +581,7 @@ UNIT_VOLT = 'V' UNIT_VOLT_AMPS = 'VA' UNIT_VOLT_AMPS_REACTIVE = 'VAR' UNIT_WATT = 'W' +UNIT_WATT_HOURS = 'Wh' DEVICE_CLASS_CONNECTIVITY = 'connectivity' DEVICE_CLASS_MOVING = 'moving' diff --git a/tests/test3.yaml b/tests/test3.yaml index 9c66242b4e..2e1dd3f078 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -335,6 +335,8 @@ sensor: name: "PZEMAC Current" power: name: "PZEMAC Power" + energy: + name: "PZEMAC Energy" frequency: name: "PZEMAC Frequency" power_factor: From 714d28a61a2b9d4cec82f1f2091c59b70b1e7c60 Mon Sep 17 00:00:00 2001 From: Nicholas Peters Date: Thu, 12 Mar 2020 17:25:00 -0400 Subject: [PATCH 171/412] Add TMP117 component (#992) * Create TMP117 sensor component --- esphome/components/tmp117/__init__.py | 0 esphome/components/tmp117/sensor.py | 67 +++++++++++++++++++++++ esphome/components/tmp117/tmp117.cpp | 76 +++++++++++++++++++++++++++ esphome/components/tmp117/tmp117.h | 27 ++++++++++ tests/test1.yaml | 3 ++ 5 files changed, 173 insertions(+) create mode 100644 esphome/components/tmp117/__init__.py create mode 100644 esphome/components/tmp117/sensor.py create mode 100644 esphome/components/tmp117/tmp117.cpp create mode 100644 esphome/components/tmp117/tmp117.h diff --git a/esphome/components/tmp117/__init__.py b/esphome/components/tmp117/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py new file mode 100644 index 0000000000..ddca3eeb64 --- /dev/null +++ b/esphome/components/tmp117/sensor.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, \ + UNIT_CELSIUS, ICON_THERMOMETER + +DEPENDENCIES = ['i2c'] + +tmp117_ns = cg.esphome_ns.namespace('tmp117') +TMP117Component = tmp117_ns.class_('TMP117Component', + cg.PollingComponent, i2c.I2CDevice, sensor.Sensor) + +CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(TMP117Component), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x48))) + + +def determine_config_register(polling_period): + if polling_period >= 16.0: + # 64 averaged conversions, max conversion time + # 0000 00 111 11 00000 + # 0000 0011 1110 0000 + return 0x03E0 + if polling_period >= 8.0: + # 64 averaged conversions, high conversion time + # 0000 00 110 11 00000 + # 0000 0011 0110 0000 + return 0x0360 + if polling_period >= 4.0: + # 64 averaged conversions, mid conversion time + # 0000 00 101 11 00000 + # 0000 0010 1110 0000 + return 0x02E0 + if polling_period >= 1.0: + # 64 averaged conversions, min conversion time + # 0000 00 000 11 00000 + # 0000 0000 0110 0000 + return 0x0060 + if polling_period >= 0.5: + # 32 averaged conversions, min conversion time + # 0000 00 000 10 00000 + # 0000 0000 0100 0000 + return 0x0040 + if polling_period >= 0.25: + # 8 averaged conversions, mid conversion time + # 0000 00 010 01 00000 + # 0000 0001 0010 0000 + return 0x0120 + if polling_period >= 0.125: + # 8 averaged conversions, min conversion time + # 0000 00 000 01 00000 + # 0000 0000 0010 0000 + return 0x0020 + # 1 averaged conversions, min conversion time + # 0000 00 000 00 00000 + # 0000 0000 0000 0000 + return 0x0000 + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) + + update_period = config[CONF_UPDATE_INTERVAL].total_seconds + cg.add(var.set_config(determine_config_register(update_period))) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp new file mode 100644 index 0000000000..9040d9bfed --- /dev/null +++ b/esphome/components/tmp117/tmp117.cpp @@ -0,0 +1,76 @@ +// Implementation based on: +// - DHT 12 Component + +#include "tmp117.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tmp117 { + +static const char *TAG = "tmp117"; + +void TMP117Component::update() { + int16_t data; + if (!this->read_data_(&data)) { + this->status_set_warning(); + return; + } + if ((uint16_t) data != 0x8000) { + float temperature = data * 0.0078125f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + } else { + ESP_LOGD(TAG, "TMP117 not ready"); + } +} +void TMP117Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TMP117..."); + + if (!this->write_config_(this->config_)) { + this->mark_failed(); + return; + } + + int16_t data; + if (!this->read_data_(&data)) { + this->mark_failed(); + return; + } +} +void TMP117Component::dump_config() { + ESP_LOGD(TAG, "TMP117:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TMP117 failed!"); + } + LOG_SENSOR(" ", "Temperature", this); +} +float TMP117Component::get_setup_priority() const { return setup_priority::DATA; } +bool TMP117Component::read_data_(int16_t *data) { + if (!this->read_byte_16(0, (uint16_t *) data)) { + ESP_LOGW(TAG, "Updating TMP117 failed!"); + return false; + } + return true; +} + +bool TMP117Component::read_config_(uint16_t *config) { + if (!this->read_byte_16(1, (uint16_t *) config)) { + ESP_LOGW(TAG, "Reading TMP117 config failed!"); + return false; + } + return true; +} + +bool TMP117Component::write_config_(uint16_t config) { + if (!this->write_byte_16(1, config)) { + ESP_LOGE(TAG, "Writing TMP117 config failed!"); + return false; + } + return true; +} + +} // namespace tmp117 +} // namespace esphome diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h new file mode 100644 index 0000000000..162dbb64db --- /dev/null +++ b/esphome/components/tmp117/tmp117.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tmp117 { + +class TMP117Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void set_config(uint16_t config) { config_ = config; }; + + protected: + bool read_data_(int16_t *data); + bool read_config_(uint16_t *config); + bool write_config_(uint16_t config); + + uint16_t config_; +}; + +} // namespace tmp117 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 93e6638330..b3b397ddb1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -716,6 +716,9 @@ sensor: name: "Lightning Energy" distance: name: "Distance Storm" + - platform: tmp117 + name: "TMP117 Temperature" + update_interval: 5s esp32_touch: setup_mode: False From c632b0e1d40d50927d521cfd253bff017792541b Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Fri, 13 Mar 2020 08:27:22 +1100 Subject: [PATCH 172/412] Unittests for esphome python code (#931) --- .coveragerc | 2 + esphome/core.py | 3 + pytest.ini | 4 + requirements_test.txt | 6 + script/fulltest | 1 + script/unit_test | 9 + tests/unit_tests/conftest.py | 30 ++ tests/unit_tests/fixtures/helpers/file-a.txt | 1 + .../unit_tests/fixtures/helpers/file-b_1.txt | 1 + .../unit_tests/fixtures/helpers/file-b_2.txt | 1 + tests/unit_tests/fixtures/helpers/file-c.txt | 1 + tests/unit_tests/strategies.py | 15 + tests/unit_tests/test_config_validation.py | 113 ++++ tests/unit_tests/test_core.py | 491 ++++++++++++++++++ tests/unit_tests/test_helpers.py | 208 ++++++++ tests/unit_tests/test_pins.py | 326 ++++++++++++ 16 files changed, 1212 insertions(+) create mode 100644 .coveragerc create mode 100644 pytest.ini create mode 100755 script/unit_test create mode 100644 tests/unit_tests/conftest.py create mode 100644 tests/unit_tests/fixtures/helpers/file-a.txt create mode 100644 tests/unit_tests/fixtures/helpers/file-b_1.txt create mode 100644 tests/unit_tests/fixtures/helpers/file-b_2.txt create mode 100644 tests/unit_tests/fixtures/helpers/file-c.txt create mode 100644 tests/unit_tests/strategies.py create mode 100644 tests/unit_tests/test_config_validation.py create mode 100644 tests/unit_tests/test_core.py create mode 100644 tests/unit_tests/test_helpers.py create mode 100644 tests/unit_tests/test_pins.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..723242b288 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = esphome/components/* diff --git a/esphome/core.py b/esphome/core.py index b4bea49dbd..7734636a76 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -143,6 +143,9 @@ class TimePeriod: return f'{self.total_days}d' return '0s' + def __repr__(self): + return f"TimePeriod<{self.total_microseconds}>" + @property def total_microseconds(self): return self.total_milliseconds * 1000 + (self.microseconds or 0) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..a91a2ea200 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = + --cov=esphome + --cov-branch diff --git a/requirements_test.txt b/requirements_test.txt index 7711b3867a..85f7d511a8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,3 +16,9 @@ pylint==2.4.4 ; python_version>"3" flake8==3.7.9 pillow pexpect + +# Unit tests +pytest==5.3.2 +pytest-cov==2.8.1 +pytest-mock==1.13.0 +hypothesis==4.57.0 diff --git a/script/fulltest b/script/fulltest index 0fa88516c2..795482281a 100755 --- a/script/fulltest +++ b/script/fulltest @@ -9,4 +9,5 @@ set -x script/ci-custom.py script/lint-python script/lint-cpp +script/unit_test script/test diff --git a/script/unit_test b/script/unit_test new file mode 100755 index 0000000000..1e5c288632 --- /dev/null +++ b/script/unit_test @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +set -x + +pytest tests/unit_tests diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py new file mode 100644 index 0000000000..adef39a0b3 --- /dev/null +++ b/tests/unit_tests/conftest.py @@ -0,0 +1,30 @@ +""" +ESPHome Unittests +~~~~~~~~~~~~~~~~~ + +Configuration file for unit tests. + +If adding unit tests ensure that they are fast. Slower integration tests should +not be part of a unit test suite. + +""" +import sys +import pytest + +from pathlib import Path + + +here = Path(__file__).parent + +# Configure location of package root +package_root = here.parent.parent +sys.path.insert(0, package_root.as_posix()) + + +@pytest.fixture +def fixture_path() -> Path: + """ + Location of all fixture files. + """ + return here / "fixtures" + diff --git a/tests/unit_tests/fixtures/helpers/file-a.txt b/tests/unit_tests/fixtures/helpers/file-a.txt new file mode 100644 index 0000000000..a9d1060fb6 --- /dev/null +++ b/tests/unit_tests/fixtures/helpers/file-a.txt @@ -0,0 +1 @@ +A files are unique. diff --git a/tests/unit_tests/fixtures/helpers/file-b_1.txt b/tests/unit_tests/fixtures/helpers/file-b_1.txt new file mode 100644 index 0000000000..907b98d934 --- /dev/null +++ b/tests/unit_tests/fixtures/helpers/file-b_1.txt @@ -0,0 +1 @@ +All b files match. diff --git a/tests/unit_tests/fixtures/helpers/file-b_2.txt b/tests/unit_tests/fixtures/helpers/file-b_2.txt new file mode 100644 index 0000000000..907b98d934 --- /dev/null +++ b/tests/unit_tests/fixtures/helpers/file-b_2.txt @@ -0,0 +1 @@ +All b files match. diff --git a/tests/unit_tests/fixtures/helpers/file-c.txt b/tests/unit_tests/fixtures/helpers/file-c.txt new file mode 100644 index 0000000000..558763720e --- /dev/null +++ b/tests/unit_tests/fixtures/helpers/file-c.txt @@ -0,0 +1 @@ +C files are unique. diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py new file mode 100644 index 0000000000..f4763f047f --- /dev/null +++ b/tests/unit_tests/strategies.py @@ -0,0 +1,15 @@ +from typing import Text + +import hypothesis.strategies._internal.core as st +from hypothesis.strategies._internal.strategies import SearchStrategy + + +@st.defines_strategy_with_reusable_values +def mac_addr_strings(): + # type: () -> SearchStrategy[Text] + """A strategy for MAC address strings. + + This consists of six strings representing integers [0..255], + without zero-padding, joined by dots. + """ + return st.builds("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}".format, *(6 * [st.integers(0, 255)])) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py new file mode 100644 index 0000000000..a19330062e --- /dev/null +++ b/tests/unit_tests/test_config_validation.py @@ -0,0 +1,113 @@ +import pytest +import string + +from hypothesis import given, example +from hypothesis.strategies import one_of, text, integers, booleans, builds + +from esphome import config_validation +from esphome.config_validation import Invalid +from esphome.core import Lambda, HexInt + + +def test_check_not_tamplatable__invalid(): + with pytest.raises(Invalid, match="This option is not templatable!"): + config_validation.check_not_templatable(Lambda("")) + + +@given(one_of( + booleans(), + integers(), + text(alphabet=string.ascii_letters + string.digits)), +) +def test_alphanumeric__valid(value): + actual = config_validation.alphanumeric(value) + + assert actual == str(value) + + +@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_")) +def test_valid_name__valid(value): + actual = config_validation.valid_name(value) + + assert actual == value + + +@pytest.mark.parametrize("value", ( + "foo bar", "FooBar", "foo::bar" +)) +def test_valid_name__invalid(value): + with pytest.raises(Invalid): + config_validation.valid_name(value) + + +@given(one_of(integers(), text())) +def test_string__valid(value): + actual = config_validation.string(value) + + assert actual == str(value) + + +@pytest.mark.parametrize("value", ( + {}, [], True, False, None +)) +def test_string__invalid(value): + with pytest.raises(Invalid): + config_validation.string(value) + + +@given(text()) +def test_strict_string__valid(value): + actual = config_validation.string_strict(value) + + assert actual == value + + +@pytest.mark.parametrize("value", (None, 123)) +def test_string_string__invalid(value): + with pytest.raises(Invalid, match="Must be string, got"): + config_validation.string_strict(value) + + +@given(builds(lambda v: "mdi:" + v, text())) +@example("") +def test_icon__valid(value): + actual = config_validation.icon(value) + + assert actual == value + + +def test_icon__invalid(): + with pytest.raises(Invalid, match="Icons should start with prefix"): + config_validation.icon("foo") + + +@pytest.mark.parametrize("value", ( + "True", "YES", "on", "enAblE", True +)) +def test_boolean__valid_true(value): + assert config_validation.boolean(value) is True + + +@pytest.mark.parametrize("value", ( + "False", "NO", "off", "disAblE", False +)) +def test_boolean__valid_false(value): + assert config_validation.boolean(value) is False + + +@pytest.mark.parametrize("value", ( + None, 1, 0, "foo" +)) +def test_boolean__invalid(value): + with pytest.raises(Invalid, match="Expected boolean value"): + config_validation.boolean(value) + + +# TODO: ensure_list +@given(integers()) +def hex_int__valid(value): + actual = config_validation.hex_int(value) + + assert isinstance(actual, HexInt) + assert actual == value + diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py new file mode 100644 index 0000000000..c02aed6447 --- /dev/null +++ b/tests/unit_tests/test_core.py @@ -0,0 +1,491 @@ +import pytest + +from hypothesis import given +from hypothesis.provisional import ip4_addr_strings +from strategies import mac_addr_strings + +from esphome import core, const + + +class TestHexInt: + @pytest.mark.parametrize("value, expected", ( + (1, "0x01"), + (255, "0xFF"), + (128, "0x80"), + (256, "0x100"), + (-1, "-0x01"), # TODO: this currently fails + )) + def test_str(self, value, expected): + target = core.HexInt(value) + + actual = str(target) + + assert actual == expected + + +class TestIPAddress: + @given(value=ip4_addr_strings()) + def test_init__valid(self, value): + core.IPAddress(*value.split(".")) + + @pytest.mark.parametrize("value", ("127.0.0", "localhost", "")) + def test_init__invalid(self, value): + with pytest.raises(ValueError, match="IPAddress must consist of 4 items"): + core.IPAddress(*value.split(".")) + + @given(value=ip4_addr_strings()) + def test_str(self, value): + target = core.IPAddress(*value.split(".")) + + actual = str(target) + + assert actual == value + + +class TestMACAddress: + @given(value=mac_addr_strings()) + def test_init__valid(self, value): + core.MACAddress(*value.split(":")) + + @pytest.mark.parametrize("value", ("1:2:3:4:5", "localhost", "")) + def test_init__invalid(self, value): + with pytest.raises(ValueError, match="MAC Address must consist of 6 items"): + core.MACAddress(*value.split(":")) + + @given(value=mac_addr_strings()) + def test_str(self, value): + target = core.MACAddress(*(int(v, 16) for v in value.split(":"))) + + actual = str(target) + + assert actual == value + + def test_as_hex(self): + target = core.MACAddress(0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF) + + actual = target.as_hex + + assert actual.text == "0xDEADBEEF00FFULL" + + +@pytest.mark.parametrize("value", ( + 1, 2, -1, 0, 1.0, -1.0, 42.0009, -42.0009 +)) +def test_is_approximately_integer__in_range(value): + actual = core.is_approximately_integer(value) + + assert actual is True + + +@pytest.mark.parametrize("value", ( + 42.01, -42.01, 1.5 +)) +def test_is_approximately_integer__not_in_range(value): + actual = core.is_approximately_integer(value) + + assert actual is False + + +class TestTimePeriod: + @pytest.mark.parametrize("kwargs, expected", ( + ({}, {}), + ({"microseconds": 1}, {"microseconds": 1}), + ({"microseconds": 1.0001}, {"microseconds": 1}), + ({"milliseconds": 2}, {"milliseconds": 2}), + ({"milliseconds": 2.0001}, {"milliseconds": 2}), + ({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}), + ({"seconds": 3}, {"seconds": 3}), + ({"seconds": 3.0001}, {"seconds": 3}), + ({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}), + ({"minutes": 4}, {"minutes": 4}), + ({"minutes": 4.0001}, {"minutes": 4}), + ({"minutes": 4.1}, {"minutes": 4, "seconds": 6}), + ({"hours": 5}, {"hours": 5}), + ({"hours": 5.0001}, {"hours": 5}), + ({"hours": 5.1}, {"hours": 5, "minutes": 6}), + ({"days": 6}, {"days": 6}), + ({"days": 6.0001}, {"days": 6}), + ({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}), + )) + def test_init(self, kwargs, expected): + target = core.TimePeriod(**kwargs) + + actual = target.as_dict() + + assert actual == expected + + def test_init__microseconds_with_fraction(self): + with pytest.raises(ValueError, match="Maximum precision is microseconds"): + core.TimePeriod(microseconds=1.1) + + @pytest.mark.parametrize("kwargs, expected", ( + ({}, "0s"), + ({"microseconds": 1}, "1us"), + ({"microseconds": 1.0001}, "1us"), + ({"milliseconds": 2}, "2ms"), + ({"milliseconds": 2.0001}, "2ms"), + ({"milliseconds": 2.01}, "2010us"), + ({"seconds": 3}, "3s"), + ({"seconds": 3.0001}, "3s"), + ({"seconds": 3.01}, "3010ms"), + ({"minutes": 4}, "4min"), + ({"minutes": 4.0001}, "4min"), + ({"minutes": 4.1}, "246s"), + ({"hours": 5}, "5h"), + ({"hours": 5.0001}, "5h"), + ({"hours": 5.1}, "306min"), + ({"days": 6}, "6d"), + ({"days": 6.0001}, "6d"), + ({"days": 6.1}, "8784min"), + )) + def test_str(self, kwargs, expected): + target = core.TimePeriod(**kwargs) + + actual = str(target) + + assert actual == expected + + @pytest.mark.parametrize("comparison, other, expected", ( + ("__eq__", core.TimePeriod(microseconds=900), False), + ("__eq__", core.TimePeriod(milliseconds=1), True), + ("__eq__", core.TimePeriod(microseconds=1100), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + + ("__ne__", core.TimePeriod(microseconds=900), True), + ("__ne__", core.TimePeriod(milliseconds=1), False), + ("__ne__", core.TimePeriod(microseconds=1100), True), + ("__ne__", 1000, NotImplemented), + ("__ne__", "1000", NotImplemented), + ("__ne__", True, NotImplemented), + ("__ne__", object(), NotImplemented), + ("__ne__", None, NotImplemented), + + ("__lt__", core.TimePeriod(microseconds=900), False), + ("__lt__", core.TimePeriod(milliseconds=1), False), + ("__lt__", core.TimePeriod(microseconds=1100), True), + ("__lt__", 1000, NotImplemented), + ("__lt__", "1000", NotImplemented), + ("__lt__", True, NotImplemented), + ("__lt__", object(), NotImplemented), + ("__lt__", None, NotImplemented), + + ("__gt__", core.TimePeriod(microseconds=900), True), + ("__gt__", core.TimePeriod(milliseconds=1), False), + ("__gt__", core.TimePeriod(microseconds=1100), False), + ("__gt__", 1000, NotImplemented), + ("__gt__", "1000", NotImplemented), + ("__gt__", True, NotImplemented), + ("__gt__", object(), NotImplemented), + ("__gt__", None, NotImplemented), + + ("__le__", core.TimePeriod(microseconds=900), False), + ("__le__", core.TimePeriod(milliseconds=1), True), + ("__le__", core.TimePeriod(microseconds=1100), True), + ("__le__", 1000, NotImplemented), + ("__le__", "1000", NotImplemented), + ("__le__", True, NotImplemented), + ("__le__", object(), NotImplemented), + ("__le__", None, NotImplemented), + + ("__ge__", core.TimePeriod(microseconds=900), True), + ("__ge__", core.TimePeriod(milliseconds=1), True), + ("__ge__", core.TimePeriod(microseconds=1100), False), + ("__ge__", 1000, NotImplemented), + ("__ge__", "1000", NotImplemented), + ("__ge__", True, NotImplemented), + ("__ge__", object(), NotImplemented), + ("__ge__", None, NotImplemented), + )) + def test_comparison(self, comparison, other, expected): + target = core.TimePeriod(microseconds=1000) + + actual = getattr(target, comparison)(other) + + assert actual == expected + + +SAMPLE_LAMBDA = """ +it.strftime(64, 0, id(my_font), TextAlign::TOP_CENTER, "%H:%M:%S", id(esptime).now()); +it.printf(64, 16, id(my_font2), TextAlign::TOP_CENTER, "%.1f°C (%.1f%%)", id( office_tmp ).state, id(office_hmd).state); +""" + + +class TestLambda: + def test_init__copy_initializer(self): + value = core.Lambda("foo") + target = core.Lambda(value) + + assert str(target) is value.value + + def test_parts(self): + target = core.Lambda(SAMPLE_LAMBDA.strip()) + + # Check cache + assert target._parts is None + actual = target.parts + assert target._parts is actual + assert target.parts is actual + + assert actual == [ + "it.strftime(64, 0, ", + "my_font", + "", + ", TextAlign::TOP_CENTER, \"%H:%M:%S\", ", + "esptime", + ".", + "now());\nit.printf(64, 16, ", + "my_font2", + "", + ", TextAlign::TOP_CENTER, \"%.1f°C (%.1f%%)\", ", + "office_tmp", + ".", + "state, ", + "office_hmd", + ".", + "state);" + ] + + def test_requires_ids(self): + target = core.Lambda(SAMPLE_LAMBDA.strip()) + + # Check cache + assert target._requires_ids is None + actual = target.requires_ids + assert target._requires_ids is actual + assert target.requires_ids is actual + + assert actual == [ + core.ID("my_font"), + core.ID("esptime"), + core.ID("my_font2"), + core.ID("office_tmp"), + core.ID("office_hmd"), + ] + + def test_value_setter(self): + target = core.Lambda("") + + # Populate cache + _ = target.parts + _ = target.requires_ids + + target.value = SAMPLE_LAMBDA + + # Check cache has been cleared + assert target._parts is None + assert target._requires_ids is None + + assert target.value == SAMPLE_LAMBDA + + def test_repr(self): + target = core.Lambda("id(var).value == 1") + + assert repr(target) == "Lambda" + + +class TestID: + @pytest.fixture + def target(self): + return core.ID(None, is_declaration=True, type="binary_sensor::Example") + + @pytest.mark.parametrize("id, is_manual, expected", ( + ("foo", None, True), + (None, None, False), + ("foo", True, True), + ("foo", False, False), + (None, True, True), + )) + def test_init__resolve_is_manual(self, id, is_manual, expected): + target = core.ID(id, is_manual=is_manual) + + assert target.is_manual == expected + + @pytest.mark.parametrize("registered_ids, expected", ( + ([], "binary_sensor_example"), + (["binary_sensor_example"], "binary_sensor_example_2"), + (["foo"], "binary_sensor_example"), + (["binary_sensor_example", "foo", "binary_sensor_example_2"], "binary_sensor_example_3"), + )) + def test_resolve(self, target, registered_ids, expected): + actual = target.resolve(registered_ids) + + assert actual == expected + assert str(target) == expected + + def test_copy(self, target): + target.resolve([]) + + actual = target.copy() + + assert actual is not target + assert all(getattr(actual, n) == getattr(target, n) + for n in ("id", "is_declaration", "type", "is_manual")) + + @pytest.mark.parametrize("comparison, other, expected", ( + ("__eq__", core.ID(id="foo"), True), + ("__eq__", core.ID(id="bar"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + )) + def test_comparison(self, comparison, other, expected): + target = core.ID(id="foo") + + actual = getattr(target, comparison)(other) + + assert actual == expected + + +class TestDocumentLocation: + @pytest.fixture + def target(self): + return core.DocumentLocation( + document="foo.txt", + line=10, + column=20, + ) + + def test_str(self, target): + actual = str(target) + + assert actual == "foo.txt 10:20" + + +class TestDocumentRange: + @pytest.fixture + def target(self): + return core.DocumentRange( + core.DocumentLocation( + document="foo.txt", + line=10, + column=20, + ), + core.DocumentLocation( + document="foo.txt", + line=15, + column=12, + ), + ) + + def test_str(self, target): + actual = str(target) + + assert actual == "[foo.txt 10:20 - foo.txt 15:12]" + + +class TestDefine: + @pytest.mark.parametrize("name, value, prop, expected", ( + ("ANSWER", None, "as_build_flag", "-DANSWER"), + ("ANSWER", None, "as_macro", "#define ANSWER"), + ("ANSWER", None, "as_tuple", ("ANSWER", None)), + ("ANSWER", 42, "as_build_flag", "-DANSWER=42"), + ("ANSWER", 42, "as_macro", "#define ANSWER 42"), + ("ANSWER", 42, "as_tuple", ("ANSWER", 42)), + )) + def test_properties(self, name, value, prop, expected): + target = core.Define(name, value) + + actual = getattr(target, prop) + + assert actual == expected + + @pytest.mark.parametrize("comparison, other, expected", ( + ("__eq__", core.Define(name="FOO", value=42), True), + ("__eq__", core.Define(name="FOO", value=13), False), + ("__eq__", core.Define(name="FOO"), False), + ("__eq__", core.Define(name="BAR", value=42), False), + ("__eq__", core.Define(name="BAR"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + )) + def test_comparison(self, comparison, other, expected): + target = core.Define(name="FOO", value=42) + + actual = getattr(target, comparison)(other) + + assert actual == expected + + +class TestLibrary: + @pytest.mark.parametrize("name, value, prop, expected", ( + ("mylib", None, "as_lib_dep", "mylib"), + ("mylib", None, "as_tuple", ("mylib", None)), + ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), + ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), + )) + def test_properties(self, name, value, prop, expected): + target = core.Library(name, value) + + actual = getattr(target, prop) + + assert actual == expected + + @pytest.mark.parametrize("comparison, other, expected", ( + ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), + ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), + ("__eq__", core.Library(name="libbar", version="1.2.3"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), + )) + def test_comparison(self, comparison, other, expected): + target = core.Library(name="libfoo", version="1.2.3") + + actual = getattr(target, comparison)(other) + + assert actual == expected + + +class TestEsphomeCore: + @pytest.fixture + def target(self, fixture_path): + target = core.EsphomeCore() + target.build_path = "foo/build" + target.config_path = "foo/config" + return target + + def test_reset(self, target): + """Call reset on target and compare to new instance""" + other = core.EsphomeCore() + + target.reset() + + # TODO: raw_config and config differ, should they? + assert target.__dict__ == other.__dict__ + + def test_address__none(self, target): + assert target.address is None + + def test_address__wifi(self, target): + target.config[const.CONF_WIFI] = {const.CONF_USE_ADDRESS: "1.2.3.4"} + target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + + assert target.address == "1.2.3.4" + + def test_address__ethernet(self, target): + target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + + assert target.address == "4.3.2.1" + + def test_is_esp32(self, target): + target.esp_platform = "ESP32" + + assert target.is_esp32 is True + assert target.is_esp8266 is False + + def test_is_esp8266(self, target): + target.esp_platform = "ESP8266" + + assert target.is_esp32 is False + assert target.is_esp8266 is True diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py new file mode 100644 index 0000000000..e48286ae51 --- /dev/null +++ b/tests/unit_tests/test_helpers.py @@ -0,0 +1,208 @@ +import pytest + +from hypothesis import given +from hypothesis.provisional import ip4_addr_strings + +from esphome import helpers + + +@pytest.mark.parametrize("preferred_string, current_strings, expected", ( + ("foo", [], "foo"), + # TODO: Should this actually start at 1? + ("foo", ["foo"], "foo_2"), + ("foo", ("foo",), "foo_2"), + ("foo", ("foo", "foo_2"), "foo_3"), + ("foo", ("foo", "foo_2", "foo_2"), "foo_3"), +)) +def test_ensure_unique_string(preferred_string, current_strings, expected): + actual = helpers.ensure_unique_string(preferred_string, current_strings) + + assert actual == expected + + +@pytest.mark.parametrize("text, expected", ( + ("foo", "foo"), + ("foo\nbar", "foo\nbar"), + ("foo\nbar\neek", "foo\n bar\neek"), +)) +def test_indent_all_but_first_and_last(text, expected): + actual = helpers.indent_all_but_first_and_last(text) + + assert actual == expected + + +@pytest.mark.parametrize("text, expected", ( + ("foo", [" foo"]), + ("foo\nbar", [" foo", " bar"]), + ("foo\nbar\neek", [" foo", " bar", " eek"]), +)) +def test_indent_list(text, expected): + actual = helpers.indent_list(text) + + assert actual == expected + + +@pytest.mark.parametrize("text, expected", ( + ("foo", " foo"), + ("foo\nbar", " foo\n bar"), + ("foo\nbar\neek", " foo\n bar\n eek"), +)) +def test_indent(text, expected): + actual = helpers.indent(text) + + assert actual == expected + + +@pytest.mark.parametrize("string, expected", ( + ("foo", '"foo"'), + ("foo\nbar", '"foo\\012bar"'), + ("foo\\bar", '"foo\\134bar"'), + ('foo "bar"', '"foo \\042bar\\042"'), + ('foo 🐍', '"foo \\360\\237\\220\\215"'), +)) +def test_cpp_string_escape(string, expected): + actual = helpers.cpp_string_escape(string) + + assert actual == expected + + +@pytest.mark.parametrize("host", ( + "127.0.0", "localhost", "127.0.0.b", +)) +def test_is_ip_address__invalid(host): + actual = helpers.is_ip_address(host) + + assert actual is False + + +@given(value=ip4_addr_strings()) +def test_is_ip_address__valid(value): + actual = helpers.is_ip_address(value) + + assert actual is True + + +@pytest.mark.parametrize("var, value, default, expected", ( + ("FOO", None, False, False), + ("FOO", None, True, True), + ("FOO", "", False, False), + ("FOO", "Yes", False, True), + ("FOO", "123", False, True), +)) +def test_get_bool_env(monkeypatch, var, value, default, expected): + if value is None: + monkeypatch.delenv(var, raising=False) + else: + monkeypatch.setenv(var, value) + + actual = helpers.get_bool_env(var, default) + + assert actual == expected + + +@pytest.mark.parametrize("value, expected", ( + (None, False), + ("Yes", True) +)) +def test_is_hassio(monkeypatch, value, expected): + if value is None: + monkeypatch.delenv("ESPHOME_IS_HASSIO", raising=False) + else: + monkeypatch.setenv("ESPHOME_IS_HASSIO", value) + + actual = helpers.is_hassio() + + assert actual == expected + + +def test_walk_files(fixture_path): + path = fixture_path / "helpers" + + actual = list(helpers.walk_files(path)) + + # Ensure paths start with the root + assert all(p.startswith(path.as_posix()) for p in actual) + + +class Test_write_file_if_changed: + def test_src_and_dst_match(self, tmp_path): + text = "A files are unique.\n" + initial = text + dst = tmp_path / "file-a.txt" + dst.write_text(initial) + + helpers.write_file_if_changed(dst, text) + + assert dst.read_text() == text + + def test_src_and_dst_do_not_match(self, tmp_path): + text = "A files are unique.\n" + initial = "B files are unique.\n" + dst = tmp_path / "file-a.txt" + dst.write_text(initial) + + helpers.write_file_if_changed(dst, text) + + assert dst.read_text() == text + + def test_dst_does_not_exist(self, tmp_path): + text = "A files are unique.\n" + dst = tmp_path / "file-a.txt" + + helpers.write_file_if_changed(dst, text) + + assert dst.read_text() == text + + +class Test_copy_file_if_changed: + def test_src_and_dst_match(self, tmp_path, fixture_path): + src = fixture_path / "helpers" / "file-a.txt" + initial = fixture_path / "helpers" / "file-a.txt" + dst = tmp_path / "file-a.txt" + + dst.write_text(initial.read_text()) + + helpers.copy_file_if_changed(src, dst) + + def test_src_and_dst_do_not_match(self, tmp_path, fixture_path): + src = fixture_path / "helpers" / "file-a.txt" + initial = fixture_path / "helpers" / "file-c.txt" + dst = tmp_path / "file-a.txt" + + dst.write_text(initial.read_text()) + + helpers.copy_file_if_changed(src, dst) + + assert src.read_text() == dst.read_text() + + def test_dst_does_not_exist(self, tmp_path, fixture_path): + src = fixture_path / "helpers" / "file-a.txt" + dst = tmp_path / "file-a.txt" + + helpers.copy_file_if_changed(src, dst) + + assert dst.exists() + assert src.read_text() == dst.read_text() + + +@pytest.mark.parametrize("file1, file2, expected", ( + # Same file + ("file-a.txt", "file-a.txt", True), + # Different files, different size + ("file-a.txt", "file-b_1.txt", False), + # Different files, same size + ("file-a.txt", "file-c.txt", False), + # Same files + ("file-b_1.txt", "file-b_2.txt", True), + # Not a file + ("file-a.txt", "", False), + # File doesn't exist + ("file-a.txt", "file-d.txt", False), +)) +def test_file_compare(fixture_path, file1, file2, expected): + path1 = fixture_path / "helpers" / file1 + path2 = fixture_path / "helpers" / file2 + + actual = helpers.file_compare(path1, path2) + + assert actual == expected diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py new file mode 100644 index 0000000000..606c20eea2 --- /dev/null +++ b/tests/unit_tests/test_pins.py @@ -0,0 +1,326 @@ +""" +Please Note: + +These tests cover the process of identifying information about pins, they do not +check if the definition of MCUs and pins is correct. + +""" +import logging + +import pytest + +from esphome.config_validation import Invalid +from esphome.core import EsphomeCore +from esphome import pins + + +MOCK_ESP8266_BOARD_ID = "_mock_esp8266" +MOCK_ESP8266_PINS = {'X0': 16, 'X1': 5, 'X2': 4, 'LED': 2} +MOCK_ESP8266_BOARD_ALIAS_ID = "_mock_esp8266_alias" +MOCK_ESP8266_FLASH_SIZE = pins.FLASH_SIZE_2_MB + +MOCK_ESP32_BOARD_ID = "_mock_esp32" +MOCK_ESP32_PINS = {'Y0': 12, 'Y1': 8, 'Y2': 3, 'LED': 9, "A0": 8} +MOCK_ESP32_BOARD_ALIAS_ID = "_mock_esp32_alias" + +UNKNOWN_PLATFORM = "STM32" + + +@pytest.fixture +def mock_mcu(monkeypatch): + """ + Add a mock MCU into the lists as a stable fixture + """ + pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS + pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE + pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID + pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE + pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS + pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID + yield + del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] + del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] + del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] + del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] + del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] + del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] + + +@pytest.fixture +def core(monkeypatch, mock_mcu): + core = EsphomeCore() + monkeypatch.setattr(pins, "CORE", core) + return core + + +@pytest.fixture +def core_esp8266(core): + core.esp_platform = "ESP8266" + core.board = MOCK_ESP8266_BOARD_ID + return core + + +@pytest.fixture +def core_esp32(core): + core.esp_platform = "ESP32" + core.board = MOCK_ESP32_BOARD_ID + return core + + +class Test_lookup_pin: + @pytest.mark.parametrize("value, expected", ( + ("X1", 5), + ("MOSI", 13), + )) + def test_valid_esp8266_pin(self, core_esp8266, value, expected): + actual = pins._lookup_pin(value) + + assert actual == expected + + def test_valid_esp8266_pin_alias(self, core_esp8266): + core_esp8266.board = MOCK_ESP8266_BOARD_ALIAS_ID + + actual = pins._lookup_pin("X2") + + assert actual == 4 + + @pytest.mark.parametrize("value, expected", ( + ("Y1", 8), + ("A0", 8), + ("MOSI", 23), + )) + def test_valid_esp32_pin(self, core_esp32, value, expected): + actual = pins._lookup_pin(value) + + assert actual == expected + + @pytest.mark.xfail(reason="This may be expected") + def test_valid_32_pin_alias(self, core_esp32): + core_esp32.board = MOCK_ESP32_BOARD_ALIAS_ID + + actual = pins._lookup_pin("Y2") + + assert actual == 3 + + def test_invalid_pin(self, core_esp8266): + with pytest.raises(Invalid, match="Cannot resolve pin name 'X42' for board _mock_esp8266."): + pins._lookup_pin("X42") + + def test_unsupported_platform(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins._lookup_pin("TX") + + +class Test_translate_pin: + @pytest.mark.parametrize("value, expected", ( + (2, 2), + ("3", 3), + ("GPIO4", 4), + ("TX", 1), + ("Y0", 12), + )) + def test_valid_values(self, core_esp32, value, expected): + actual = pins._translate_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value", ({}, None)) + def test_invalid_values(self, core_esp32, value): + with pytest.raises(Invalid, match="This variable only supports"): + pins._translate_pin(value) + + +class Test_validate_gpio_pin: + def test_esp32_valid(self, core_esp32): + actual = pins.validate_gpio_pin("GPIO22") + + assert actual == 22 + + @pytest.mark.parametrize("value, match", ( + (-1, "ESP32: Invalid pin number: -1"), + (40, "ESP32: Invalid pin number: 40"), + (6, "This pin cannot be used on ESP32s and"), + (7, "This pin cannot be used on ESP32s and"), + (8, "This pin cannot be used on ESP32s and"), + (11, "This pin cannot be used on ESP32s and"), + (20, "The pin GPIO20 is not usable on ESP32s"), + (24, "The pin GPIO24 is not usable on ESP32s"), + (28, "The pin GPIO28 is not usable on ESP32s"), + (29, "The pin GPIO29 is not usable on ESP32s"), + (30, "The pin GPIO30 is not usable on ESP32s"), + (31, "The pin GPIO31 is not usable on ESP32s"), + )) + def test_esp32_invalid_pin(self, core_esp32, value, match): + with pytest.raises(Invalid, match=match): + pins.validate_gpio_pin(value) + + @pytest.mark.parametrize("value", (9, 10)) + def test_esp32_warning(self, core_esp32, caplog, value): + caplog.at_level(logging.WARNING) + pins.validate_gpio_pin(value) + + assert len(caplog.messages) == 1 + assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.") + + def test_esp8266_valid(self, core_esp8266): + actual = pins.validate_gpio_pin("GPIO12") + + assert actual == 12 + + @pytest.mark.parametrize("value, match", ( + (-1, "ESP8266: Invalid pin number: -1"), + (18, "ESP8266: Invalid pin number: 18"), + (6, "This pin cannot be used on ESP8266s and"), + (7, "This pin cannot be used on ESP8266s and"), + (8, "This pin cannot be used on ESP8266s and"), + (11, "This pin cannot be used on ESP8266s and"), + )) + def test_esp8266_invalid_pin(self, core_esp8266, value, match): + with pytest.raises(Invalid, match=match): + pins.validate_gpio_pin(value) + + @pytest.mark.parametrize("value", (9, 10)) + def test_esp8266_warning(self, core_esp8266, caplog, value): + caplog.at_level(logging.WARNING) + pins.validate_gpio_pin(value) + + assert len(caplog.messages) == 1 + assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.") + + def test_unknown_device(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins.validate_gpio_pin("0") + + +class Test_input_pin: + @pytest.mark.parametrize("value, expected", ( + ("X0", 16), + )) + def test_valid_esp8266_values(self, core_esp8266, value, expected): + actual = pins.input_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value, expected", ( + ("Y0", 12), + (17, 17), + )) + def test_valid_esp32_values(self, core_esp32, value, expected): + actual = pins.input_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value", (17,)) + def test_invalid_esp8266_values(self, core_esp8266, value): + with pytest.raises(Invalid): + pins.input_pin(value) + + def test_unknown_platform(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins.input_pin(2) + + +class Test_input_pullup_pin: + @pytest.mark.parametrize("value, expected", ( + ("X0", 16), + )) + def test_valid_esp8266_values(self, core_esp8266, value, expected): + actual = pins.input_pullup_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value, expected", ( + ("Y0", 12), + (17, 17), + )) + def test_valid_esp32_values(self, core_esp32, value, expected): + actual = pins.input_pullup_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value", (0,)) + def test_invalid_esp8266_values(self, core_esp8266, value): + with pytest.raises(Invalid): + pins.input_pullup_pin(value) + + def test_unknown_platform(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins.input_pullup_pin(2) + + +class Test_output_pin: + @pytest.mark.parametrize("value, expected", ( + ("X0", 16), + )) + def test_valid_esp8266_values(self, core_esp8266, value, expected): + actual = pins.output_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value, expected", ( + ("Y0", 12), + (17, 17), + )) + def test_valid_esp32_values(self, core_esp32, value, expected): + actual = pins.output_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value", (17,)) + def test_invalid_esp8266_values(self, core_esp8266, value): + with pytest.raises(Invalid): + pins.output_pin(value) + + @pytest.mark.parametrize("value", range(34, 40)) + def test_invalid_esp32_values(self, core_esp32, value): + with pytest.raises(Invalid): + pins.output_pin(value) + + def test_unknown_platform(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins.output_pin(2) + + +class Test_analog_pin: + @pytest.mark.parametrize("value, expected", ( + (17, 17), + )) + def test_valid_esp8266_values(self, core_esp8266, value, expected): + actual = pins.analog_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value, expected", ( + (32, 32), + (39, 39), + )) + def test_valid_esp32_values(self, core_esp32, value, expected): + actual = pins.analog_pin(value) + + assert actual == expected + + @pytest.mark.parametrize("value", ("X0",)) + def test_invalid_esp8266_values(self, core_esp8266, value): + with pytest.raises(Invalid): + pins.analog_pin(value) + + @pytest.mark.parametrize("value", ("Y0",)) + def test_invalid_esp32_values(self, core_esp32, value): + with pytest.raises(Invalid): + pins.analog_pin(value) + + def test_unknown_platform(self, core): + core.esp_platform = UNKNOWN_PLATFORM + + with pytest.raises(NotImplementedError): + pins.analog_pin(2) From 4cb30a22ac804124e2198784e34bd6c575ed5d96 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Fri, 13 Mar 2020 13:27:19 -0400 Subject: [PATCH 173/412] Corrections to default register values of ATM90E32 component (#982) * Corrections to default register values of ATM90E32 component --- esphome/components/atm90e32/atm90e32.cpp | 27 ++++++++++++++-------- esphome/components/atm90e32/atm90e32.h | 6 +++-- esphome/components/atm90e32/atm90e32_reg.h | 12 +++++----- esphome/components/atm90e32/sensor.py | 11 +++++++-- tests/test1.yaml | 15 ++++++------ 5 files changed, 44 insertions(+), 27 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index bc1e326147..85e38fce3e 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -68,12 +68,17 @@ void ATM90E32Component::update() { } void ATM90E32Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up ATM90E32Component..."); + ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - uint16_t mmode0 = 0x185; + uint16_t mmode0 = 0x87; // 3P4W 50Hz if (line_freq_ == 60) { - mmode0 |= 1 << 12; + mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz + } + + if (current_phases_ == 2) { + mmode0 |= 1 << 8; // sets 8th bit to 1, 3P3W + mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) } this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset @@ -84,13 +89,15 @@ void ATM90E32Component::setup() { this->mark_failed(); return; } - - this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0x0A55); // ZX2, ZX1, ZX0 pin config - this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) - this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels - this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x0AFC); // Active Startup Power Threshold = 50% - this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold = 50% - this->write16_(ATM90E32_REGISTER_PPHASETH, 0x00BC); // Active Phase Threshold = 10% + this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 + this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) + this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 + this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% + this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 + this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_); // A Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_); // A line current gain this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_); // B Voltage rms gain diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 3daa31d15d..eb5de3878c 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -29,6 +29,7 @@ class ATM90E32Component : public PollingComponent, chip_temperature_sensor_ = chip_temperature_sensor; } void set_line_freq(int freq) { line_freq_ = freq; } + void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } protected: @@ -55,8 +56,8 @@ class ATM90E32Component : public PollingComponent, float get_chip_temperature_(); struct ATM90E32Phase { - uint16_t volt_gain_{41820}; - uint16_t ct_gain_{25498}; + uint16_t volt_gain_{7305}; + uint16_t ct_gain_{27961}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; @@ -67,6 +68,7 @@ class ATM90E32Component : public PollingComponent, sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; + int current_phases_{3}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index ca3715a2a8..dc2048fbc2 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -234,12 +234,12 @@ static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE; // Lower Word (B RMS static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS Current) /* THD, FREQUENCY, ANGLE & TEMPTEMP REGISTERS*/ -static const uint16_t ATM90E32_REGISTER_THDNUA = 0xF1; // A Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNUB = 0xF2; // B Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNUC = 0xF3; // C Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNIA = 0xF5; // A Current THD+N -static const uint16_t ATM90E32_REGISTER_THDNIB = 0xF6; // B Current THD+N -static const uint16_t ATM90E32_REGISTER_THDNIC = 0xF7; // C Current THD+N +static const uint16_t ATM90E32_REGISTER_UPEAKA = 0xF1; // A Voltage Peak +static const uint16_t ATM90E32_REGISTER_UPEAKB = 0xF2; // B Voltage Peak +static const uint16_t ATM90E32_REGISTER_UPEAKC = 0xF3; // C Voltage Peak +static const uint16_t ATM90E32_REGISTER_IPEAKA = 0xF5; // A Current Peak +static const uint16_t ATM90E32_REGISTER_IPEAKB = 0xF6; // B Current Peak +static const uint16_t ATM90E32_REGISTER_IPEAKC = 0xF7; // C Current Peak static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8; // Frequency static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9; // A Mean Phase Angle static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA; // B Mean Phase Angle diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 490fdb4719..fc526dfbc0 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -14,12 +14,17 @@ CONF_REACTIVE_POWER = 'reactive_power' CONF_LINE_FREQUENCY = 'line_frequency' CONF_CHIP_TEMPERATURE = 'chip_temperature' CONF_GAIN_PGA = 'gain_pga' +CONF_CURRENT_PHASES = 'current_phases' CONF_GAIN_VOLTAGE = 'gain_voltage' CONF_GAIN_CT = 'gain_ct' LINE_FREQS = { '50HZ': 50, '60HZ': 60, } +CURRENT_PHASES = { + '2': 2, + '3': 3, +} PGA_GAINS = { '1X': 0x0, '2X': 0x15, @@ -36,8 +41,8 @@ ATM90E32_PHASE_SCHEMA = cv.Schema({ cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_VOLT_AMPS_REACTIVE, ICON_LIGHTBULB, 2), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), - cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t, - cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t, + cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, + cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, }) CONFIG_SCHEMA = cv.Schema({ @@ -48,6 +53,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Optional(CONF_CURRENT_PHASES, default='3'): cv.enum(CURRENT_PHASES, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), }).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) @@ -85,4 +91,5 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) diff --git a/tests/test1.yaml b/tests/test1.yaml index b3b397ddb1..9102873fb6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -265,8 +265,8 @@ sensor: name: "EMON Reactive Power CT1" power_factor: name: "EMON Power Factor CT1" - gain_voltage: 47660 - gain_ct: 12577 + gain_voltage: 7305 + gain_ct: 27961 phase_b: current: name: "EMON CT2 Current" @@ -276,8 +276,8 @@ sensor: name: "EMON Reactive Power CT2" power_factor: name: "EMON Power Factor CT2" - gain_voltage: 47660 - gain_ct: 12577 + gain_voltage: 7305 + gain_ct: 27961 phase_c: current: name: "EMON CT3 Current" @@ -287,13 +287,14 @@ sensor: name: "EMON Reactive Power CT3" power_factor: name: "EMON Power Factor CT3" - gain_voltage: 47660 - gain_ct: 12577 + gain_voltage: 7305 + gain_ct: 27961 frequency: name: "EMON Line Frequency" chip_temperature: name: "EMON Chip Temp A" - line_frequency: 50Hz + line_frequency: 60Hz + current_phases: 3 gain_pga: 2X - platform: bh1750 name: "Living Room Brightness 3" From 4ec636c08feb2e4d7aecf37e33580f2371df3903 Mon Sep 17 00:00:00 2001 From: Germain Masse Date: Sat, 21 Mar 2020 19:31:07 +0100 Subject: [PATCH 174/412] Add AHT10 sensor (#949) --- esphome/components/aht10/__init__.py | 0 esphome/components/aht10/aht10.cpp | 127 +++++++++++++++++++++++++++ esphome/components/aht10/aht10.h | 26 ++++++ esphome/components/aht10/sensor.py | 30 +++++++ tests/test3.yaml | 5 ++ 5 files changed, 188 insertions(+) create mode 100644 esphome/components/aht10/__init__.py create mode 100644 esphome/components/aht10/aht10.cpp create mode 100644 esphome/components/aht10/aht10.h create mode 100644 esphome/components/aht10/sensor.py diff --git a/esphome/components/aht10/__init__.py b/esphome/components/aht10/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp new file mode 100644 index 0000000000..6951254e0d --- /dev/null +++ b/esphome/components/aht10/aht10.cpp @@ -0,0 +1,127 @@ +// Implementation based on: +// - AHT10: https://github.com/Thinary/AHT10 +// - Official Datasheet (cn): +// http://www.aosong.com/userfiles/files/media/aht10%E8%A7%84%E6%A0%BC%E4%B9%A6v1_1%EF%BC%8820191015%EF%BC%89.pdf +// - Unofficial Translated Datasheet (en): +// https://wiki.liutyi.info/download/attachments/30507639/Aosong_AHT10_en_draft_0c.pdf +// +// When configured for humidity, the log 'Components should block for at most 20-30ms in loop().' will be generated in +// verbose mode. This is due to technical specs of the sensor and can not be avoided. +// +// According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost +// immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best +// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. + +#include "aht10.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace aht10 { + +static const char *TAG = "aht10"; +static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; +static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement +static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms +static const uint8_t AHT10_ATTEMPS = 3; // safety margin, normally 3 attemps are enough: 3*30=90ms + +void AHT10Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + + if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + uint8_t data; + if (!this->read_byte(0, &data, AHT10_DEFAULT_DELAY)) { + ESP_LOGD(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED + ESP_LOGE(TAG, "AHT10 calibration failed!"); + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, "AHT10 calibrated"); +} + +void AHT10Component::update() { + if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->status_set_warning(); + return; + } + uint8_t data[6]; + uint8_t delay = AHT10_DEFAULT_DELAY; + if (this->humidity_sensor_ != nullptr) + delay = AHT10_HUMIDITY_DELAY; + for (int i = 0; i < AHT10_ATTEMPS; ++i) { + ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); + if (!this->read_bytes(0, data, 6, delay)) { + ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); + } else if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy + ESP_LOGD(TAG, "AHT10 is busy, waiting..."); + } else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) { + // Unrealistic humidity (0x0) + if (this->humidity_sensor_ == nullptr) { + ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required"); + break; + } else { + ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); + if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->status_set_warning(); + return; + } + } + } else { + // data is valid, we can break the loop + ESP_LOGVV(TAG, "Answer at %6ld", millis()); + break; + } + } + if ((data[0] & 0x80) == 0x80) { + ESP_LOGE(TAG, "Measurements reading timed-out!"); + this->status_set_warning(); + return; + } + + uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; + uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; + + float temperature = ((200.0 * (float) raw_temperature) / 1048576.0) - 50.0; + float humidity; + if (raw_humidity == 0) { // unrealistic value + humidity = NAN; + } else { + humidity = (float) raw_humidity * 100.0 / 1048576.0; + } + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + if (isnan(humidity)) + ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); +} + +float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } + +void AHT10Component::dump_config() { + ESP_LOGCONFIG(TAG, "AHT10:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +} // namespace aht10 +} // namespace esphome diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h new file mode 100644 index 0000000000..bfb6b07a7a --- /dev/null +++ b/esphome/components/aht10/aht10.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace aht10 { + +class AHT10Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + protected: + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace aht10 +} // namespace esphome diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py new file mode 100644 index 0000000000..71b0adce79 --- /dev/null +++ b/esphome/components/aht10/sensor.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +aht10_ns = cg.esphome_ns.namespace('aht10') +AHT10Component = aht10_ns.class_('AHT10Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(AHT10Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 2e1dd3f078..9407cab687 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -221,6 +221,11 @@ sensor: - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world + - platform: aht10 + temperature: + name: "Temperature" + humidity: + name: "Humidity" - platform: am2320 temperature: name: "Temperature" From 25cdbacecc01a52eee82b34bcb68f8450ada33a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 30 Mar 2020 18:32:48 +0100 Subject: [PATCH 175/412] wifi: retry connection if the connection is not valid (#994) --- esphome/components/wifi/wifi_component.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e68ab1765b..40f12a8adc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -420,6 +420,12 @@ void WiFiComponent::check_connecting_finished() { wl_status_t status = this->wifi_sta_status_(); if (status == WL_CONNECTED) { + if (WiFi.SSID().equals("")) { + ESP_LOGW(TAG, "Incomplete connection."); + this->retry_connect(); + return; + } + ESP_LOGI(TAG, "WiFi Connected!"); this->print_connect_params_(); From 4620ad612432a45d01a1cee7dffb249a3ef3f32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Biernacki?= Date: Sat, 4 Apr 2020 23:23:23 +0200 Subject: [PATCH 176/412] Support for pcd8544 (nokia 5110 and 3310) screen (#973) * First version of working compontent for pc8544 screen * Fixed lint errors * Fixed lint errors #2 --- esphome/components/pcd8544/__init__.py | 0 esphome/components/pcd8544/display.py | 39 ++++++++ esphome/components/pcd8544/pcd_8544.cpp | 127 ++++++++++++++++++++++++ esphome/components/pcd8544/pcd_8544.h | 75 ++++++++++++++ tests/test1.yaml | 6 ++ 5 files changed, 247 insertions(+) create mode 100644 esphome/components/pcd8544/__init__.py create mode 100644 esphome/components/pcd8544/display.py create mode 100644 esphome/components/pcd8544/pcd_8544.cpp create mode 100644 esphome/components/pcd8544/pcd_8544.h diff --git a/esphome/components/pcd8544/__init__.py b/esphome/components/pcd8544/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py new file mode 100644 index 0000000000..e47937e46a --- /dev/null +++ b/esphome/components/pcd8544/display.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import ( + CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, CONF_CS_PIN, +) + +DEPENDENCIES = ['spi'] + +pcd8544_ns = cg.esphome_ns.namespace('pcd8544') +PCD8544 = pcd8544_ns.class_('PCD8544', cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice) + + +CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PCD8544), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE +}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + yield cg.register_component(var, config) + yield display.register_display(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp new file mode 100644 index 0000000000..ed9d1bbd43 --- /dev/null +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -0,0 +1,127 @@ +#include "pcd_8544.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pcd8544 { + +static const char *TAG = "pcd_8544"; + +void PCD8544::setup_pins_() { + this->spi_setup(); + this->init_reset_(); + this->dc_pin_->setup(); +} + +void PCD8544::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void PCD8544::initialize() { + this->init_internal_(this->get_buffer_length_()); + + this->command(this->PCD8544_FUNCTIONSET | this->PCD8544_EXTENDEDINSTRUCTION); + // LCD bias select (4 is optimal?) + this->command(this->PCD8544_SETBIAS | 0x04); + + // contrast + // TODO: in future version we may add a user a control over contrast + this->command(this->PCD8544_SETVOP | 0x7f); // Experimentally determined + + // normal mode + this->command(this->PCD8544_FUNCTIONSET); + + // Set display to Normal + this->command(this->PCD8544_DISPLAYCONTROL | this->PCD8544_DISPLAYNORMAL); +} + +void PCD8544::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} +void PCD8544::end_command_() { this->disable(); } +void PCD8544::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +void PCD8544::end_data_() { this->disable(); } + +int PCD8544::get_width_internal() { return 84; } +int PCD8544::get_height_internal() { return 48; } + +size_t PCD8544::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT PCD8544::display() { + uint8_t col, maxcol, p; + + for (p = 0; p < 6; p++) { + this->command(this->PCD8544_SETYADDR | p); + + // start at the beginning of the row + col = 0; + maxcol = this->get_width_internal() - 1; + + this->command(this->PCD8544_SETXADDR | col); + + this->start_data_(); + for (; col <= maxcol; col++) { + this->write_byte(this->buffer_[(this->get_width_internal() * p) + col]); + } + this->end_data_(); + } + + this->command(this->PCD8544_SETYADDR); +} + +void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, int color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) { + return; + } + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y % 8; + if (color) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} + +void PCD8544::dump_config() { + LOG_DISPLAY("", "PCD8544", this); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void PCD8544::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void PCD8544::update() { + this->do_update_(); + this->display(); +} + +void PCD8544::fill(int color) { + uint8_t fill = color ? 0xFF : 0x00; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} + +} // namespace pcd8544 +} // namespace esphome diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h new file mode 100644 index 0000000000..a1c247bf7b --- /dev/null +++ b/esphome/components/pcd8544/pcd_8544.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace pcd8544 { + +class PCD8544 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + const uint8_t PCD8544_POWERDOWN = 0x04; + const uint8_t PCD8544_ENTRYMODE = 0x02; + const uint8_t PCD8544_EXTENDEDINSTRUCTION = 0x01; + + const uint8_t PCD8544_DISPLAYBLANK = 0x0; + const uint8_t PCD8544_DISPLAYNORMAL = 0x4; + const uint8_t PCD8544_DISPLAYALLON = 0x1; + const uint8_t PCD8544_DISPLAYINVERTED = 0x5; + + const uint8_t PCD8544_FUNCTIONSET = 0x20; + const uint8_t PCD8544_DISPLAYCONTROL = 0x08; + const uint8_t PCD8544_SETYADDR = 0x40; + const uint8_t PCD8544_SETXADDR = 0x80; + + const uint8_t PCD8544_SETTEMP = 0x04; + const uint8_t PCD8544_SETBIAS = 0x10; + const uint8_t PCD8544_SETVOP = 0x80; + + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + void command(uint8_t value); + void data(uint8_t value); + + void initialize(); + void dump_config() override; + void HOT display(); + + void update() override; + + void fill(int color) override; + + void setup() override { + this->setup_pins_(); + this->initialize(); + } + + protected: + void draw_absolute_pixel_internal(int x, int y, int color) override; + + void setup_pins_(); + + void init_reset_(); + + size_t get_buffer_length_(); + + void start_command_(); + void end_command_(); + void start_data_(); + void end_data_(); + + int get_width_internal() override; + int get_height_internal() override; + + GPIOPin *reset_pin_; + GPIOPin *dc_pin_; +}; + +} // namespace pcd8544 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 9102873fb6..1fc6b650c4 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1444,6 +1444,12 @@ display: lambda: |- it.set_component_value("gauge", 50); it.set_component_text("textview", "Hello World!"); +- platform: pcd8544 + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1306_i2c model: "SSD1306_128X64" reset_pin: GPIO23 From 79248e8b7477aa577ee64df2fe3878e9efaa582f Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 5 Apr 2020 13:42:43 -0300 Subject: [PATCH 177/412] fix servo bug restoring state and starting servo detached (#1008) --- esphome/components/servo/servo.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 19165be23d..a37188740c 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -36,11 +36,11 @@ class Servo : public Component { this->rtc_ = global_preferences.make_preference(global_servo_id); global_servo_id++; if (this->rtc_.load(&v)) { - this->write(v); + this->output_->set_level(v); return; } } - this->write(0.0f); + this->detach(); } void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } From 3b7a47fb907ed1e2bf91ab304c97279752c81fde Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Sun, 5 Apr 2020 21:50:52 +0300 Subject: [PATCH 178/412] VSCode devcontainer support (#914) * Devcontainer * Removed header from json --- .devcontainer/devcontainer.json | 31 +++++++++++++++++++++++++++++++ docker/Dockerfile.dev | 13 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 docker/Dockerfile.dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..5ce1768f5f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "ESPHome Dev", + "context": "..", + "dockerFile": "../docker/Dockerfile.dev", + "postCreateCommand": "mkdir -p config && pip3 install -e .", + "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], + "appPort": 6052, + "extensions": [ + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", + "yaml.customTags": [ + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] + } +} diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000000..a3871e2513 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM esphome/esphome-base-amd64:2.0.1 + +COPY . . + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + python3-wheel \ + net-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspaces +ENV SHELL /bin/bash From 43cf3063e0c2a6af80bb23288870b71ff978c2dc Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 5 Apr 2020 22:14:49 -0300 Subject: [PATCH 179/412] removes comments from lambda (#998) * removes comments from lambda * include comments in lambda test * pylint no else return --- esphome/core.py | 15 +- tests/unit_tests/test_core.py | 272 +++++++++++++++++----------------- 2 files changed, 152 insertions(+), 135 deletions(-) diff --git a/esphome/core.py b/esphome/core.py index 7734636a76..4b1d3b2115 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -230,10 +230,23 @@ class Lambda: self._parts = None self._requires_ids = None + # https://stackoverflow.com/a/241506/229052 + def comment_remover(self, text): + def replacer(match): + s = match.group(0) + if s.startswith('/'): + return " " # note: a space and not an empty string + return s + pattern = re.compile( + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE + ) + return re.sub(pattern, replacer, text) + @property def parts(self): if self._parts is None: - self._parts = re.split(LAMBDA_PROG, self._value) + self._parts = re.split(LAMBDA_PROG, self.comment_remover(self._value)) return self._parts @property diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index c02aed6447..14f6990bd9 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -9,11 +9,11 @@ from esphome import core, const class TestHexInt: @pytest.mark.parametrize("value, expected", ( - (1, "0x01"), - (255, "0xFF"), - (128, "0x80"), - (256, "0x100"), - (-1, "-0x01"), # TODO: this currently fails + (1, "0x01"), + (255, "0xFF"), + (128, "0x80"), + (256, "0x100"), + (-1, "-0x01"), # TODO: this currently fails )) def test_str(self, value, expected): target = core.HexInt(value) @@ -88,24 +88,24 @@ def test_is_approximately_integer__not_in_range(value): class TestTimePeriod: @pytest.mark.parametrize("kwargs, expected", ( - ({}, {}), - ({"microseconds": 1}, {"microseconds": 1}), - ({"microseconds": 1.0001}, {"microseconds": 1}), - ({"milliseconds": 2}, {"milliseconds": 2}), - ({"milliseconds": 2.0001}, {"milliseconds": 2}), - ({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}), - ({"seconds": 3}, {"seconds": 3}), - ({"seconds": 3.0001}, {"seconds": 3}), - ({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}), - ({"minutes": 4}, {"minutes": 4}), - ({"minutes": 4.0001}, {"minutes": 4}), - ({"minutes": 4.1}, {"minutes": 4, "seconds": 6}), - ({"hours": 5}, {"hours": 5}), - ({"hours": 5.0001}, {"hours": 5}), - ({"hours": 5.1}, {"hours": 5, "minutes": 6}), - ({"days": 6}, {"days": 6}), - ({"days": 6.0001}, {"days": 6}), - ({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}), + ({}, {}), + ({"microseconds": 1}, {"microseconds": 1}), + ({"microseconds": 1.0001}, {"microseconds": 1}), + ({"milliseconds": 2}, {"milliseconds": 2}), + ({"milliseconds": 2.0001}, {"milliseconds": 2}), + ({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}), + ({"seconds": 3}, {"seconds": 3}), + ({"seconds": 3.0001}, {"seconds": 3}), + ({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}), + ({"minutes": 4}, {"minutes": 4}), + ({"minutes": 4.0001}, {"minutes": 4}), + ({"minutes": 4.1}, {"minutes": 4, "seconds": 6}), + ({"hours": 5}, {"hours": 5}), + ({"hours": 5.0001}, {"hours": 5}), + ({"hours": 5.1}, {"hours": 5, "minutes": 6}), + ({"days": 6}, {"days": 6}), + ({"days": 6.0001}, {"days": 6}), + ({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}), )) def test_init(self, kwargs, expected): target = core.TimePeriod(**kwargs) @@ -119,24 +119,24 @@ class TestTimePeriod: core.TimePeriod(microseconds=1.1) @pytest.mark.parametrize("kwargs, expected", ( - ({}, "0s"), - ({"microseconds": 1}, "1us"), - ({"microseconds": 1.0001}, "1us"), - ({"milliseconds": 2}, "2ms"), - ({"milliseconds": 2.0001}, "2ms"), - ({"milliseconds": 2.01}, "2010us"), - ({"seconds": 3}, "3s"), - ({"seconds": 3.0001}, "3s"), - ({"seconds": 3.01}, "3010ms"), - ({"minutes": 4}, "4min"), - ({"minutes": 4.0001}, "4min"), - ({"minutes": 4.1}, "246s"), - ({"hours": 5}, "5h"), - ({"hours": 5.0001}, "5h"), - ({"hours": 5.1}, "306min"), - ({"days": 6}, "6d"), - ({"days": 6.0001}, "6d"), - ({"days": 6.1}, "8784min"), + ({}, "0s"), + ({"microseconds": 1}, "1us"), + ({"microseconds": 1.0001}, "1us"), + ({"milliseconds": 2}, "2ms"), + ({"milliseconds": 2.0001}, "2ms"), + ({"milliseconds": 2.01}, "2010us"), + ({"seconds": 3}, "3s"), + ({"seconds": 3.0001}, "3s"), + ({"seconds": 3.01}, "3010ms"), + ({"minutes": 4}, "4min"), + ({"minutes": 4.0001}, "4min"), + ({"minutes": 4.1}, "246s"), + ({"hours": 5}, "5h"), + ({"hours": 5.0001}, "5h"), + ({"hours": 5.1}, "306min"), + ({"days": 6}, "6d"), + ({"days": 6.0001}, "6d"), + ({"days": 6.1}, "8784min"), )) def test_str(self, kwargs, expected): target = core.TimePeriod(**kwargs) @@ -146,59 +146,59 @@ class TestTimePeriod: assert actual == expected @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.TimePeriod(microseconds=900), False), - ("__eq__", core.TimePeriod(milliseconds=1), True), - ("__eq__", core.TimePeriod(microseconds=1100), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), + ("__eq__", core.TimePeriod(microseconds=900), False), + ("__eq__", core.TimePeriod(milliseconds=1), True), + ("__eq__", core.TimePeriod(microseconds=1100), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), - ("__ne__", core.TimePeriod(microseconds=900), True), - ("__ne__", core.TimePeriod(milliseconds=1), False), - ("__ne__", core.TimePeriod(microseconds=1100), True), - ("__ne__", 1000, NotImplemented), - ("__ne__", "1000", NotImplemented), - ("__ne__", True, NotImplemented), - ("__ne__", object(), NotImplemented), - ("__ne__", None, NotImplemented), + ("__ne__", core.TimePeriod(microseconds=900), True), + ("__ne__", core.TimePeriod(milliseconds=1), False), + ("__ne__", core.TimePeriod(microseconds=1100), True), + ("__ne__", 1000, NotImplemented), + ("__ne__", "1000", NotImplemented), + ("__ne__", True, NotImplemented), + ("__ne__", object(), NotImplemented), + ("__ne__", None, NotImplemented), - ("__lt__", core.TimePeriod(microseconds=900), False), - ("__lt__", core.TimePeriod(milliseconds=1), False), - ("__lt__", core.TimePeriod(microseconds=1100), True), - ("__lt__", 1000, NotImplemented), - ("__lt__", "1000", NotImplemented), - ("__lt__", True, NotImplemented), - ("__lt__", object(), NotImplemented), - ("__lt__", None, NotImplemented), + ("__lt__", core.TimePeriod(microseconds=900), False), + ("__lt__", core.TimePeriod(milliseconds=1), False), + ("__lt__", core.TimePeriod(microseconds=1100), True), + ("__lt__", 1000, NotImplemented), + ("__lt__", "1000", NotImplemented), + ("__lt__", True, NotImplemented), + ("__lt__", object(), NotImplemented), + ("__lt__", None, NotImplemented), - ("__gt__", core.TimePeriod(microseconds=900), True), - ("__gt__", core.TimePeriod(milliseconds=1), False), - ("__gt__", core.TimePeriod(microseconds=1100), False), - ("__gt__", 1000, NotImplemented), - ("__gt__", "1000", NotImplemented), - ("__gt__", True, NotImplemented), - ("__gt__", object(), NotImplemented), - ("__gt__", None, NotImplemented), + ("__gt__", core.TimePeriod(microseconds=900), True), + ("__gt__", core.TimePeriod(milliseconds=1), False), + ("__gt__", core.TimePeriod(microseconds=1100), False), + ("__gt__", 1000, NotImplemented), + ("__gt__", "1000", NotImplemented), + ("__gt__", True, NotImplemented), + ("__gt__", object(), NotImplemented), + ("__gt__", None, NotImplemented), - ("__le__", core.TimePeriod(microseconds=900), False), - ("__le__", core.TimePeriod(milliseconds=1), True), - ("__le__", core.TimePeriod(microseconds=1100), True), - ("__le__", 1000, NotImplemented), - ("__le__", "1000", NotImplemented), - ("__le__", True, NotImplemented), - ("__le__", object(), NotImplemented), - ("__le__", None, NotImplemented), + ("__le__", core.TimePeriod(microseconds=900), False), + ("__le__", core.TimePeriod(milliseconds=1), True), + ("__le__", core.TimePeriod(microseconds=1100), True), + ("__le__", 1000, NotImplemented), + ("__le__", "1000", NotImplemented), + ("__le__", True, NotImplemented), + ("__le__", object(), NotImplemented), + ("__le__", None, NotImplemented), - ("__ge__", core.TimePeriod(microseconds=900), True), - ("__ge__", core.TimePeriod(milliseconds=1), True), - ("__ge__", core.TimePeriod(microseconds=1100), False), - ("__ge__", 1000, NotImplemented), - ("__ge__", "1000", NotImplemented), - ("__ge__", True, NotImplemented), - ("__ge__", object(), NotImplemented), - ("__ge__", None, NotImplemented), + ("__ge__", core.TimePeriod(microseconds=900), True), + ("__ge__", core.TimePeriod(milliseconds=1), True), + ("__ge__", core.TimePeriod(microseconds=1100), False), + ("__ge__", 1000, NotImplemented), + ("__ge__", "1000", NotImplemented), + ("__ge__", True, NotImplemented), + ("__ge__", object(), NotImplemented), + ("__ge__", None, NotImplemented), )) def test_comparison(self, comparison, other, expected): target = core.TimePeriod(microseconds=1000) @@ -211,6 +211,10 @@ class TestTimePeriod: SAMPLE_LAMBDA = """ it.strftime(64, 0, id(my_font), TextAlign::TOP_CENTER, "%H:%M:%S", id(esptime).now()); it.printf(64, 16, id(my_font2), TextAlign::TOP_CENTER, "%.1f°C (%.1f%%)", id( office_tmp ).state, id(office_hmd).state); +//id(my_commented_id) +int x = 4;/* id(my_commented_id2) +id(my_commented_id3) +*/ """ @@ -246,7 +250,7 @@ class TestLambda: "state, ", "office_hmd", ".", - "state);" + "state);\n \nint x = 4; " ] def test_requires_ids(self): @@ -293,11 +297,11 @@ class TestID: return core.ID(None, is_declaration=True, type="binary_sensor::Example") @pytest.mark.parametrize("id, is_manual, expected", ( - ("foo", None, True), - (None, None, False), - ("foo", True, True), - ("foo", False, False), - (None, True, True), + ("foo", None, True), + (None, None, False), + ("foo", True, True), + ("foo", False, False), + (None, True, True), )) def test_init__resolve_is_manual(self, id, is_manual, expected): target = core.ID(id, is_manual=is_manual) @@ -305,10 +309,10 @@ class TestID: assert target.is_manual == expected @pytest.mark.parametrize("registered_ids, expected", ( - ([], "binary_sensor_example"), - (["binary_sensor_example"], "binary_sensor_example_2"), - (["foo"], "binary_sensor_example"), - (["binary_sensor_example", "foo", "binary_sensor_example_2"], "binary_sensor_example_3"), + ([], "binary_sensor_example"), + (["binary_sensor_example"], "binary_sensor_example_2"), + (["foo"], "binary_sensor_example"), + (["binary_sensor_example", "foo", "binary_sensor_example_2"], "binary_sensor_example_3"), )) def test_resolve(self, target, registered_ids, expected): actual = target.resolve(registered_ids) @@ -326,13 +330,13 @@ class TestID: for n in ("id", "is_declaration", "type", "is_manual")) @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.ID(id="foo"), True), - ("__eq__", core.ID(id="bar"), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), + ("__eq__", core.ID(id="foo"), True), + ("__eq__", core.ID(id="bar"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), )) def test_comparison(self, comparison, other, expected): target = core.ID(id="foo") @@ -381,12 +385,12 @@ class TestDocumentRange: class TestDefine: @pytest.mark.parametrize("name, value, prop, expected", ( - ("ANSWER", None, "as_build_flag", "-DANSWER"), - ("ANSWER", None, "as_macro", "#define ANSWER"), - ("ANSWER", None, "as_tuple", ("ANSWER", None)), - ("ANSWER", 42, "as_build_flag", "-DANSWER=42"), - ("ANSWER", 42, "as_macro", "#define ANSWER 42"), - ("ANSWER", 42, "as_tuple", ("ANSWER", 42)), + ("ANSWER", None, "as_build_flag", "-DANSWER"), + ("ANSWER", None, "as_macro", "#define ANSWER"), + ("ANSWER", None, "as_tuple", ("ANSWER", None)), + ("ANSWER", 42, "as_build_flag", "-DANSWER=42"), + ("ANSWER", 42, "as_macro", "#define ANSWER 42"), + ("ANSWER", 42, "as_tuple", ("ANSWER", 42)), )) def test_properties(self, name, value, prop, expected): target = core.Define(name, value) @@ -396,16 +400,16 @@ class TestDefine: assert actual == expected @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.Define(name="FOO", value=42), True), - ("__eq__", core.Define(name="FOO", value=13), False), - ("__eq__", core.Define(name="FOO"), False), - ("__eq__", core.Define(name="BAR", value=42), False), - ("__eq__", core.Define(name="BAR"), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), + ("__eq__", core.Define(name="FOO", value=42), True), + ("__eq__", core.Define(name="FOO", value=13), False), + ("__eq__", core.Define(name="FOO"), False), + ("__eq__", core.Define(name="BAR", value=42), False), + ("__eq__", core.Define(name="BAR"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), )) def test_comparison(self, comparison, other, expected): target = core.Define(name="FOO", value=42) @@ -417,10 +421,10 @@ class TestDefine: class TestLibrary: @pytest.mark.parametrize("name, value, prop, expected", ( - ("mylib", None, "as_lib_dep", "mylib"), - ("mylib", None, "as_tuple", ("mylib", None)), - ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), - ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), + ("mylib", None, "as_lib_dep", "mylib"), + ("mylib", None, "as_tuple", ("mylib", None)), + ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), + ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), )) def test_properties(self, name, value, prop, expected): target = core.Library(name, value) @@ -430,14 +434,14 @@ class TestLibrary: assert actual == expected @pytest.mark.parametrize("comparison, other, expected", ( - ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), - ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), - ("__eq__", core.Library(name="libbar", version="1.2.3"), False), - ("__eq__", 1000, NotImplemented), - ("__eq__", "1000", NotImplemented), - ("__eq__", True, NotImplemented), - ("__eq__", object(), NotImplemented), - ("__eq__", None, NotImplemented), + ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), + ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), + ("__eq__", core.Library(name="libbar", version="1.2.3"), False), + ("__eq__", 1000, NotImplemented), + ("__eq__", "1000", NotImplemented), + ("__eq__", True, NotImplemented), + ("__eq__", object(), NotImplemented), + ("__eq__", None, NotImplemented), )) def test_comparison(self, comparison, other, expected): target = core.Library(name="libfoo", version="1.2.3") From dea6675c218fe4b3c6435cadb33d7da74dc18b0f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 6 Apr 2020 19:11:41 +0200 Subject: [PATCH 180/412] Add HM3301 laser dust detection sensor (#963) * Add HM3301 laser dust detection sensor * Fixed after lint * Fixed after lint * added status clear warning --- esphome/components/hm3301/__init__.py | 0 esphome/components/hm3301/hm3301.cpp | 82 +++++++++++++++++++++++++++ esphome/components/hm3301/hm3301.h | 42 ++++++++++++++ esphome/components/hm3301/sensor.py | 43 ++++++++++++++ platformio.ini | 1 + tests/test1.yaml | 7 +++ tests/test3.yaml | 7 +++ 7 files changed, 182 insertions(+) create mode 100644 esphome/components/hm3301/__init__.py create mode 100644 esphome/components/hm3301/hm3301.cpp create mode 100644 esphome/components/hm3301/hm3301.h create mode 100644 esphome/components/hm3301/sensor.py diff --git a/esphome/components/hm3301/__init__.py b/esphome/components/hm3301/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp new file mode 100644 index 0000000000..6456ee354a --- /dev/null +++ b/esphome/components/hm3301/hm3301.cpp @@ -0,0 +1,82 @@ +#include "hm3301.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hm3301 { + +static const char *TAG = "hm3301.sensor"; + +static const uint8_t PM_1_0_VALUE_INDEX = 5; +static const uint8_t PM_2_5_VALUE_INDEX = 6; +static const uint8_t PM_10_0_VALUE_INDEX = 7; + +void HM3301Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HM3301..."); + hm3301_ = new HM330X(); + error_code_ = hm3301_->init(); + if (error_code_ != NO_ERROR) { + this->mark_failed(); + return; + } +} + +void HM3301Component::dump_config() { + ESP_LOGCONFIG(TAG, "HM3301:"); + LOG_I2C_DEVICE(this); + if (error_code_ == ERROR_COMM) { + ESP_LOGE(TAG, "Communication with HM3301 failed!"); + } + + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); +} + +float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } + +void HM3301Component::update() { + if (!this->read_sensor_value_(data_buffer_)) { + ESP_LOGW(TAG, "Read result failed"); + this->status_set_warning(); + return; + } + + if (!this->validate_checksum_(data_buffer_)) { + ESP_LOGW(TAG, "Checksum validation failed"); + this->status_set_warning(); + return; + } + + if (this->pm_1_0_sensor_ != nullptr) { + uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); + this->pm_1_0_sensor_->publish_state(value); + } + if (this->pm_2_5_sensor_ != nullptr) { + uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); + this->pm_2_5_sensor_->publish_state(value); + } + if (this->pm_10_0_sensor_ != nullptr) { + uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + this->pm_10_0_sensor_->publish_state(value); + } + + this->status_clear_warning(); +} + +bool HM3301Component::read_sensor_value_(uint8_t *data) { return !hm3301_->read_sensor_value(data, 29); } + +bool HM3301Component::validate_checksum_(const uint8_t *data) { + uint8_t sum = 0; + for (int i = 0; i < 28; i++) { + sum += data[i]; + } + + return sum == data[28]; +} + +uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) { + return (uint16_t) data[i * 2] << 8 | data[i * 2 + 1]; +} + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h new file mode 100644 index 0000000000..0fbb32612e --- /dev/null +++ b/esphome/components/hm3301/hm3301.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace hm3301 { + +class HM3301Component : public PollingComponent, public i2c::I2CDevice { + public: + HM3301Component() = default; + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + HM330X *hm3301_; + + HM330XErrorCode error_code_{NO_ERROR}; + + uint8_t data_buffer_[30]; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + bool read_sensor_value_(uint8_t *); + bool validate_checksum_(const uint8_t *); + uint16_t get_sensor_value_(const uint8_t *, uint8_t); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py new file mode 100644 index 0000000000..718d0a20bb --- /dev/null +++ b/esphome/components/hm3301/sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, \ + UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON + +DEPENDENCIES = ['i2c'] + +hm3301_ns = cg.esphome_ns.namespace('hm3301') +HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(HM3301Component), + + cv.Optional(CONF_PM_1_0): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + cv.Optional(CONF_PM_2_5): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + cv.Optional(CONF_PM_10_0): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_PM_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + + # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 + cg.add_library('6306', '1.0.3') diff --git a/platformio.ini b/platformio.ini index 408e5af1ce..a6e9926de6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,6 +19,7 @@ lib_deps = ESPAsyncTCP-esphome@1.2.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) 6865@1.0.0 ; TM1651 Battery Display + 6306@1.0.3 ; HM3301 build_flags = -Wno-reorder -DUSE_WEB_SERVER diff --git a/tests/test1.yaml b/tests/test1.yaml index 1fc6b650c4..f87d77c9c2 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -720,6 +720,13 @@ sensor: - platform: tmp117 name: "TMP117 Temperature" update_interval: 5s + - platform: hm3301 + pm_1_0: + name: "PM1.0" + pm_2_5: + name: "PM2.5" + pm_10_0: + name: "PM10.0" esp32_touch: setup_mode: False diff --git a/tests/test3.yaml b/tests/test3.yaml index 9407cab687..e7c3da18de 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -353,6 +353,13 @@ sensor: name: "PZEMDC Current" power: name: "PZEMDC Power" + - platform: hm3301 + pm_1_0: + name: "PM1.0" + pm_2_5: + name: "PM2.5" + pm_10_0: + name: "PM10.0" time: - platform: homeassistant From 8613c02d5c3e40938e39def1671fb37c69c9db11 Mon Sep 17 00:00:00 2001 From: kroimon Date: Wed, 8 Apr 2020 14:31:23 +0200 Subject: [PATCH 181/412] Add constant_brightness property to CWWW/RGBWW lights (#1007) Fixes https://github.com/esphome/feature-requests/issues/460 Co-authored-by: Otto Winter --- esphome/components/cwww/cwww_light_output.h | 4 ++- esphome/components/cwww/light.py | 4 +++ esphome/components/light/light_color_values.h | 25 +++++++++++++------ esphome/components/light/light_state.cpp | 10 +++++--- esphome/components/light/light_state.h | 5 ++-- esphome/components/rgbww/light.py | 4 +++ esphome/components/rgbww/rgbww_light_output.h | 4 ++- tests/test1.yaml | 1 + 8 files changed, 41 insertions(+), 16 deletions(-) diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 8192039511..3351a98d24 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -13,6 +13,7 @@ class CWWWLightOutput : public light::LightOutput { void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -25,7 +26,7 @@ class CWWWLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float cwhite, wwhite; - state->current_values_as_cwww(&cwhite, &wwhite); + state->current_values_as_cwww(&cwhite, &wwhite, this->constant_brightness_); this->cold_white_->set_level(cwhite); this->warm_white_->set_level(wwhite); } @@ -35,6 +36,7 @@ class CWWWLightOutput : public light::LightOutput { output::FloatOutput *warm_white_; float cold_white_temperature_; float warm_white_temperature_; + bool constant_brightness_; }; } // namespace cwww diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index f86f1eace0..5cc4262105 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -7,12 +7,15 @@ from esphome.const import CONF_OUTPUT_ID, CONF_COLD_WHITE, CONF_WARM_WHITE, \ cwww_ns = cg.esphome_ns.namespace('cwww') CWWWLightOutput = cwww_ns.class_('CWWWLightOutput', light.LightOutput) +CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' + CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, }) @@ -26,3 +29,4 @@ def to_code(config): wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 9ac8be1dd0..39a93cbbcd 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -190,24 +190,33 @@ class LightColorValues { /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white) const { + float *cold_white, float *warm_white, bool constant_brightness = false) const { this->as_rgb(red, green, blue); const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white = this->state_ * this->brightness_ * this->white_ * (cw_fraction / max_cw_ww); - *warm_white = this->state_ * this->brightness_ * this->white_ * (ww_fraction / max_cw_ww); + *cold_white = this->state_ * this->brightness_ * this->white_ * cw_fraction; + *warm_white = this->state_ * this->brightness_ * this->white_ * ww_fraction; + if (!constant_brightness) { + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + *cold_white /= max_cw_ww; + *warm_white /= max_cw_ww; + } } /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white) const { + void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, + bool constant_brightness = false) const { const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white = this->state_ * this->brightness_ * (cw_fraction / max_cw_ww); - *warm_white = this->state_ * this->brightness_ * (ww_fraction / max_cw_ww); + *cold_white = this->state_ * this->brightness_ * cw_fraction; + *warm_white = this->state_ * this->brightness_ * ww_fraction; + if (!constant_brightness) { + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + *cold_white /= max_cw_ww; + *warm_white /= max_cw_ww; + } } /// Compare this LightColorValues to rhs, return true if and only if all attributes match. diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 5e9166795e..c6e7df0811 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -718,19 +718,21 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f *blue = gamma_correct(*blue, this->gamma_correct_); *white = gamma_correct(*white, this->gamma_correct_); } -void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white) { +void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, + bool constant_brightness) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white); + warm_white, constant_brightness); *red = gamma_correct(*red, this->gamma_correct_); *green = gamma_correct(*green, this->gamma_correct_); *blue = gamma_correct(*blue, this->gamma_correct_); *cold_white = gamma_correct(*cold_white, this->gamma_correct_); *warm_white = gamma_correct(*warm_white, this->gamma_correct_); } -void LightState::current_values_as_cwww(float *cold_white, float *warm_white) { +void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); - this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white); + this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, + constant_brightness); *cold_white = gamma_correct(*cold_white, this->gamma_correct_); *warm_white = gamma_correct(*warm_white, this->gamma_correct_); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 07a0e3147b..f399cc2be4 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -270,9 +270,10 @@ class LightState : public Nameable, public Component { void current_values_as_rgbw(float *red, float *green, float *blue, float *white); - void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white); + void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, + bool constant_brightness = false); - void current_values_as_cwww(float *cold_white, float *warm_white); + void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); protected: friend LightOutput; diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index c804eed942..78f4bee630 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -8,6 +8,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_ rgbww_ns = cg.esphome_ns.namespace('rgbww') RGBWWLightOutput = rgbww_ns.class_('RGBWWLightOutput', light.LightOutput) +CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' + CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), cv.Required(CONF_RED): cv.use_id(output.FloatOutput), @@ -17,6 +19,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, }) @@ -38,3 +41,4 @@ def to_code(config): wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index ef9e99a3eb..a975331a37 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -16,6 +16,7 @@ class RGBWWLightOutput : public light::LightOutput { void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -28,7 +29,7 @@ class RGBWWLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -44,6 +45,7 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *warm_white_; float cold_white_temperature_; float warm_white_temperature_; + bool constant_brightness_; }; } // namespace rgbww diff --git a/tests/test1.yaml b/tests/test1.yaml index f87d77c9c2..8483281b59 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1074,6 +1074,7 @@ light: warm_white: pca_6 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds + constant_brightness: true - platform: fastled_clockless id: addr1 chipset: WS2811 From 17fd9d5107d8ddbf51abcf8288c3088126cd9cdd Mon Sep 17 00:00:00 2001 From: Andrew Zaborowski Date: Thu, 9 Apr 2020 16:12:42 +0200 Subject: [PATCH 182/412] web_server: Add cover calls to REST API (#999) Add the GET and POST handler for cover components. Also add covers to the index page although the Open/Close buttons that are shown for covers will need a few lines added to webserver-v1.js, without them they don't do anything. --- esphome/components/web_server/web_server.cpp | 85 ++++++++++++++++++++ esphome/components/web_server/web_server.h | 10 +++ 2 files changed, 95 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index c0708b763f..1f6cd10666 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -108,6 +108,12 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->text_sensor_json(obj, obj->state).c_str(), "state"); #endif + +#ifdef USE_COVER + for (auto *obj : App.get_covers()) + if (!obj->is_internal()) + client->send(this->cover_json(obj).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -180,6 +186,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "text_sensor", ""); #endif +#ifdef USE_COVER + for (auto *obj : App.get_covers()) + write_row(stream, obj, "cover", ""); +#endif + stream->print(F("
NameStateActions

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

is_internal()) + return; + this->events_.send(this->cover_json(obj).c_str(), "state"); +} +void WebServer::handle_cover_request(AsyncWebServerRequest *request, UrlMatch match) { + for (cover::Cover *obj : App.get_covers()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->cover_json(obj); + request->send(200, "text/json", data.c_str()); + continue; + } + + auto call = obj->make_call(); + if (match.method == "open") { + call.set_command_open(); + } else if (match.method == "close") { + call.set_command_close(); + } else if (match.method == "stop") { + call.set_command_stop(); + } else if (match.method != "set") { + request->send(404); + return; + } + + auto traits = obj->get_traits(); + if ((request->hasParam("position") && !traits.get_supports_position()) || + (request->hasParam("tilt") && !traits.get_supports_tilt())) { + request->send(409); + return; + } + + if (request->hasParam("position")) + call.set_position(request->getParam("position")->value().toFloat()); + if (request->hasParam("tilt")) + call.set_tilt(request->getParam("tilt")->value().toFloat()); + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::cover_json(cover::Cover *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "cover-" + obj->get_object_id(); + root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN"; + root["value"] = obj->position; + root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + + if (obj->get_traits().get_supports_tilt()) + root["tilt"] = obj->tilt; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -542,6 +615,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_COVER + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -610,6 +688,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_COVER + if (match.domain == "cover") { + this->handle_cover_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index def1cac0ea..b3bf2ef7f7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -144,6 +144,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value); #endif +#ifdef USE_COVER + void on_cover_update(cover::Cover *obj) override; + + /// Handle a cover request under '/cover//'. + void handle_cover_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the cover state as a JSON string. + std::string cover_json(cover::Cover *obj); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. From 835079ad43593258cd38988209f303ccb4efac7c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 10 Apr 2020 05:07:18 +0200 Subject: [PATCH 183/412] Add AC Dimmer support (#880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add AC Dimmer support Fixes https://github.com/esphome/feature-requests/issues/278 * fixes basically missed the output pin setup and in the switching was switching true true true :P * Format * Enable ESP32 * Also setup ZC pin * Support multiple dimmers sharing ZC pin * Fix ESP32 * Lint * off gate on zc detect * tests pins validation * Climate Mitsubishi (#725) * add climate * Mitsubishi updates * refactor mitsubishi to use climate_ir * lint * fix: only decode when not str already (#923) Signed-off-by: wilmardo * fix climate-ir bad merge (#935) * fix climate-ir bad merge * add mitshubishi test * http_request: fix memory allocation (#916) * http_request version fix (#917) * PID Climate (#885) * PID Climate * Add sensor for debugging PID output value * Add dump_config, use percent * Add more observable values * Update * Set target temperature * Add autotuner * Add algorithm explanation * Add autotuner action, update controller * Add simulator * Format * Change defaults * Updates * Use b''.decode() instead of str(b'') (#941) Handling of request arguments in WizardRequestHandler is not decoding bytes and rather just doing a str conversion resulting in a value of "b''" being supplied to the wizard code. * Adding the espressif 2.6.3 (#944) * extract and use current version of python 3 (#938) * Inverted output in neopixelbus (#895) * Added inverted output * Added support for inverted output in neopixelbus * Update esphome/components/neopixelbus/light.py Co-Authored-By: Otto Winter * Update light.py * corrected lint errors Co-authored-by: Otto Winter * Added degree symbol for MAX7219 7-segment display. (#764) The ascii char to use it is "~" (0x7E). Disclaimer: I didn't test this yet. * Fix dump/tx of 64 bit codes (#940) * Fix dump/tx of 64 bit codes * fixed source format * Update hdc1080.cpp (#887) * Update hdc1080.cpp increase waittime, to fix reading errors * Fix: Update HDC1080.cpp i fixed the my change on write_bytes * add tcl112 support for dry, fan and swing (#939) * Fix SGP30 incorrect baseline reading/writing (#936) * Split the SGP30 baseline into 2 values - According to the SGP30 datasheet, each eCO2 and TVOC baseline is a 2-byte value (MSB first) - The current implementation ignores the MSB of each of the value - Update the schema to allow 2 different baseline values (optional, but both need to be specified for the baseline to apply) * Make both eCO2 and TVOC required if the optional baseline is defined * Make dump_config() looks better * Add register_*_effect to allow registering custom effects (#947) This allows to register custom effect from user components, allowing for bigger composability of source. * Bugfix/normalize core comparisons (and Python 3 update fixes) (#952) * Correct implementation of comparisons to be Pythonic If a comparison cannot be made return NotImplemented, this allows the Python interpreter to try other comparisons (eg __ieq__) and either return False (in the case of __eq__) or raise a TypeError exception (eg in the case of __lt__). * Python 3 updates * Add a more helpful message in exception if platform is not defined * Added a basic pre-commit check * Add transmit pioneer (#922) * Added pioneer_protocol to support transmit_pioneer * Display tm1637 (#946) * add TM1637 support * Support a further variant of Xiaomi CGG1 (#930) * Daikin climate ir component (#964) * Daikin ARC43XXX IR remote controller support * Format and lint fixes * Check temperature values against allowed min/max * fix tm1637 missing __init__.py (#975) * Add AC Dimmer support Fixes https://github.com/esphome/feature-requests/issues/278 * fixes basically missed the output pin setup and in the switching was switching true true true :P * Format * Enable ESP32 * Also setup ZC pin * Support multiple dimmers sharing ZC pin * Fix ESP32 * Lint * off gate on zc detect * tests pins validation * fix esp8266 many dimmers, changed timing * Increased value resolution, added min power * use min_power from base class * fix min_power. add init with half cycle * added method for trailing pulse, trailing and leading * fix method name. try filter invalid falling pulse * renamed to ac_dimmer * fix ESP32 not configuring zero cross twice Co-authored-by: Guillermo Ruffino Co-authored-by: Wilmar den Ouden Co-authored-by: Nikolay Vasilchuk Co-authored-by: Tim Savage Co-authored-by: Vc <37367415+Valcob@users.noreply.github.com> Co-authored-by: gitolicious Co-authored-by: voibit Co-authored-by: Luar Roji Co-authored-by: András Bíró <1202136+andrasbiro@users.noreply.github.com> Co-authored-by: dmkif Co-authored-by: Panuruj Khambanonda (PK) Co-authored-by: Kamil Trzciński Co-authored-by: Keith Burzinski Co-authored-by: Mario <4376789+mario-tux@users.noreply.github.com> Co-authored-by: Héctor Giménez --- esphome/components/ac_dimmer/__init__.py | 0 esphome/components/ac_dimmer/ac_dimmer.cpp | 217 +++++++++++++++++++++ esphome/components/ac_dimmer/ac_dimmer.h | 66 +++++++ esphome/components/ac_dimmer/output.py | 43 ++++ tests/test1.yaml | 4 + tests/test3.yaml | 4 + 6 files changed, 334 insertions(+) create mode 100644 esphome/components/ac_dimmer/__init__.py create mode 100644 esphome/components/ac_dimmer/ac_dimmer.cpp create mode 100644 esphome/components/ac_dimmer/ac_dimmer.h create mode 100644 esphome/components/ac_dimmer/output.py diff --git a/esphome/components/ac_dimmer/__init__.py b/esphome/components/ac_dimmer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp new file mode 100644 index 0000000000..a60cc9e29a --- /dev/null +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -0,0 +1,217 @@ +#include "ac_dimmer.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +namespace esphome { +namespace ac_dimmer { + +static const char *TAG = "ac_dimmer"; + +// Global array to store dimmer objects +static AcDimmerDataStore *all_dimmers[32]; + +/// Time in microseconds the gate should be held high +/// 10µs should be long enough for most triacs +/// For reference: BT136 datasheet says 2µs nominal (page 7) +static uint32_t GATE_ENABLE_TIME = 10; + +/// Function called from timer interrupt +/// Input is current time in microseconds (micros()) +/// Returns when next "event" is expected in µs, or 0 if no such event known. +uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { + // If no ZC signal received yet. + if (this->crossed_zero_at == 0) + return 0; + + uint32_t time_since_zc = now - this->crossed_zero_at; + if (this->value == 65535 || this->value == 0) { + return 0; + } + + if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) { + this->enable_time_us = 0; + this->gate_pin->digital_write(true); + // Prevent too short pulses + this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); + } + if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) { + this->disable_time_us = 0; + this->gate_pin->digital_write(false); + } + + if (time_since_zc < this->enable_time_us) + // Next event is enable, return time until that event + return this->enable_time_us - time_since_zc; + else if (time_since_zc < disable_time_us) { + // Next event is disable, return time until that event + return this->disable_time_us - time_since_zc; + } + + if (time_since_zc >= this->cycle_time_us) { + // Already past last cycle time, schedule next call shortly + return 100; + } + + return this->cycle_time_us - time_since_zc; +} + +/// Run timer interrupt code and return in how many µs the next event is expected +uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { + // run at least with 1kHz + uint32_t min_dt_us = 1000; + uint32_t now = micros(); + for (auto *dimmer : all_dimmers) { + if (dimmer == nullptr) + // no more dimmers + break; + uint32_t res = dimmer->timer_intr(now); + if (res != 0 && res < min_dt_us) + min_dt_us = res; + } + // return time until next timer1 interrupt in µs + return min_dt_us; +} + +/// GPIO interrupt routine, called when ZC pin triggers +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { + uint32_t prev_crossed = this->crossed_zero_at; + + // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms + // in any case the cycle last at least 5ms + this->crossed_zero_at = micros(); + uint32_t cycle_time = this->crossed_zero_at - prev_crossed; + if (cycle_time > 5000) { + this->cycle_time_us = cycle_time; + } else { + // Otherwise this is noise and this is 2nd (or 3rd...) fall in the same pulse + // Consider this is the right fall edge and accumulate the cycle time instead + this->cycle_time_us += cycle_time; + } + + if (this->value == 65535) { + // fully on, enable output immediately + this->gate_pin->digital_write(true); + } else if (this->init_cycle) { + // send a full cycle + this->init_cycle = false; + this->enable_time_us = 0; + this->disable_time_us = cycle_time_us; + } else if (this->value == 0) { + // fully off, disable output immediately + this->gate_pin->digital_write(false); + } else { + if (this->method == DIM_METHOD_TRAILING) { + this->enable_time_us = 1; // cannot be 0 + this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535); + } else { + // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic + // also take into account min_power + auto min_us = this->cycle_time_us * this->min_power / 1000; + this->enable_time_us = max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); + if (this->method == DIM_METHOD_LEADING_PULSE) { + // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone + // this is for brightness near 99% + this->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); + } else { + this->gate_pin->digital_write(false); + this->disable_time_us = this->cycle_time_us; + } + } + } +} + +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { + // Attaching pin interrupts on the same pin will override the previous interupt + // However, the user expects that multiple dimmers sharing the same ZC pin will work. + // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers + // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. + for (auto *dimmer : all_dimmers) { + if (dimmer == nullptr) + break; + if (dimmer->zero_cross_pin_number == store->zero_cross_pin_number) { + dimmer->gpio_intr(); + } + } +} + +#ifdef ARDUINO_ARCH_ESP32 +// ESP32 implementation, uses basically the same code but needs to wrap +// timer_interrupt() function to auto-reschedule +static hw_timer_t *dimmer_timer = nullptr; +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } +#endif + +void AcDimmer::setup() { + // extend all_dimmers array with our dimmer + + // Need to be sure the zero cross pin is setup only once, ESP8266 fails and ESP32 seems to fail silently + auto setup_zero_cross_pin = true; + + for (auto &all_dimmer : all_dimmers) { + if (all_dimmer == nullptr) { + all_dimmer = &this->store_; + break; + } + if (all_dimmer->zero_cross_pin_number == this->zero_cross_pin_->get_pin()) { + setup_zero_cross_pin = false; + } + } + + this->gate_pin_->setup(); + this->store_.gate_pin = this->gate_pin_->to_isr(); + this->store_.zero_cross_pin_number = this->zero_cross_pin_->get_pin(); + this->store_.min_power = static_cast(this->min_power_ * 1000); + this->min_power_ = 0; + this->store_.method = this->method_; + + if (setup_zero_cross_pin) { + this->zero_cross_pin_->setup(); + this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr(); + this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING); + } + +#ifdef ARDUINO_ARCH_ESP8266 + // Uses ESP8266 waveform (soft PWM) class + // PWM and AcDimmer can even run at the same time this way + setTimer1Callback(&timer_interrupt); +#endif +#ifdef ARDUINO_ARCH_ESP32 + // 80 Divider -> 1 count=1µs + dimmer_timer = timerBegin(0, 80, true); + timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); + // For ESP32, we can't use dynamic interval calculation because the timerX functions + // are not callable from ISR (placed in flash storage). + // Here we just use an interrupt firing every 50 µs. + timerAlarmWrite(dimmer_timer, 50, true); + timerAlarmEnable(dimmer_timer); +#endif +} +void AcDimmer::write_state(float state) { + auto new_value = static_cast(roundf(state * 65535)); + if (new_value != 0 && this->store_.value == 0) + this->store_.init_cycle = this->init_with_half_cycle_; + this->store_.value = new_value; +} +void AcDimmer::dump_config() { + ESP_LOGCONFIG(TAG, "AcDimmer:"); + LOG_PIN(" Output Pin: ", this->gate_pin_); + LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); + ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->store_.min_power / 10.0f); + ESP_LOGCONFIG(TAG, " Init with half cycle: %s", YESNO(this->init_with_half_cycle_)); + if (method_ == DIM_METHOD_LEADING_PULSE) + ESP_LOGCONFIG(TAG, " Method: leading pulse"); + else if (method_ == DIM_METHOD_LEADING) + ESP_LOGCONFIG(TAG, " Method: leading"); + else + ESP_LOGCONFIG(TAG, " Method: trailing"); + + LOG_FLOAT_OUTPUT(this); + ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); +} + +} // namespace ac_dimmer +} // namespace esphome diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h new file mode 100644 index 0000000000..00da061cfd --- /dev/null +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -0,0 +1,66 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace ac_dimmer { + +enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING }; + +struct AcDimmerDataStore { + /// Zero-cross pin + ISRInternalGPIOPin *zero_cross_pin; + /// Zero-cross pin number - used to share ZC pin across multiple dimmers + uint8_t zero_cross_pin_number; + /// Output pin to write to + ISRInternalGPIOPin *gate_pin; + /// Value of the dimmer - 0 to 65535. + uint16_t value; + /// Minimum power for activation + uint16_t min_power; + /// Time between the last two ZC pulses + uint32_t cycle_time_us; + /// Time (in micros()) of last ZC signal + uint32_t crossed_zero_at; + /// Time since last ZC pulse to enable gate pin. 0 means not set. + uint32_t enable_time_us; + /// Time since last ZC pulse to disable gate pin. 0 means no disable. + uint32_t disable_time_us; + /// Set to send the first half ac cycle complete + bool init_cycle; + /// Dimmer method + DimMethod method; + + uint32_t timer_intr(uint32_t now); + + void gpio_intr(); + static void s_gpio_intr(AcDimmerDataStore *store); +#ifdef ARDUINO_ARCH_ESP32 + static void s_timer_intr(); +#endif +}; + +class AcDimmer : public output::FloatOutput, public Component { + public: + void setup() override; + + void dump_config() override; + void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; } + void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } + void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; } + void set_method(DimMethod method) { method_ = method; } + + protected: + void write_state(float state) override; + + GPIOPin *gate_pin_; + GPIOPin *zero_cross_pin_; + AcDimmerDataStore store_; + bool init_with_half_cycle_; + DimMethod method_; +}; + +} // namespace ac_dimmer +} // namespace esphome diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py new file mode 100644 index 0000000000..16f04ac984 --- /dev/null +++ b/esphome/components/ac_dimmer/output.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import output +from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD + +ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer') +AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component) + +DimMethod = ac_dimmer_ns.enum('DimMethod') +DIM_METHODS = { + 'LEADING_PULSE': DimMethod.DIM_METHOD_LEADING_PULSE, + 'LEADING': DimMethod.DIM_METHOD_LEADING, + 'TRAILING': DimMethod.DIM_METHOD_TRAILING, +} + +CONF_GATE_PIN = 'gate_pin' +CONF_ZERO_CROSS_PIN = 'zero_cross_pin' +CONF_INIT_WITH_HALF_CYCLE = 'init_with_half_cycle' +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(AcDimmer), + cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, + cv.Optional(CONF_METHOD, default='leading pulse'): cv.enum(DIM_METHODS, upper=True, space='_'), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + # override default min power to 10% + if CONF_MIN_POWER not in config: + config[CONF_MIN_POWER] = 0.1 + yield output.register_output(var, config) + + pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN]) + cg.add(var.set_gate_pin(pin)) + pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN]) + cg.add(var.set_zero_cross_pin(pin)) + cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE])) + cg.add(var.set_method(config[CONF_METHOD])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 8483281b59..16fc382b2f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1012,6 +1012,10 @@ output: id: id24 pin: GPIO26 period: 15s + - platform: ac_dimmer + id: dimmer1 + gate_pin: GPIO5 + zero_cross_pin: GPIO26 light: - platform: binary diff --git a/tests/test3.yaml b/tests/test3.yaml index e7c3da18de..2a37095aca 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -649,6 +649,10 @@ output: return {s}; outputs: - id: custom_float + - platform: ac_dimmer + id: dimmer1 + gate_pin: GPIO5 + zero_cross_pin: GPIO12 mcp23017: id: mcp23017_hub From c1dfed5c08dcd00f967d1fe53befefbdff1c0b1f Mon Sep 17 00:00:00 2001 From: Alex Reid Date: Sun, 12 Apr 2020 15:07:10 -0400 Subject: [PATCH 184/412] feat: Add support for MCP23016 IO Expander (#1012) * feat: Add support for MCP23016 IO extander * fix: Fix style --- esphome/components/mcp23016/__init__.py | 50 +++++++++++++ esphome/components/mcp23016/mcp23016.cpp | 91 ++++++++++++++++++++++++ esphome/components/mcp23016/mcp23016.h | 71 ++++++++++++++++++ tests/test1.yaml | 18 +++++ 4 files changed, 230 insertions(+) create mode 100644 esphome/components/mcp23016/__init__.py create mode 100644 esphome/components/mcp23016/mcp23016.cpp create mode 100644 esphome/components/mcp23016/mcp23016.h diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py new file mode 100644 index 0000000000..93c3d3843c --- /dev/null +++ b/esphome/components/mcp23016/__init__.py @@ -0,0 +1,50 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +mcp23016_ns = cg.esphome_ns.namespace('mcp23016') +MCP23016GPIOMode = mcp23016_ns.enum('MCP23016GPIOMode') +MCP23016_GPIO_MODES = { + 'INPUT': MCP23016GPIOMode.MCP23016_INPUT, + 'OUTPUT': MCP23016GPIOMode.MCP23016_OUTPUT, +} + +MCP23016 = mcp23016_ns.class_('MCP23016', cg.Component, i2c.I2CDevice) +MCP23016GPIOPin = mcp23016_ns.class_('MCP23016GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(MCP23016), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + +CONF_MCP23016 = 'mcp23016' +MCP23016_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23016): cv.use_id(MCP23016), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23016_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +MCP23016_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23016): cv.use_id(MCP23016), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23016_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23016, + (MCP23016_OUTPUT_PIN_SCHEMA, MCP23016_INPUT_PIN_SCHEMA)) +def mcp23016_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23016]) + yield MCP23016GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp new file mode 100644 index 0000000000..bd04486965 --- /dev/null +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -0,0 +1,91 @@ +#include "mcp23016.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23016 { + +static const char *TAG = "mcp23016"; + +void MCP23016::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23016..."); + uint8_t iocon; + if (!this->read_reg_(MCP23016_IOCON0, &iocon)) { + this->mark_failed(); + return; + } + + // all pins input + this->write_reg_(MCP23016_IODIR0, 0xFF); + this->write_reg_(MCP23016_IODIR1, 0xFF); +} +bool MCP23016::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return value & (1 << bit); +} +void MCP23016::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; + this->update_reg_(pin, value, reg_addr); +} +void MCP23016::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1; + switch (mode) { + case MCP23016_INPUT: + this->update_reg_(pin, true, iodir); + break; + case MCP23016_OUTPUT: + this->update_reg_(pin, false, iodir); + break; + default: + break; + } +} +float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } +bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} +bool MCP23016::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} +void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23016_OLAT0) { + reg_value = this->olat_0_; + } else if (reg_addr == MCP23016_OLAT1) { + reg_value = this->olat_1_; + } else { + this->read_reg_(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg_(reg_addr, reg_value); + + if (reg_addr == MCP23016_OLAT0) { + this->olat_0_ = reg_value; + } else if (reg_addr == MCP23016_OLAT1) { + this->olat_1_ = reg_value; + } +} + +MCP23016GPIOPin::MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23016GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23016GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23016 +} // namespace esphome diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h new file mode 100644 index 0000000000..53502f80eb --- /dev/null +++ b/esphome/components/mcp23016/mcp23016.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp23016 { + +/// Modes for MCP23016 pins +enum MCP23016GPIOMode : uint8_t { + MCP23016_INPUT = INPUT, // 0x00 + MCP23016_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23016GPIORegisters { + // 0 side + MCP23016_GP0 = 0x00, + MCP23016_OLAT0 = 0x02, + MCP23016_IPOL0 = 0x04, + MCP23016_IODIR0 = 0x06, + MCP23016_INTCAP0 = 0x08, + MCP23016_IOCON0 = 0x0A, + // 1 side + MCP23016_GP1 = 0x01, + MCP23016_OLAT1 = 0x03, + MCP23016_IPOL1 = 0x04, + MCP23016_IODIR1 = 0x07, + MCP23016_INTCAP1 = 0x08, + MCP23016_IOCON1 = 0x0B, +}; + +class MCP23016 : public Component, public i2c::I2CDevice { + public: + MCP23016() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + float get_setup_priority() const override; + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); + + uint8_t olat_0_{0x00}; + uint8_t olat_1_{0x00}; +}; + +class MCP23016GPIOPin : public GPIOPin { + public: + MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23016 *parent_; +}; + +} // namespace mcp23016 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 16fc382b2f..8019148e07 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -867,6 +867,13 @@ binary_sensor: number: 7 mode: INPUT_PULLUP inverted: False + - platform: gpio + name: "MCP23 binary sensor" + pin: + mcp23016: mcp23016_hub + number: 7 + mode: INPUT + inverted: False - platform: remote_receiver name: "Raw Remote Receiver Test" @@ -990,6 +997,13 @@ output: number: 0 mode: OUTPUT inverted: False + - platform: gpio + id: id25 + pin: + mcp23016: mcp23016_hub + number: 0 + mode: OUTPUT + inverted: False - platform: my9231 id: my_0 channel: 0 @@ -1569,6 +1583,10 @@ mcp23008: - id: 'mcp23008_hub' address: 0x22 +mcp23016: + - id: 'mcp23016_hub' + address: 0x23 + stepper: - platform: a4988 id: my_stepper From 65f4d30fd006ced61b1a1852c3fbaa958b655dfd Mon Sep 17 00:00:00 2001 From: puuu Date: Fri, 17 Apr 2020 06:57:58 +0900 Subject: [PATCH 185/412] Daikin climate receiver support (#1001) * climate.daikin: implement remote receive * climate.daikin: fix temperature value in special modes * climate.daikin: tweak timing to fit better to ir-remote signal --- esphome/components/daikin/climate.py | 2 +- esphome/components/daikin/daikin.cpp | 101 ++++++++++++++++++++++++++- esphome/components/daikin/daikin.h | 10 ++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py index f1d5c7ed4a..ff3f506fb2 100644 --- a/esphome/components/daikin/climate.py +++ b/esphome/components/daikin/climate.py @@ -8,7 +8,7 @@ AUTO_LOAD = ['climate_ir'] daikin_ns = cg.esphome_ns.namespace('daikin') DaikinClimate = daikin_ns.class_('DaikinClimate', climate_ir.ClimateIR) -CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(DaikinClimate), }) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index eabbb96014..b6e80d62a7 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -115,7 +115,8 @@ uint8_t DaikinClimate::temperature_() { // Force special temperatures depending on the mode switch (this->mode) { case climate::CLIMATE_MODE_FAN_ONLY: - return 25; + return 0x32; + case climate::CLIMATE_MODE_AUTO: case climate::CLIMATE_MODE_DRY: return 0xc0; default: @@ -124,5 +125,103 @@ uint8_t DaikinClimate::temperature_() { } } +bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) + return false; + uint8_t mode = frame[5]; + if (mode & DAIKIN_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + case DAIKIN_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[6]; + if (!(temperature & 0xC0)) { + this->target_temperature = temperature >> 1; + } + uint8_t fan_mode = frame[8]; + if (fan_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + else + this->swing_mode = climate::CLIMATE_SWING_OFF; + switch (fan_mode & 0xF0) { + case DAIKIN_FAN_1: + case DAIKIN_FAN_2: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_FAN_4: + case DAIKIN_FAN_5: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DAIKIN_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) + byte |= 1 << bit; + else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) + return false; + } else if (pos == 1) { + // frame header + if (byte != 0xDA) + return false; + } else if (pos == 2) { + // frame header + if (byte != 0x27) + return false; + } else if (pos == 3) { + // frame header + if (byte != 0x00) + return false; + } else if (pos == 4) { + // frame type + if (byte != 0x00) + return false; + } + } + return this->parse_state_frame_(state_frame); +} + } // namespace daikin } // namespace esphome diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index ea69256701..4671d57570 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -31,11 +31,14 @@ const uint8_t DAIKIN_FAN_5 = 0x70; const uint32_t DAIKIN_IR_FREQUENCY = 38000; const uint32_t DAIKIN_HEADER_MARK = 3360; const uint32_t DAIKIN_HEADER_SPACE = 1760; -const uint32_t DAIKIN_BIT_MARK = 360; +const uint32_t DAIKIN_BIT_MARK = 520; const uint32_t DAIKIN_ONE_SPACE = 1370; -const uint32_t DAIKIN_ZERO_SPACE = 520; +const uint32_t DAIKIN_ZERO_SPACE = 360; const uint32_t DAIKIN_MESSAGE_SPACE = 32300; +// State Frame size +const uint8_t DAIKIN_STATE_FRAME_SIZE = 19; + class DaikinClimate : public climate_ir::ClimateIR { public: DaikinClimate() @@ -51,6 +54,9 @@ class DaikinClimate : public climate_ir::ClimateIR { uint8_t operation_mode_(); uint8_t fan_speed_(); uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); }; } // namespace daikin From d44754889378e1359bd2b09bf78f095b90b92091 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Mon, 20 Apr 2020 10:05:58 +1000 Subject: [PATCH 187/412] Tests for CPP Code generation and some Python3 improvements (#961) * Basic pytest configuration * Added unit_test script that triggers pytest * Changed "fixtures" to fixture_path This is consistent with pytest's tmp_path * Initial unit tests for esphome.helpers * Disabled coverage reporting for esphome/components. Focus initial unittest efforts on the core code. * Migrated some ip_address to hypothesis * Added a hypothesis MAC address strategy * Initial tests for core * Added hypothesis to requirements * Added tests for core classes TestTimePeriod Lambda ID DocumentLocation DocumentRange Define Library * Updated test config so package root is discovered * Setup fixtures and inital tests for pins * Added tests for validate GPIO * Added tests for pin type * Added initial config_validation tests * Added more tests for config_validation * Added comparison unit tests * Added repr to core.TimePeriod. Simplified identifying faults in tests * Fixed inverted gt/lt tests * Some tests for Espcore * Updated syntax for Python3 * Removed usage of kwarg that isn't required * Started writing test cases * Started writing test cases for cpp_generator * Additional docs and more Python3 releated improvements * More test cases for cpp_generator. * Fixed linter errors * Add codegen tests to ensure file API remains stable * Add test cases for cpp_helpers --- esphome/components/globals/__init__.py | 2 +- esphome/components/neopixelbus/light.py | 2 +- .../components/waveshare_epaper/display.py | 4 +- esphome/cpp_generator.py | 329 +++++++++--------- requirements_test.txt | 1 + tests/unit_tests/test_codegen.py | 26 ++ tests/unit_tests/test_core.py | 2 +- tests/unit_tests/test_cpp_generator.py | 293 ++++++++++++++++ tests/unit_tests/test_cpp_helpers.py | 85 +++++ 9 files changed, 574 insertions(+), 170 deletions(-) create mode 100644 tests/unit_tests/test_codegen.py create mode 100644 tests/unit_tests/test_cpp_generator.py create mode 100644 tests/unit_tests/test_cpp_helpers.py diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index d285f1e97f..e59a7e6acb 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -30,7 +30,7 @@ def to_code(config): initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE]) rhs = GlobalsComponent.new(template_args, initial_value) - glob = cg.Pvariable(config[CONF_ID], rhs, type=res_type) + glob = cg.Pvariable(config[CONF_ID], rhs, res_type) yield cg.register_component(glob, config) if config[CONF_RESTORE_VALUE]: diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index fb83e4740d..2b84882e59 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -169,7 +169,7 @@ def to_code(config): else: out_type = NeoPixelRGBLightOutput.template(template) rhs = out_type.new() - var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, type=out_type) + var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type) yield light.register_light(var, config) yield cg.register_component(var, config) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 77322cbb70..bbbd8c0d3a 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -57,10 +57,10 @@ def to_code(config): model_type, model = MODELS[config[CONF_MODEL]] if model_type == 'a': rhs = WaveshareEPaperTypeA.new(model) - var = cg.Pvariable(config[CONF_ID], rhs, type=WaveshareEPaperTypeA) + var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA) elif model_type == 'b': rhs = model.new() - var = cg.Pvariable(config[CONF_ID], rhs, type=model) + var = cg.Pvariable(config[CONF_ID], rhs, model) else: raise NotImplementedError() diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b5239e9413..e9bcdc7d1f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,9 +1,9 @@ +import abc import inspect - import math # pylint: disable=unused-import, wrong-import-order -from typing import Any, Generator, List, Optional, Tuple, Type, Union, Dict, Callable # noqa +from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence from esphome.core import ( # noqa CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds, @@ -13,29 +13,35 @@ from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.util import OrderedDict -class Expression: +class Expression(abc.ABC): + __slots__ = () + + @abc.abstractmethod def __str__(self): - raise NotImplementedError + """ + Convert expression into C++ code + """ SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod, - Type[bool], Type[int], Type[float], List[Any]] + Type[bool], Type[int], Type[float], Sequence[Any]] class RawExpression(Expression): - def __init__(self, text): # type: (Union[str, str]) -> None - super().__init__() + __slots__ = ("text", ) + + def __init__(self, text: str): self.text = text def __str__(self): - return str(self.text) + return self.text -# pylint: disable=redefined-builtin class AssignmentExpression(Expression): - def __init__(self, type, modifier, name, rhs, obj): - super().__init__() - self.type = type + __slots__ = ("type", "modifier", "name", "rhs", "obj") + + def __init__(self, type_, modifier, name, rhs, obj): + self.type = type_ self.modifier = modifier self.name = name self.rhs = safe_exp(rhs) @@ -48,9 +54,10 @@ class AssignmentExpression(Expression): class VariableDeclarationExpression(Expression): - def __init__(self, type, modifier, name): - super().__init__() - self.type = type + __slots__ = ("type", "modifier", "name") + + def __init__(self, type_, modifier, name): + self.type = type_ self.modifier = modifier self.name = name @@ -59,8 +66,9 @@ class VariableDeclarationExpression(Expression): class ExpressionList(Expression): - def __init__(self, *args): - super().__init__() + __slots__ = ("args", ) + + def __init__(self, *args: Optional[SafeExpType]): # Remove every None on end args = list(args) while args and args[-1] is None: @@ -76,8 +84,9 @@ class ExpressionList(Expression): class TemplateArguments(Expression): - def __init__(self, *args): # type: (*SafeExpType) -> None - super().__init__() + __slots__ = ("args", ) + + def __init__(self, *args: SafeExpType): self.args = ExpressionList(*args) def __str__(self): @@ -88,8 +97,9 @@ class TemplateArguments(Expression): class CallExpression(Expression): - def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None - super().__init__() + __slots__ = ("base", "template_args", "args") + + def __init__(self, base: Expression, *args: SafeExpType): self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] @@ -105,9 +115,11 @@ class CallExpression(Expression): class StructInitializer(Expression): - def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None - super().__init__() + __slots__ = ("base", "args") + + def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]): self.base = base + # TODO: args is always a Tuple, is this check required? if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() @@ -126,9 +138,10 @@ class StructInitializer(Expression): class ArrayInitializer(Expression): - def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - super().__init__() - self.multiline = kwargs.get('multiline', False) + __slots__ = ("multiline", "args") + + def __init__(self, *args: Any, multiline: bool = False): + self.multiline = multiline self.args = [] for arg in args: if arg is None: @@ -150,18 +163,20 @@ class ArrayInitializer(Expression): class ParameterExpression(Expression): - def __init__(self, type, id): - super().__init__() - self.type = safe_exp(type) - self.id = id + __slots__ = ("type", "id") + + def __init__(self, type_, id_): + self.type = safe_exp(type_) + self.id = id_ def __str__(self): return f"{self.type} {self.id}" class ParameterListExpression(Expression): - def __init__(self, *parameters): - super().__init__() + __slots__ = ("parameters", ) + + def __init__(self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]]): self.parameters = [] for parameter in parameters: if not isinstance(parameter, ParameterExpression): @@ -173,8 +188,9 @@ class ParameterListExpression(Expression): class LambdaExpression(Expression): - def __init__(self, parts, parameters, capture='=', return_type=None): - super().__init__() + __slots__ = ("parts", "parameters", "capture", "return_type") + + def __init__(self, parts, parameters, capture: str = '=', return_type=None): self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -194,23 +210,25 @@ class LambdaExpression(Expression): return ''.join(str(part) for part in self.parts) -class Literal(Expression): - def __str__(self): - raise NotImplementedError +# pylint: disable=abstract-method +class Literal(Expression, metaclass=abc.ABCMeta): + __slots__ = () class StringLiteral(Literal): - def __init__(self, string): # type: (Union[str, str]) -> None - super().__init__() + __slots__ = ("string", ) + + def __init__(self, string: str): self.string = string def __str__(self): - return '{}'.format(cpp_string_escape(self.string)) + return cpp_string_escape(self.string) class IntLiteral(Literal): - def __init__(self, i): # type: (Union[int]) -> None - super().__init__() + __slots__ = ("i", ) + + def __init__(self, i: int): self.i = i def __str__(self): @@ -224,7 +242,9 @@ class IntLiteral(Literal): class BoolLiteral(Literal): - def __init__(self, binary): # type: (bool) -> None + __slots__ = ("binary", ) + + def __init__(self, binary: bool): super().__init__() self.binary = binary @@ -233,8 +253,9 @@ class BoolLiteral(Literal): class HexIntLiteral(Literal): - def __init__(self, i): # type: (int) -> None - super().__init__() + __slots__ = ("i", ) + + def __init__(self, i: int): self.i = HexInt(i) def __str__(self): @@ -242,21 +263,18 @@ class HexIntLiteral(Literal): class FloatLiteral(Literal): - def __init__(self, value): # type: (float) -> None - super().__init__() - self.float_ = value + __slots__ = ("f", ) + + def __init__(self, value: float): + self.f = value def __str__(self): - if math.isnan(self.float_): + if math.isnan(self.f): return "NAN" - return f"{self.float_}f" + return f"{self.f}f" -# pylint: disable=bad-continuation -def safe_exp( - obj # type: Union[Expression, bool, str, int, float, TimePeriod, list] - ): - # type: (...) -> Expression +def safe_exp(obj: SafeExpType) -> Expression: """Try to convert obj to an expression by automatically converting native python types to expressions/literals. """ @@ -301,17 +319,20 @@ def safe_exp( raise ValueError("Object is not an expression", obj) -class Statement: - def __init__(self): - pass +class Statement(abc.ABC): + __slots__ = () + @abc.abstractmethod def __str__(self): - raise NotImplementedError + """ + Convert statement into C++ code + """ class RawStatement(Statement): - def __init__(self, text): - super().__init__() + __slots__ = ("text", ) + + def __init__(self, text: str): self.text = text def __str__(self): @@ -319,8 +340,9 @@ class RawStatement(Statement): class ExpressionStatement(Statement): + __slots__ = ("expression", ) + def __init__(self, expression): - super().__init__() self.expression = safe_exp(expression) def __str__(self): @@ -328,115 +350,105 @@ class ExpressionStatement(Statement): class LineComment(Statement): - def __init__(self, value): # type: (str) -> None - super().__init__() - self._value = value + __slots__ = ("value", ) + + def __init__(self, value: str): + self.value = value def __str__(self): - parts = self._value.split('\n') + parts = self.value.split('\n') parts = [f'// {x}' for x in parts] return '\n'.join(parts) class ProgmemAssignmentExpression(AssignmentExpression): - def __init__(self, type, name, rhs, obj): - super().__init__( - type, '', name, rhs, obj - ) + __slots__ = () + + def __init__(self, type_, name, rhs, obj): + super().__init__(type_, '', name, rhs, obj) def __str__(self): - type_ = self.type - return f"static const {type_} {self.name}[] PROGMEM = {self.rhs}" + return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" -def progmem_array(id, rhs): +def progmem_array(id_, rhs) -> "MockObj": rhs = safe_exp(rhs) - obj = MockObj(id, '.') - assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) + obj = MockObj(id_, '.') + assignment = ProgmemAssignmentExpression(id_.type, id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def statement(expression): # type: (Union[Expression, Statement]) -> Statement +def statement(expression: Union[Expression, Statement]) -> Statement: + """Convert expression into a statement unless is already a statement. + """ if isinstance(expression, Statement): return expression return ExpressionStatement(expression) -def variable(id, # type: ID - rhs, # type: SafeExpType - type=None # type: MockObj - ): - # type: (...) -> MockObj +def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new variable (not pointer type) in the code generation. - :param id: The ID used to declare the variable. + :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. - :param type: Manually define a type for the variable, only use this when it's not possible + :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). :returns The new variable as a MockObj. """ - assert isinstance(id, ID) + assert isinstance(id_, ID) rhs = safe_exp(rhs) - obj = MockObj(id, '.') - if type is not None: - id.type = type - assignment = AssignmentExpression(id.type, '', id, rhs, obj) + obj = MockObj(id_, '.') + if type_ is not None: + id_.type = type_ + assignment = AssignmentExpression(id_.type, '', id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def Pvariable(id, # type: ID - rhs, # type: SafeExpType - type=None # type: MockObj - ): - # type: (...) -> MockObj +def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new pointer variable in the code generation. - :param id: The ID used to declare the variable. + :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. - :param type: Manually define a type for the variable, only use this when it's not possible + :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). :returns The new variable as a MockObj. """ rhs = safe_exp(rhs) - obj = MockObj(id, '->') - if type is not None: - id.type = type - decl = VariableDeclarationExpression(id.type, '*', id) + obj = MockObj(id_, '->') + if type_ is not None: + id_.type = type_ + decl = VariableDeclarationExpression(id_.type, '*', id_) CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id, rhs, obj) + assignment = AssignmentExpression(None, None, id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def new_Pvariable(id, # type: ID - *args # type: *SafeExpType - ): +def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: """Declare a new pointer variable in the code generation by calling it's constructor with the given arguments. - :param id: The ID used to declare the variable (also specifies the type). + :param id_: The ID used to declare the variable (also specifies the type). :param args: The values to pass to the constructor. :returns The new variable as a MockObj. """ if args and isinstance(args[0], TemplateArguments): - id = id.copy() - id.type = id.type.template(args[0]) + id_ = id_.copy() + id_.type = id_.type.template(args[0]) args = args[1:] - rhs = id.type.new(*args) - return Pvariable(id, rhs) + rhs = id_.type.new(*args) + return Pvariable(id_, rhs) -def add(expression, # type: Union[Expression, Statement] - ): - # type: (...) -> None +def add(expression: Union[Expression, Statement]): """Add an expression to the codegen section. After this is called, the given given expression will @@ -445,17 +457,12 @@ def add(expression, # type: Union[Expression, Statement] CORE.add(expression) -def add_global(expression, # type: Union[SafeExpType, Statement] - ): - # type: (...) -> None +def add_global(expression: Union[SafeExpType, Statement]): """Add an expression to the codegen global storage (above setup()).""" CORE.add_global(expression) -def add_library(name, # type: str - version # type: Optional[str] - ): - # type: (...) -> None +def add_library(name: str, version: Optional[str]): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') @@ -464,17 +471,12 @@ def add_library(name, # type: str CORE.add_library(Library(name, version)) -def add_build_flag(build_flag, # type: str - ): - # type: (...) -> None +def add_build_flag(build_flag: str): """Add a global build flag to the compiler flags.""" CORE.add_build_flag(build_flag) -def add_define(name, # type: str - value=None, # type: Optional[SafeExpType] - ): - # type: (...) -> None +def add_define(name: str, value: SafeExpType = None): """Add a global define to the auto-generated defines.h file. Optionally define a value to set this define to. @@ -486,42 +488,40 @@ def add_define(name, # type: str @coroutine -def get_variable(id): # type: (ID) -> Generator[MockObj] +def get_variable(id_: ID) -> Generator["MockObj", None, None]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. This is a coroutine, you need to await it with a 'yield' expression! - :param id: The ID to retrieve + :param id_: The ID to retrieve :return: The variable as a MockObj. """ - var = yield CORE.get_variable(id) + var = yield CORE.get_variable(id_) yield var @coroutine -def get_variable_with_full_id(id): # type: (ID) -> Generator[ID, MockObj] +def get_variable_with_full_id(id_: ID) -> Generator[Tuple[ID, "MockObj"], None, None]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. This is a coroutine, you need to await it with a 'yield' expression! - :param id: The ID to retrieve + :param id_: The ID to retrieve :return: The variable as a MockObj. """ - full_id, var = yield CORE.get_variable_with_full_id(id) + full_id, var = yield CORE.get_variable_with_full_id(id_) yield full_id, var @coroutine -def process_lambda(value, # type: Lambda - parameters, # type: List[Tuple[SafeExpType, str]] - capture='=', # type: str - return_type=None # type: Optional[SafeExpType] - ): - # type: (...) -> Generator[LambdaExpression] +def process_lambda( + value: Lambda, parameters: List[Tuple[SafeExpType, str]], + capture: str = '=', return_type: SafeExpType = None +) -> Generator[LambdaExpression, None, None]: """Process the given lambda value into a LambdaExpression. This is a coroutine because lambdas can depend on other IDs, @@ -560,11 +560,10 @@ def is_template(value): @coroutine -def templatable(value, # type: Any - args, # type: List[Tuple[SafeExpType, str]] - output_type, # type: Optional[SafeExpType], - to_exp=None # type: Optional[Any] - ): +def templatable(value: Any, + args: List[Tuple[SafeExpType, str]], + output_type: Optional[SafeExpType], + to_exp: Any = None): """Generate code for a templatable config option. If `value` is a templated value, the lambda expression is returned. @@ -593,12 +592,13 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ + __slots__ = ("base", "op") + def __init__(self, base, op='.'): self.base = base self.op = op - super().__init__() - def __getattr__(self, attr): # type: (str) -> MockObj + def __getattr__(self, attr: str) -> "MockObj": next_op = '.' if attr.startswith('P') and self.op not in ['::', '']: attr = attr[1:] @@ -611,55 +611,55 @@ class MockObj(Expression): call = CallExpression(self.base, *args) return MockObj(call, self.op) - def __str__(self): # type: () -> str + def __str__(self): return str(self.base) def __repr__(self): return 'MockObj<{}>'.format(str(self.base)) @property - def _(self): # type: () -> MockObj + def _(self) -> "MockObj": return MockObj(f'{self.base}{self.op}') @property - def new(self): # type: () -> MockObj + def new(self) -> "MockObj": return MockObj(f'new {self.base}', '->') - def template(self, *args): # type: (*SafeExpType) -> MockObj + def template(self, *args: SafeExpType) -> "MockObj": if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] return MockObj(f'{self.base}{args}') - def namespace(self, name): # type: (str) -> MockObj + def namespace(self, name: str) -> "MockObj": return MockObj(f'{self._}{name}', '::') - def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass + def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass": op = '' if self.op == '' else '::' return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents) - def struct(self, name): # type: (str) -> MockObjClass + def struct(self, name: str) -> "MockObjClass": return self.class_(name) - def enum(self, name, is_class=False): # type: (str, bool) -> MockObj + def enum(self, name: str, is_class: bool = False) -> "MockObj": return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) - def operator(self, name): # type: (str) -> MockObj + def operator(self, name: str) -> "MockObj": if name == 'ref': return MockObj(f'{self.base} &', '') if name == 'ptr': return MockObj(f'{self.base} *', '') if name == "const": return MockObj(f'const {self.base}', '') - raise NotImplementedError + raise ValueError("Expected one of ref, ptr, const.") @property - def using(self): # type: () -> MockObj + def using(self) -> "MockObj": assert self.op == '::' return MockObj(f'using namespace {self.base}') - def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj + def __getitem__(self, item: Union[str, Expression]) -> "MockObj": next_op = '.' if isinstance(item, str) and item.startswith('P'): item = item[1:] @@ -678,13 +678,13 @@ class MockObjEnum(MockObj): kwargs['base'] = base MockObj.__init__(self, *args, **kwargs) - def __str__(self): # type: () -> str + def __str__(self): if self._is_class: return super().__str__() return f'{self.base}{self.op}{self._enum}' def __repr__(self): - return 'MockObj<{}>'.format(str(self.base)) + return f'MockObj<{str(self.base)}>' class MockObjClass(MockObj): @@ -699,7 +699,7 @@ class MockObjClass(MockObj): # pylint: disable=protected-access self._parents += paren._parents - def inherits_from(self, other): # type: (MockObjClass) -> bool + def inherits_from(self, other: "MockObjClass") -> bool: if self == other: return True for parent in self._parents: @@ -707,8 +707,7 @@ class MockObjClass(MockObj): return True return False - def template(self, *args): - # type: (*SafeExpType) -> MockObjClass + def template(self, *args: SafeExpType) -> "MockObjClass": if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: @@ -718,4 +717,4 @@ class MockObjClass(MockObj): return MockObjClass(f'{self.base}{args}', parents=new_parents) def __repr__(self): - return 'MockObjClass<{}, parents={}>'.format(str(self.base), self._parents) + return f'MockObjClass<{str(self.base)}, parents={self._parents}>' diff --git a/requirements_test.txt b/requirements_test.txt index 85f7d511a8..268f78fcdf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -21,4 +21,5 @@ pexpect pytest==5.3.2 pytest-cov==2.8.1 pytest-mock==1.13.0 +asyncmock==0.4.2 hypothesis==4.57.0 diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py new file mode 100644 index 0000000000..931e191de6 --- /dev/null +++ b/tests/unit_tests/test_codegen.py @@ -0,0 +1,26 @@ +import pytest + +from esphome import codegen as cg + + +# Test interface remains the same. +@pytest.mark.parametrize("attr", ( + # from cpp_generator + "Expression", "RawExpression", "RawStatement", "TemplateArguments", + "StructInitializer", "ArrayInitializer", "safe_exp", "Statement", "LineComment", + "progmem_array", "statement", "variable", "Pvariable", "new_Pvariable", + "add", "add_global", "add_library", "add_build_flag", "add_define", + "get_variable", "get_variable_with_full_id", "process_lambda", "is_template", "templatable", "MockObj", + "MockObjClass", + # from cpp_helpers + "gpio_pin_expression", "register_component", "build_registry_entry", + "build_registry_list", "extract_registry_entry_config", "register_parented", + "global_ns", "void", "nullptr", "float_", "double", "bool_", "int_", "std_ns", "std_string", + "std_vector", "uint8", "uint16", "uint32", "int32", "const_char_ptr", "NAN", + "esphome_ns", "App", "Nameable", "Component", "ComponentPtr", + # from cpp_types + "PollingComponent", "Application", "optional", "arduino_json_ns", "JsonObject", + "JsonObjectRef", "JsonObjectConstRef", "Controller", "GPIOPin" +)) +def test_exists(attr): + assert hasattr(cg, attr) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 14f6990bd9..cd0b0947f3 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -459,13 +459,13 @@ class TestEsphomeCore: target.config_path = "foo/config" return target + @pytest.mark.xfail(reason="raw_config and config differ, should they?") def test_reset(self, target): """Call reset on target and compare to new instance""" other = core.EsphomeCore() target.reset() - # TODO: raw_config and config differ, should they? assert target.__dict__ == other.__dict__ def test_address__none(self, target): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py new file mode 100644 index 0000000000..b130124b54 --- /dev/null +++ b/tests/unit_tests/test_cpp_generator.py @@ -0,0 +1,293 @@ +from typing import Iterator + +import math + +import pytest + +from esphome import cpp_generator as cg +from esphome import cpp_types as ct + + +class TestExpressions: + @pytest.mark.parametrize("target, expected", ( + (cg.RawExpression("foo && bar"), "foo && bar"), + + (cg.AssignmentExpression(None, None, "foo", "bar", None), 'foo = "bar"'), + (cg.AssignmentExpression(ct.float_, "*", "foo", 1, None), 'float *foo = 1'), + (cg.AssignmentExpression(ct.float_, "", "foo", 1, None), 'float foo = 1'), + + (cg.VariableDeclarationExpression(ct.int32, "*", "foo"), "int32_t *foo"), + (cg.VariableDeclarationExpression(ct.int32, "", "foo"), "int32_t foo"), + + (cg.ParameterExpression(ct.std_string, "foo"), "std::string foo"), + )) + def test_str__simple(self, target: cg.Expression, expected: str): + actual = str(target) + + assert actual == expected + + +class TestExpressionList: + SAMPLE_ARGS = (1, "2", True, None, None) + + def test_str(self): + target = cg.ExpressionList(*self.SAMPLE_ARGS) + + actual = str(target) + + assert actual == '1, "2", true' + + def test_iter(self): + target = cg.ExpressionList(*self.SAMPLE_ARGS) + + actual = iter(target) + + assert isinstance(actual, Iterator) + assert len(tuple(actual)) == 3 + + +class TestTemplateArguments: + SAMPLE_ARGS = (int, 1, "2", True, None, None) + + def test_str(self): + target = cg.TemplateArguments(*self.SAMPLE_ARGS) + + actual = str(target) + + assert actual == '' + + def test_iter(self): + target = cg.TemplateArguments(*self.SAMPLE_ARGS) + + actual = iter(target) + + assert isinstance(actual, Iterator) + assert len(tuple(actual)) == 4 + + +class TestCallExpression: + def test_str__no_template_args(self): + target = cg.CallExpression( + cg.RawExpression("my_function"), + 1, "2", False + ) + + actual = str(target) + + assert actual == 'my_function(1, "2", false)' + + def test_str__with_template_args(self): + target = cg.CallExpression( + cg.RawExpression("my_function"), + cg.TemplateArguments(int, float), + 1, "2", False + ) + + actual = str(target) + + assert actual == 'my_function(1, "2", false)' + + +class TestStructInitializer: + def test_str(self): + target = cg.StructInitializer( + cg.MockObjClass("foo::MyStruct", parents=()), + ("state", "on"), + ("min_length", 1), + ("max_length", 5), + ("foo", None), + ) + + actual = str(target) + + assert actual == 'foo::MyStruct{\n' \ + ' .state = "on",\n' \ + ' .min_length = 1,\n' \ + ' .max_length = 5,\n' \ + '}' + + +class TestArrayInitializer: + def test_str__empty(self): + target = cg.ArrayInitializer( + None, None + ) + + actual = str(target) + + assert actual == "{}" + + def test_str__not_multiline(self): + target = cg.ArrayInitializer( + 1, 2, 3, 4 + ) + + actual = str(target) + + assert actual == "{1, 2, 3, 4}" + + def test_str__multiline(self): + target = cg.ArrayInitializer( + 1, 2, 3, 4, multiline=True + ) + + actual = str(target) + + assert actual == "{\n 1,\n 2,\n 3,\n 4,\n}" + + +class TestParameterListExpression: + def test_str(self): + target = cg.ParameterListExpression( + cg.ParameterExpression(int, "foo"), + (float, "bar"), + ) + + actual = str(target) + + assert actual == "int32_t foo, float bar" + + +class TestLambdaExpression: + def test_str__no_return(self): + target = cg.LambdaExpression( + ( + "if ((foo == 5) && (bar < 10))) {\n", + "}", + ), + ((int, "foo"), (float, "bar")), + ) + + actual = str(target) + + assert actual == ( + "[=](int32_t foo, float bar) {\n" + " if ((foo == 5) && (bar < 10))) {\n" + " }\n" + "}" + ) + + def test_str__with_return(self): + target = cg.LambdaExpression( + ("return (foo == 5) && (bar < 10));", ), + cg.ParameterListExpression((int, "foo"), (float, "bar")), + "=", + bool, + ) + + actual = str(target) + + assert actual == ( + "[=](int32_t foo, float bar) -> bool {\n" + " return (foo == 5) && (bar < 10));\n" + "}" + ) + + +class TestLiterals: + @pytest.mark.parametrize("target, expected", ( + (cg.StringLiteral("foo"), '"foo"'), + + (cg.IntLiteral(0), "0"), + (cg.IntLiteral(42), "42"), + (cg.IntLiteral(4304967295), "4304967295ULL"), + (cg.IntLiteral(2150483647), "2150483647UL"), + (cg.IntLiteral(-2150083647), "-2150083647LL"), + + (cg.BoolLiteral(True), "true"), + (cg.BoolLiteral(False), "false"), + + (cg.HexIntLiteral(0), "0x00"), + (cg.HexIntLiteral(42), "0x2A"), + (cg.HexIntLiteral(682), "0x2AA"), + + (cg.FloatLiteral(0.0), "0.0f"), + (cg.FloatLiteral(4.2), "4.2f"), + (cg.FloatLiteral(1.23456789), "1.23456789f"), + (cg.FloatLiteral(math.nan), "NAN"), + )) + def test_str__simple(self, target: cg.Literal, expected: str): + actual = str(target) + + assert actual == expected + + +FAKE_ENUM_VALUE = cg.EnumValue() +FAKE_ENUM_VALUE.enum_value = "foo" + + +@pytest.mark.parametrize("obj, expected_type", ( + (cg.RawExpression("foo"), cg.RawExpression), + (FAKE_ENUM_VALUE, cg.StringLiteral), + (True, cg.BoolLiteral), + ("foo", cg.StringLiteral), + (cg.HexInt(42), cg.HexIntLiteral), + (42, cg.IntLiteral), + (42.1, cg.FloatLiteral), + (cg.TimePeriodMicroseconds(microseconds=42), cg.IntLiteral), + (cg.TimePeriodMilliseconds(milliseconds=42), cg.IntLiteral), + (cg.TimePeriodSeconds(seconds=42), cg.IntLiteral), + (cg.TimePeriodMinutes(minutes=42), cg.IntLiteral), + ((1, 2, 3), cg.ArrayInitializer), + ([1, 2, 3], cg.ArrayInitializer), +)) +def test_safe_exp__allowed_values(obj, expected_type): + actual = cg.safe_exp(obj) + + assert isinstance(actual, expected_type) + + +@pytest.mark.parametrize("obj, expected_type", ( + (bool, ct.bool_), + (int, ct.int32), + (float, ct.float_), +)) +def test_safe_exp__allowed_types(obj, expected_type): + actual = cg.safe_exp(obj) + + assert actual is expected_type + + +@pytest.mark.parametrize("obj, expected_error", ( + (cg.ID("foo"), "Object foo is an ID."), + ((x for x in "foo"), r"Object <.*> is a coroutine."), + (None, "Object is not an expression"), +)) +def test_safe_exp__invalid_values(obj, expected_error): + with pytest.raises(ValueError, match=expected_error): + cg.safe_exp(obj) + + +class TestStatements: + @pytest.mark.parametrize("target, expected", ( + (cg.RawStatement("foo && bar"), "foo && bar"), + + (cg.ExpressionStatement("foo"), '"foo";'), + (cg.ExpressionStatement(42), '42;'), + + (cg.LineComment("The point of foo is..."), "// The point of foo is..."), + (cg.LineComment("Help help\nI'm being repressed"), "// Help help\n// I'm being repressed"), + + ( + cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar", None), + 'static const uint16_t foo[] PROGMEM = "bar"' + ) + )) + def test_str__simple(self, target: cg.Statement, expected: str): + actual = str(target) + + assert actual == expected + + +# TODO: This method has side effects in CORE +# def test_progmem_array(): +# pass + + +class TestMockObj: + def test_getattr(self): + target = cg.MockObj("foo") + actual = target.eek + assert isinstance(actual, cg.MockObj) + assert actual.base == "foo.eek" + assert actual.op == "." diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py new file mode 100644 index 0000000000..d8f32e7a51 --- /dev/null +++ b/tests/unit_tests/test_cpp_helpers.py @@ -0,0 +1,85 @@ +import pytest +from mock import Mock + +from esphome import cpp_helpers as ch +from esphome import const +from esphome.cpp_generator import MockObj + + +def test_gpio_pin_expression__conf_is_none(monkeypatch): + target = ch.gpio_pin_expression(None) + + actual = next(target) + + assert actual is None + + +def test_gpio_pin_expression__new_pin(monkeypatch): + target = ch.gpio_pin_expression({ + const.CONF_NUMBER: 42, + const.CONF_MODE: "input", + const.CONF_INVERTED: False + }) + + actual = next(target) + + assert isinstance(actual, MockObj) + + +def test_register_component(monkeypatch): + var = Mock(base="foo.bar") + + app_mock = Mock(register_component=Mock(return_value=var)) + monkeypatch.setattr(ch, "App", app_mock) + + core_mock = Mock(component_ids=["foo.bar"]) + monkeypatch.setattr(ch, "CORE", core_mock) + + add_mock = Mock() + monkeypatch.setattr(ch, "add", add_mock) + + target = ch.register_component(var, {}) + + actual = next(target) + + assert actual is var + add_mock.assert_called_once() + app_mock.register_component.assert_called_with(var) + assert core_mock.component_ids == [] + + +def test_register_component__no_component_id(monkeypatch): + var = Mock(base="foo.eek") + + core_mock = Mock(component_ids=["foo.bar"]) + monkeypatch.setattr(ch, "CORE", core_mock) + + with pytest.raises(ValueError, match="Component ID foo.eek was not declared to"): + target = ch.register_component(var, {}) + next(target) + + +def test_register_component__with_setup_priority(monkeypatch): + var = Mock(base="foo.bar") + + app_mock = Mock(register_component=Mock(return_value=var)) + monkeypatch.setattr(ch, "App", app_mock) + + core_mock = Mock(component_ids=["foo.bar"]) + monkeypatch.setattr(ch, "CORE", core_mock) + + add_mock = Mock() + monkeypatch.setattr(ch, "add", add_mock) + + target = ch.register_component(var, { + const.CONF_SETUP_PRIORITY: "123", + const.CONF_UPDATE_INTERVAL: "456", + }) + + actual = next(target) + + assert actual is var + add_mock.assert_called() + assert add_mock.call_count == 3 + app_mock.register_component.assert_called_with(var) + assert core_mock.component_ids == [] From 8040e3cf95d097bd76110bb071f11fac7d7c96d3 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 21 Apr 2020 22:53:14 -0300 Subject: [PATCH 188/412] Climate whirlpool (#1029) * wip * transmitter ready * climate.whirlpool receiver implemented (#971) * receiver implemented * Support for two models of temp ranges * temperature type and lint * more lint Co-authored-by: Guillermo Ruffino * add test * not mess line endings Co-authored-by: mmanza <40872469+mmanza@users.noreply.github.com> --- esphome/components/whirlpool/__init__.py | 0 esphome/components/whirlpool/climate.py | 26 ++ esphome/components/whirlpool/whirlpool.cpp | 289 +++++++++++++++++++++ esphome/components/whirlpool/whirlpool.h | 63 +++++ tests/test1.yaml | 2 + 5 files changed, 380 insertions(+) create mode 100644 esphome/components/whirlpool/__init__.py create mode 100644 esphome/components/whirlpool/climate.py create mode 100644 esphome/components/whirlpool/whirlpool.cpp create mode 100644 esphome/components/whirlpool/whirlpool.h diff --git a/esphome/components/whirlpool/__init__.py b/esphome/components/whirlpool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py new file mode 100644 index 0000000000..1083b86618 --- /dev/null +++ b/esphome/components/whirlpool/climate.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID, CONF_MODEL + +AUTO_LOAD = ['climate_ir'] + +whirlpool_ns = cg.esphome_ns.namespace('whirlpool') +WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR) + +Model = whirlpool_ns.enum('Model') +MODELS = { + 'DG11J1-3A': Model.MODEL_DG11J1_3A, + 'DG11J1-91': Model.MODEL_DG11J1_91, +} + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(WhirlpoolClimate), + cv.Optional(CONF_MODEL, default='DG11J1-3A'): cv.enum(MODELS, upper=True) +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp new file mode 100644 index 0000000000..0956f816ce --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -0,0 +1,289 @@ +#include "whirlpool.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace whirlpool { + +static const char *TAG = "whirlpool.climate"; + +const uint16_t WHIRLPOOL_HEADER_MARK = 9000; +const uint16_t WHIRLPOOL_HEADER_SPACE = 4494; +const uint16_t WHIRLPOOL_BIT_MARK = 572; +const uint16_t WHIRLPOOL_ONE_SPACE = 1659; +const uint16_t WHIRLPOOL_ZERO_SPACE = 553; +const uint32_t WHIRLPOOL_GAP = 7960; + +const uint32_t WHIRLPOOL_CARRIER_FREQUENCY = 38000; + +const uint8_t WHIRLPOOL_STATE_LENGTH = 21; + +const uint8_t WHIRLPOOL_HEAT = 0; +const uint8_t WHIRLPOOL_DRY = 3; +const uint8_t WHIRLPOOL_COOL = 2; +const uint8_t WHIRLPOOL_FAN = 4; +const uint8_t WHIRLPOOL_AUTO = 1; + +const uint8_t WHIRLPOOL_FAN_AUTO = 0; +const uint8_t WHIRLPOOL_FAN_HIGH = 1; +const uint8_t WHIRLPOOL_FAN_MED = 2; +const uint8_t WHIRLPOOL_FAN_LOW = 3; + +const uint8_t WHIRLPOOL_SWING_MASK = 128; + +const uint8_t WHIRLPOOL_POWER = 0x04; + +void WhirlpoolClimate::transmit_state() { + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + remote_state[0] = 0x83; + remote_state[1] = 0x06; + remote_state[6] = 0x80; + // MODEL DG11J191 + remote_state[18] = 0x08; + + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + if (powered_on != this->powered_on_assumed_) { + // Set power toggle command + remote_state[2] = 4; + remote_state[15] = 1; + this->powered_on_assumed_ = powered_on; + } + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + // set fan auto + // set temp auto temp + // set sleep false + remote_state[3] = WHIRLPOOL_AUTO; + remote_state[15] = 0x17; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[3] = WHIRLPOOL_HEAT; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[3] = WHIRLPOOL_COOL; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[3] = WHIRLPOOL_DRY; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[3] = WHIRLPOOL_FAN; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_OFF: + default: + break; + } + + // Temperature + auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_())); + remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4; + + // Fan speed + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state[2] |= WHIRLPOOL_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[2] |= WHIRLPOOL_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[2] |= WHIRLPOOL_FAN_LOW; + break; + default: + break; + } + + // Swing + ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? "true" : "false"); + if (this->send_swing_cmd_) { + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) { + remote_state[2] |= 128; + remote_state[8] |= 64; + } + } + + // Checksum + for (uint8_t i = 2; i < 12; i++) + remote_state[13] ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + remote_state[20] ^= remote_state[i]; + + ESP_LOGV(TAG, + "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X " + "%02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // Send code + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(WHIRLPOOL_HEADER_MARK); + data->space(WHIRLPOOL_HEADER_SPACE); + // Data + auto bytes_sent = 0; + for (uint8_t i : remote_state) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(WHIRLPOOL_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? WHIRLPOOL_ONE_SPACE : WHIRLPOOL_ZERO_SPACE); + } + bytes_sent++; + if (bytes_sent == 6 || bytes_sent == 14) { + // Divider + data->mark(WHIRLPOOL_BIT_MARK); + data->space(WHIRLPOOL_GAP); + } + } + // Footer + data->mark(WHIRLPOOL_BIT_MARK); + + transmit.perform(); +} + +bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) { + // Read bit + if (i == 6 || i == 14) { + if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP)) + return false; + } + for (int j = 0; j < 8; j++) { + if (data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ONE_SPACE)) + remote_state[i] |= 1 << j; + + else if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + + ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]); + } + // Validate footer + if (!data.expect_mark(WHIRLPOOL_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum13 = 0; + uint8_t checksum20 = 0; + // Calculate checksum and compare with signal value. + for (uint8_t i = 2; i < 12; i++) + checksum13 ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + checksum20 ^= remote_state[i]; + + if (checksum13 != remote_state[13] || checksum20 != remote_state[20]) { + ESP_LOGVV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV( + TAG, + "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X " + "%02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // verify header remote code + if (remote_state[0] != 0x83 || remote_state[1] != 0x06) + return false; + + // powr on/off button + ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER)); + + if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) { + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + + if (powered_on) { + this->mode = climate::CLIMATE_MODE_OFF; + this->powered_on_assumed_ = false; + } else { + this->powered_on_assumed_ = true; + } + } + + // Set received mode + if (powered_on_assumed_) { + auto mode = remote_state[3] & 0x7; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case WHIRLPOOL_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case WHIRLPOOL_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case WHIRLPOOL_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case WHIRLPOOL_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case WHIRLPOOL_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + + // Set received temp + int temp = remote_state[3] & 0xF0; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + temp = (uint8_t) temp >> 4; + temp += static_cast(this->temperature_min_()); + ESP_LOGVV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = remote_state[2] & 0x03; + ESP_LOGVV(TAG, "Fan: %02X", fan); + switch (fan) { + case WHIRLPOOL_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case WHIRLPOOL_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case WHIRLPOOL_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case WHIRLPOOL_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + if ((remote_state[2] & WHIRLPOOL_SWING_MASK) == WHIRLPOOL_SWING_MASK && remote_state[8] == 0x40) { + ESP_LOGVV(TAG, "Swing toggle pressed "); + if (this->swing_mode == climate::CLIMATE_SWING_OFF) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + } + + this->publish_state(); + return true; +} + +} // namespace whirlpool +} // namespace esphome diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h new file mode 100644 index 0000000000..44116b340c --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace whirlpool { + +/// Simple enum to represent models. +enum Model { + MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32 + MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30 +}; + +// Temperature +const float WHIRLPOOL_DG11J1_3A_TEMP_MAX = 32.0; +const float WHIRLPOOL_DG11J1_3A_TEMP_MIN = 18.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MAX = 30.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; + +class WhirlpoolClimate : public climate_ir::ClimateIR { + public: + WhirlpoolClimate() + : climate_ir::ClimateIR(temperature_min_(), temperature_max_(), 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + void setup() override { + climate_ir::ClimateIR::setup(); + + this->powered_on_assumed_ = this->mode != climate::CLIMATE_MODE_OFF; + } + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + climate_ir::ClimateIR::control(call); + } + + void set_model(Model model) { this->model_ = model; } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + // used to track when to send the power toggle command + bool powered_on_assumed_; + + bool send_swing_cmd_{false}; + Model model_; + + float temperature_min_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MIN : WHIRLPOOL_DG11J1_91_TEMP_MIN; + } + float temperature_max_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MAX : WHIRLPOOL_DG11J1_91_TEMP_MAX; + } +}; + +} // namespace whirlpool +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 8019148e07..dcc6700b45 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1228,6 +1228,8 @@ climate: name: Yashima Climate - platform: mitsubishi name: Mitsubishi + - platform: whirlpool + name: Whirlpool Climate switch: - platform: gpio From 6123cb7c69464ff4af4f5aa3cd9020b4590820ee Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 24 Apr 2020 20:58:32 -0300 Subject: [PATCH 189/412] add mac address to wifi info (#1030) * add mac address to wifi info * add test * lint --- esphome/components/wifi_info/text_sensor.py | 7 ++++++- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 1 + esphome/components/wifi_info/wifi_info_text_sensor.h | 7 +++++++ tests/test1.yaml | 2 ++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 81ee787848..56670b4173 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID +from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID, CONF_MAC_ADDRESS from esphome.core import coroutine DEPENDENCIES = ['wifi'] @@ -10,6 +10,7 @@ wifi_info_ns = cg.esphome_ns.namespace('wifi_info') IPAddressWiFiInfo = wifi_info_ns.class_('IPAddressWiFiInfo', text_sensor.TextSensor, cg.Component) SSIDWiFiInfo = wifi_info_ns.class_('SSIDWiFiInfo', text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_('BSSIDWiFiInfo', text_sensor.TextSensor, cg.Component) +MacAddressWifiInfo = wifi_info_ns.class_('MacAddressWifiInfo', text_sensor.TextSensor, cg.Component) CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({ @@ -21,6 +22,9 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo), }), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MacAddressWifiInfo), + }) }) @@ -37,3 +41,4 @@ def to_code(config): yield setup_conf(config, CONF_IP_ADDRESS) yield setup_conf(config, CONF_SSID) yield setup_conf(config, CONF_BSSID) + yield setup_conf(config, CONF_MAC_ADDRESS) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 704d9b3099..08a69998fb 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -9,6 +9,7 @@ static const char *TAG = "wifi_info"; void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } } // namespace wifi_info } // namespace esphome diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 9dfa684b4b..6d2be08fa0 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -60,5 +60,12 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { wifi::bssid_t last_bssid_; }; +class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { + public: + void setup() override { this->publish_state(get_mac_address_pretty()); } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-macadr"; } + void dump_config() override; +}; + } // namespace wifi_info } // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index dcc6700b45..dbb3a4bdb3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1644,3 +1644,5 @@ text_sensor: name: "SSID" bssid: name: "BSSID" + mac_address: + name: "Mac Address" From c9e224e9998918c0c0ad240225c8be56985e00ba Mon Sep 17 00:00:00 2001 From: Derek Hageman Date: Fri, 24 Apr 2020 19:10:41 -0600 Subject: [PATCH 190/412] SHTC3: Wake up the sensor during setup (#993) Since we put the sensor to sleep when not measuring, make sure to wake it up during setup. It does not respond to any other I2C commands (including reset) while asleep, so if we've done a soft reboot while it was sleeping then setup() would fail since it wouldn't respond to anything. --- esphome/components/shtcx/shtcx.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index b8daceb1af..d67031febf 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -25,6 +25,7 @@ inline const char *to_string(SHTCXType type) { void SHTCXComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SHTCx..."); + this->wake_up(); this->soft_reset(); if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { From bab0ba9c0fcb16a941e97ac33ed74019d7f94b2c Mon Sep 17 00:00:00 2001 From: ukewea <60734042+ukewea@users.noreply.github.com> Date: Sat, 25 Apr 2020 22:00:52 +0800 Subject: [PATCH 191/412] Change buffer sending process for waveshare_epaper (2.70in) (#1031) * Change buffer sending process for waveshare_epaper (2.70in) The current way ESPhome sending buffer to WaveshareEPaper2P7In does not show the expected content on the display, this commit is changing the data transferring process so the content is showing as expected. The process is adapted from the demo code provided by Waveshare, manufacturer of the E-paper display. * Fix linting eror --- .../waveshare_epaper/waveshare_epaper.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index fd869b46ec..695ba9c455 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -425,20 +425,22 @@ void WaveshareEPaper2P7In::initialize() { this->data(i); } void HOT WaveshareEPaper2P7In::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION 1 this->command(0x10); delay(2); - this->start_data_(); - this->write_array(this->buffer_, this->get_buffer_length_()); - this->end_data_(); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } delay(2); // COMMAND DATA START TRANSMISSION 2 this->command(0x13); delay(2); - this->start_data_(); - this->write_array(this->buffer_, this->get_buffer_length_()); - this->end_data_(); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } // COMMAND DISPLAY REFRESH this->command(0x12); From 31ae337931399922b5d5fd3bbdeb545533af263b Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 25 Apr 2020 15:39:34 -0300 Subject: [PATCH 192/412] add lights on off triggers (#1037) * add lights on off triggers * add test --- esphome/components/light/__init__.py | 20 ++++++++++++++-- esphome/components/light/automation.h | 34 +++++++++++++++++++++++++++ esphome/components/light/types.py | 4 ++++ tests/test1.yaml | 8 +++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 63aac69462..2a44b044b9 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,15 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.automation as auto from esphome.components import mqtt, power_supply from esphome.const import CONF_COLOR_CORRECT, \ CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, CONF_GAMMA_CORRECT, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY, CONF_RESTORE_MODE + CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY, CONF_RESTORE_MODE, \ + CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID from esphome.core import coroutine, coroutine_with_priority from .automation import light_control_to_code # noqa from .effects import validate_effects, BINARY_EFFECTS, \ MONOCHROMATIC_EFFECTS, RGB_EFFECTS, ADDRESSABLE_EFFECTS, EFFECTS_REGISTRY from .types import ( # noqa - LightState, AddressableLightState, light_ns, LightOutput, AddressableLight) + LightState, AddressableLightState, light_ns, LightOutput, AddressableLight, \ + LightTurnOnTrigger, LightTurnOffTrigger) IS_PLATFORM_COMPONENT = True @@ -26,6 +29,12 @@ LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTJSONLightComponent), cv.Optional(CONF_RESTORE_MODE, default='restore_default_off'): cv.enum(RESTORE_MODES, upper=True, space='_'), + cv.Optional(CONF_ON_TURN_ON): auto.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), + }), + cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), + }), }) BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend({ @@ -62,6 +71,13 @@ def setup_light_core_(light_var, output_var, config): effects = yield cg.build_registry_list(EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])) cg.add(light_var.add_effects(effects)) + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + yield auto.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_OFF, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + yield auto.build_automation(trigger, [], conf) + if CONF_COLOR_CORRECT in config: cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT])) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 2cd55ab6f6..dfab780658 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -98,6 +98,40 @@ template class LightIsOffCondition : public Condition { LightState *state_; }; +class LightTurnOnTrigger : public Trigger<> { + public: + LightTurnOnTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this, a_light]() { + auto is_on = a_light->current_values.is_on(); + if (is_on && !last_on_) { + this->trigger(); + } + last_on_ = is_on; + }); + last_on_ = a_light->current_values.is_on(); + } + + protected: + bool last_on_; +}; + +class LightTurnOffTrigger : public Trigger<> { + public: + LightTurnOffTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this, a_light]() { + auto is_on = a_light->current_values.is_on(); + if (!is_on && last_on_) { + this->trigger(); + } + last_on_ = is_on; + }); + last_on_ = a_light->current_values.is_on(); + } + + protected: + bool last_on_; +}; + template class AddressableSet : public Action { public: explicit AddressableSet(LightState *parent) : parent_(parent) {} diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index fb88d021f2..d32ef0214c 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -21,6 +21,10 @@ AddressableSet = light_ns.class_('AddressableSet', automation.Action) LightIsOnCondition = light_ns.class_('LightIsOnCondition', automation.Condition) LightIsOffCondition = light_ns.class_('LightIsOffCondition', automation.Condition) +# Triggers +LightTurnOnTrigger = light_ns.class_('LightTurnOnTrigger', automation.Trigger.template()) +LightTurnOffTrigger = light_ns.class_('LightTurnOffTrigger', automation.Trigger.template()) + # Effects LightEffect = light_ns.class_('LightEffect') RandomLightEffect = light_ns.class_('RandomLightEffect', LightEffect) diff --git a/tests/test1.yaml b/tests/test1.yaml index dbb3a4bdb3..59b64b0b6d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1044,6 +1044,14 @@ light: duration: 250ms - state: False duration: 250ms + on_turn_on: + - switch.template.publish: + id: livingroom_lights + state: yes + on_turn_off: + - switch.template.publish: + id: livingroom_lights + state: yes - platform: monochromatic name: "Kitchen Lights" id: kitchen From ba1222eae444ef498fa0af0f6a363b7585f4161f Mon Sep 17 00:00:00 2001 From: puuu Date: Tue, 28 Apr 2020 08:57:02 +0900 Subject: [PATCH 193/412] Bluetooth advertising automation (#995) * esp32_ble_tracker: introduce UUID comparison function * ble_presence, ble_rssi: use new UUID comparison function * esp32_ble_tracker: introduce automation on BLE advertising * test2.yaml: remove deep_sleep due to firmware size restrictions --- .../ble_presence/ble_presence_device.h | 33 +------- esphome/components/ble_rssi/ble_rssi_sensor.h | 33 +------- .../components/esp32_ble_tracker/__init__.py | 59 ++++++++++++- .../components/esp32_ble_tracker/automation.h | 82 +++++++++++++++++++ .../esp32_ble_tracker/esp32_ble_tracker.cpp | 49 ++++++++++- .../esp32_ble_tracker/esp32_ble_tracker.h | 4 + esphome/const.py | 4 + tests/test2.yaml | 23 +++++- 8 files changed, 221 insertions(+), 66 deletions(-) create mode 100644 esphome/components/esp32_ble_tracker/automation.h diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index e721db7dcd..bce6a9cf98 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -43,35 +43,10 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } } else { for (auto uuid : device.get_service_uuids()) { - switch (this->uuid_.get_uuid().len) { - case ESP_UUID_LEN_16: - if (uuid.get_uuid().len == ESP_UUID_LEN_16 && - uuid.get_uuid().uuid.uuid16 == this->uuid_.get_uuid().uuid.uuid16) { - this->publish_state(true); - this->found_ = true; - return true; - } - break; - case ESP_UUID_LEN_32: - if (uuid.get_uuid().len == ESP_UUID_LEN_32 && - uuid.get_uuid().uuid.uuid32 == this->uuid_.get_uuid().uuid.uuid32) { - this->publish_state(true); - this->found_ = true; - return true; - } - break; - case ESP_UUID_LEN_128: - if (uuid.get_uuid().len == ESP_UUID_LEN_128) { - for (int i = 0; i < ESP_UUID_LEN_128; i++) { - if (this->uuid_.get_uuid().uuid.uuid128[i] != uuid.get_uuid().uuid.uuid128[i]) { - return false; - } - } - this->publish_state(true); - this->found_ = true; - return true; - } - break; + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; } } } diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 17dd0d4a7d..2082a52469 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -41,35 +41,10 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi } } else { for (auto uuid : device.get_service_uuids()) { - switch (this->uuid_.get_uuid().len) { - case ESP_UUID_LEN_16: - if (uuid.get_uuid().len == ESP_UUID_LEN_16 && - uuid.get_uuid().uuid.uuid16 == this->uuid_.get_uuid().uuid.uuid16) { - this->publish_state(device.get_rssi()); - this->found_ = true; - return true; - } - break; - case ESP_UUID_LEN_32: - if (uuid.get_uuid().len == ESP_UUID_LEN_32 && - uuid.get_uuid().uuid.uuid32 == this->uuid_.get_uuid().uuid.uuid32) { - this->publish_state(device.get_rssi()); - this->found_ = true; - return true; - } - break; - case ESP_UUID_LEN_128: - if (uuid.get_uuid().len == ESP_UUID_LEN_128) { - for (int i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.get_uuid().uuid.uuid128[i] != this->uuid_.get_uuid().uuid.uuid128[i]) { - return false; - } - } - this->publish_state(device.get_rssi()); - this->found_ = true; - return true; - } - break; + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; } } } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 3311801b6c..c4cc7260fd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,8 +1,11 @@ import re import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ - CONF_DURATION + CONF_DURATION, CONF_TRIGGER_ID, CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_MANUFACTURER_ID, \ + CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_SERVICE_DATA_ADVERTISE, \ + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE from esphome.core import coroutine ESP_PLATFORMS = [ESP_PLATFORM_ESP32] @@ -15,6 +18,17 @@ CONF_ACTIVE = 'active' esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker') ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component) ESPBTDeviceListener = esp32_ble_tracker_ns.class_('ESPBTDeviceListener') +ESPBTDevice = esp32_ble_tracker_ns.class_('ESPBTDevice') +ESPBTDeviceConstRef = ESPBTDevice.operator('ref').operator('const') +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator('ref').operator('const') +# Triggers +ESPBTAdvertiseTrigger = esp32_ble_tracker_ns.class_( + 'ESPBTAdvertiseTrigger', automation.Trigger.template(ESPBTDeviceConstRef)) +BLEServiceDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( + 'BLEServiceDataAdvertiseTrigger', automation.Trigger.template(adv_data_t_const_ref)) +BLEManufacturerDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( + 'BLEManufacturerDataAdvertiseTrigger', automation.Trigger.template(adv_data_t_const_ref)) def validate_scan_parameters(config): @@ -85,6 +99,20 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_WINDOW, default='30ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_ACTIVE, default=True): cv.boolean, }), validate_scan_parameters), + cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + }), + cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEServiceDataAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_SERVICE_UUID): bt_uuid, + }), + cv.Optional(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEManufacturerDataAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_MANUFACTURER_ID): bt_uuid, + }), cv.Optional('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: " "it never had an effect)"), @@ -103,6 +131,35 @@ def to_code(config): cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) cg.add(var.set_scan_active(params[CONF_ACTIVE])) + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + yield automation.build_automation(trigger, [(ESPBTDeviceConstRef, 'x')], conf) + for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format): + cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID]))) + elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): + cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) + elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): + uuid128 = as_hex_array(conf[CONF_SERVICE_UUID]) + cg.add(trigger.set_service_uuid128(uuid128)) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + yield automation.build_automation(trigger, [(adv_data_t_const_ref, 'x')], conf) + for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format): + cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID]))) + elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): + cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) + elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): + uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID]) + cg.add(trigger.set_manufacturer_uuid128(uuid128)) + if CONF_MAC_ADDRESS in conf: + cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + yield automation.build_automation(trigger, [(adv_data_t_const_ref, 'x')], conf) @coroutine diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h new file mode 100644 index 0000000000..9df2587ede --- /dev/null +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_ble_tracker { +class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + this->trigger(device); + return true; + } + + protected: + uint64_t address_ = 0; +}; + +class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit BLEServiceDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + void set_service_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + for (auto &service_data : device.get_service_datas()) { + if (service_data.uuid == this->uuid_) { + this->trigger(service_data.data); + return true; + } + } + return false; + } + + protected: + uint64_t address_ = 0; + ESPBTUUID uuid_; +}; + +class BLEManufacturerDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { + public: + explicit BLEManufacturerDataAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + void set_address(uint64_t address) { this->address_ = address; } + void set_manufacturer_uuid16(uint16_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(uuid); } + void set_manufacturer_uuid32(uint32_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(uuid); } + void set_manufacturer_uuid128(uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); } + + bool parse_device(const ESPBTDevice &device) override { + if (this->address_ && device.address_uint64() != this->address_) { + return false; + } + for (auto &manufacturer_data : device.get_manufacturer_datas()) { + if (manufacturer_data.uuid == this->uuid_) { + this->trigger(manufacturer_data.data); + return true; + } + } + return false; + } + + protected: + uint64_t address_ = 0; + ESPBTUUID uuid_; +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index ab6bfa681c..5109af21fa 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -223,6 +223,22 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; return ret; } +ESPBTUUID ESPBTUUID::as_128bit() const { + if (this->uuid_.len == ESP_UUID_LEN_128) { + return *this; + } + uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint32_t uuid32; + if (this->uuid_.len == ESP_UUID_LEN_32) { + uuid32 = this->uuid_.uuid.uuid32; + } else { + uuid32 = this->uuid_.uuid.uuid16; + } + for (uint8_t i = 0; i < this->uuid_.len; i++) { + data[12 + i] = ((uuid32 >> i * 8) & 0xFF); + } + return ESPBTUUID::from_raw(data); +} bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { if (this->uuid_.len == ESP_UUID_LEN_16) { return (this->uuid_.uuid.uuid16 >> 8) == data2 || (this->uuid_.uuid.uuid16 & 0xFF) == data1; @@ -241,16 +257,43 @@ bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { } return false; } +bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { + if (this->uuid_.len == uuid.uuid_.len) { + switch (this->uuid_.len) { + case ESP_UUID_LEN_16: + if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { + return true; + } + break; + case ESP_UUID_LEN_32: + if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { + return true; + } + break; + case ESP_UUID_LEN_128: + for (int i = 0; i < ESP_UUID_LEN_128; i++) { + if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { + return false; + } + } + return true; + break; + } + } else { + return this->as_128bit() == uuid.as_128bit(); + } + return false; +} esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16); + sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, this->uuid_.uuid.uuid32 >> 16, - this->uuid_.uuid.uuid32 >> 8, this->uuid_.uuid.uuid32); + sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 5456adbfe5..8d011abfe3 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -23,8 +23,12 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + ESPBTUUID as_128bit() const; + bool contains(uint8_t data1, uint8_t data2) const; + bool operator==(const ESPBTUUID &uuid) const; + esp_bt_uuid_t get_uuid(); std::string to_string(); diff --git a/esphome/const.py b/esphome/const.py index 36ffbed254..8974009d30 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -235,6 +235,7 @@ CONF_MAC_ADDRESS = 'mac_address' CONF_MAINS_FILTER = 'mains_filter' CONF_MAKE_ID = 'make_id' CONF_MANUAL_IP = 'manual_ip' +CONF_MANUFACTURER_ID = 'manufacturer_id' CONF_MASK_DISTURBER = 'mask_disturber' CONF_MAX_CURRENT = 'max_current' CONF_MAX_DURATION = 'max_duration' @@ -280,6 +281,9 @@ CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' CONF_OFFSET = 'offset' CONF_ON = 'on' +CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' +CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = 'on_ble_manufacturer_data_advertise' +CONF_ON_BLE_SERVICE_DATA_ADVERTISE = 'on_ble_service_data_advertise' CONF_ON_BOOT = 'on_boot' CONF_ON_CLICK = 'on_click' CONF_ON_DOUBLE_CLICK = 'on_double_click' diff --git a/tests/test2.yaml b/tests/test2.yaml index bcc777b83b..d02c093b86 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -49,10 +49,6 @@ web_server: username: admin password: admin -deep_sleep: - run_duration: 20s - sleep_duration: 50s - as3935_i2c: irq_pin: GPIO12 @@ -233,6 +229,25 @@ binary_sensor: name: "Storm Alert" esp32_ble_tracker: + on_ble_advertise: + - mac_address: AC:37:43:77:5F:4C + then: + - lambda: !lambda |- + ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + - then: + - lambda: !lambda |- + ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + on_ble_service_data_advertise: + - service_uuid: ABCD + then: + - lambda: !lambda |- + ESP_LOGD("main", "Length of service data is %i", x.size()); + on_ble_manufacturer_data_advertise: + - manufacturer_id: ABCD + then: + - lambda: !lambda |- + ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); + #esp32_ble_beacon: # type: iBeacon From a2a83c5004dfe49b179cf45788c1673951f3a679 Mon Sep 17 00:00:00 2001 From: C W Date: Tue, 28 Apr 2020 08:22:33 -0700 Subject: [PATCH 194/412] Fix missing yield in uart causing watchdog timer resets in esp32 when blocking waiting on serial responses. (#1016) --- esphome/components/uart/uart.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 205e9e2300..08fc0a326e 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -96,6 +96,7 @@ bool UARTComponent::check_read_timeout_(size_t len) { ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); return false; } + yield(); } return true; } From 39b35b79ba24146dfd7d399bdceca7c4d25cd75e Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 29 Apr 2020 00:24:06 +0200 Subject: [PATCH 195/412] Make initial run variable available to addressable_lambda (#1035) * Make initial run variable available to addressable_lambda * Fix linting * Remove if clause --- esphome/components/light/addressable_light_effect.h | 10 +++++++--- esphome/components/light/effects.py | 2 +- tests/test1.yaml | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 78ae41baad..e6528fcd8a 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -50,21 +50,25 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, const std::function &f, + AddressableLambdaLightEffect(const std::string &name, + const std::function &f, uint32_t update_interval) : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} + void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const ESPColor ¤t_color) override { const uint32_t now = millis(); if (now - this->last_run_ >= this->update_interval_) { this->last_run_ = now; - this->f_(it, current_color); + this->f_(it, current_color, this->initial_run_); + this->initial_run_ = false; } } protected: - std::function f_; + std::function f_; uint32_t update_interval_; uint32_t last_run_{0}; + bool initial_run_; }; class AddressableRainbowLightEffect : public AddressableLightEffect { diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index d08e7f08f5..d8c709b8ad 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -162,7 +162,7 @@ def flicker_effect_to_code(config, effect_id): } ) def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')] + args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color'), (bool, 'initial_run')] lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]) diff --git a/tests/test1.yaml b/tests/test1.yaml index 59b64b0b6d..18adcf70a3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1155,7 +1155,9 @@ light: - addressable_lambda: name: "Test For Custom Lambda Effect" lambda: |- - it[0] = current_color; + if (initial_run) { + it[0] = current_color; + } - automation: name: Custom Effect From af66753c1b49f230e57685d44ae2ef2eb7ddc60e Mon Sep 17 00:00:00 2001 From: Jonathan Adams <813248+jonathanadams@users.noreply.github.com> Date: Wed, 29 Apr 2020 14:04:05 +0100 Subject: [PATCH 196/412] Dashboard Updates (#1025) * Restructuring of static files directory * Update jQuery to v3.5.0 * Update jQuery UI to v1.12.1 * Update jQuery Validate to v1.19.1 * Improve login page layout and color scheme * Updated header & footer colour scheme & added ESPHome logo * Restructuring of fonts directory * Restructuring of fonts directory * Corrected icon reference error * Update node layout and styling * Update Ace to v1.4.10 * JS file reorganisation * Rewrite of LogModal class including refactorization of js & html * Updated delete node function * Rewrite of editor modal * Update Materialize Stepper to v3.1.0 * Updates to the wizard modal * Added wizard validators back in and removed comments * Merge old stylesheet into new * Linting errors * Fixed dashboard layout issue when no nodes present * Introduced dynamic 3 column layout for large screens * Removed unnecessary code * Update data attribute names * Added loading indicator to editor * Open validator websocket on document ready * Automatically restart validator websocket if it closes * Minor styling updates * Improvements to on boarding process * Node display filename and then path on hover * Removing console.logs * Minor styling revisions * Added toast on begin * Fix lint Co-authored-by: Sergio Mayoral Martinez --- esphome/dashboard/static/ace.js | 16 - esphome/dashboard/static/css/esphome.css | 393 ++++++ .../materialize-stepper.min.css | 10 + .../vendor/materialize}/materialize.min.css | 0 esphome/dashboard/static/esphome.css | 256 ---- esphome/dashboard/static/esphome.js | 782 ------------ esphome/dashboard/static/ext-searchbox.js | 7 - .../static/fonts/{ => material-icons}/LICENSE | 0 .../MaterialIcons-Regular.woff | Bin .../MaterialIcons-Regular.woff2 | Bin .../fonts/{ => material-icons}/README.md | 0 .../{ => material-icons}/material-icons.css | 0 .../dashboard/static/{ => images}/favicon.ico | Bin esphome/dashboard/static/jquery-ui.min.js | 401 ------ esphome/dashboard/static/jquery.min.js | 4 - .../dashboard/static/jquery.validate.min.js | 4 - esphome/dashboard/static/js/esphome.js | 1005 +++++++++++++++ esphome/dashboard/static/js/vendor/ace/ace.js | 16 + .../static/js/vendor/ace/ext-searchbox.js | 7 + .../static/js/vendor/ace/mode-yaml.js | 7 + .../static/js/vendor/ace/theme-dreamweaver.js | 7 + .../js/vendor/jquery-ui/jquery-ui.min.js | 13 + .../jquery-validate/jquery.validate.min.js | 4 + .../static/js/vendor/jquery/jquery.min.js | 2 + .../materialize-stepper.min.js | 10 + .../vendor/materialize}/materialize.min.js | 0 .../static/materialize-stepper.min.css | 5 - .../static/materialize-stepper.min.js | 5 - esphome/dashboard/static/mode-yaml.js | 7 - esphome/dashboard/static/theme-dreamweaver.js | 7 - esphome/dashboard/templates/index.html | 1129 +++++++++-------- esphome/dashboard/templates/login.html | 136 +- 32 files changed, 2174 insertions(+), 2059 deletions(-) delete mode 100644 esphome/dashboard/static/ace.js create mode 100644 esphome/dashboard/static/css/esphome.css create mode 100644 esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css rename esphome/dashboard/static/{ => css/vendor/materialize}/materialize.min.css (100%) delete mode 100644 esphome/dashboard/static/esphome.css delete mode 100644 esphome/dashboard/static/esphome.js delete mode 100644 esphome/dashboard/static/ext-searchbox.js rename esphome/dashboard/static/fonts/{ => material-icons}/LICENSE (100%) rename esphome/dashboard/static/fonts/{ => material-icons}/MaterialIcons-Regular.woff (100%) rename esphome/dashboard/static/fonts/{ => material-icons}/MaterialIcons-Regular.woff2 (100%) rename esphome/dashboard/static/fonts/{ => material-icons}/README.md (100%) rename esphome/dashboard/static/fonts/{ => material-icons}/material-icons.css (100%) rename esphome/dashboard/static/{ => images}/favicon.ico (100%) delete mode 100644 esphome/dashboard/static/jquery-ui.min.js delete mode 100644 esphome/dashboard/static/jquery.min.js delete mode 100644 esphome/dashboard/static/jquery.validate.min.js create mode 100644 esphome/dashboard/static/js/esphome.js create mode 100644 esphome/dashboard/static/js/vendor/ace/ace.js create mode 100644 esphome/dashboard/static/js/vendor/ace/ext-searchbox.js create mode 100644 esphome/dashboard/static/js/vendor/ace/mode-yaml.js create mode 100644 esphome/dashboard/static/js/vendor/ace/theme-dreamweaver.js create mode 100644 esphome/dashboard/static/js/vendor/jquery-ui/jquery-ui.min.js create mode 100644 esphome/dashboard/static/js/vendor/jquery-validate/jquery.validate.min.js create mode 100644 esphome/dashboard/static/js/vendor/jquery/jquery.min.js create mode 100644 esphome/dashboard/static/js/vendor/materialize-stepper/materialize-stepper.min.js rename esphome/dashboard/static/{ => js/vendor/materialize}/materialize.min.js (100%) delete mode 100644 esphome/dashboard/static/materialize-stepper.min.css delete mode 100644 esphome/dashboard/static/materialize-stepper.min.js delete mode 100644 esphome/dashboard/static/mode-yaml.js delete mode 100644 esphome/dashboard/static/theme-dreamweaver.js diff --git a/esphome/dashboard/static/ace.js b/esphome/dashboard/static/ace.js deleted file mode 100644 index f5fca22af2..0000000000 --- a/esphome/dashboard/static/ace.js +++ /dev/null @@ -1,16 +0,0 @@ -(function(){function o(n){var i=e;n&&(e[n]||(e[n]={}),i=e[n]);if(!i.define||!i.define.packaged)t.original=i.define,i.define=t,i.define.packaged=!0;if(!i.require||!i.require.packaged)r.original=i.require,i.require=r,i.require.packaged=!0}var ACE_NAMESPACE = "ace",e=function(){return this}();!e&&typeof window!="undefined"&&(e=window);if(!ACE_NAMESPACE&&typeof requirejs!="undefined")return;var t=function(e,n,r){if(typeof e!="string"){t.original?t.original.apply(this,arguments):(console.error("dropping module because define wasn't a string."),console.trace());return}arguments.length==2&&(r=n),t.modules[e]||(t.payloads[e]=r,t.modules[e]=null)};t.modules={},t.payloads={};var n=function(e,t,n){if(typeof t=="string"){var i=s(e,t);if(i!=undefined)return n&&n(),i}else if(Object.prototype.toString.call(t)==="[object Array]"){var o=[];for(var u=0,a=t.length;u1&&u(t,"")>-1&&(a=RegExp(this.source,r.replace.call(o(this),"g","")),r.replace.call(e.slice(t.index),a,function(){for(var e=1;et.index&&this.lastIndex--}return t},s||(RegExp.prototype.test=function(e){var t=r.exec.call(this,e);return t&&this.global&&!t[0].length&&this.lastIndex>t.index&&this.lastIndex--,!!t})}),ace.define("ace/lib/es5-shim",["require","exports","module"],function(e,t,n){function r(){}function w(e){try{return Object.defineProperty(e,"sentinel",{}),"sentinel"in e}catch(t){}}function H(e){return e=+e,e!==e?e=0:e!==0&&e!==1/0&&e!==-1/0&&(e=(e>0||-1)*Math.floor(Math.abs(e))),e}function B(e){var t=typeof e;return e===null||t==="undefined"||t==="boolean"||t==="number"||t==="string"}function j(e){var t,n,r;if(B(e))return e;n=e.valueOf;if(typeof n=="function"){t=n.call(e);if(B(t))return t}r=e.toString;if(typeof r=="function"){t=r.call(e);if(B(t))return t}throw new TypeError}Function.prototype.bind||(Function.prototype.bind=function(t){var n=this;if(typeof n!="function")throw new TypeError("Function.prototype.bind called on incompatible "+n);var i=u.call(arguments,1),s=function(){if(this instanceof s){var e=n.apply(this,i.concat(u.call(arguments)));return Object(e)===e?e:this}return n.apply(t,i.concat(u.call(arguments)))};return n.prototype&&(r.prototype=n.prototype,s.prototype=new r,r.prototype=null),s});var i=Function.prototype.call,s=Array.prototype,o=Object.prototype,u=s.slice,a=i.bind(o.toString),f=i.bind(o.hasOwnProperty),l,c,h,p,d;if(d=f(o,"__defineGetter__"))l=i.bind(o.__defineGetter__),c=i.bind(o.__defineSetter__),h=i.bind(o.__lookupGetter__),p=i.bind(o.__lookupSetter__);if([1,2].splice(0).length!=2)if(!function(){function e(e){var t=new Array(e+2);return t[0]=t[1]=0,t}var t=[],n;t.splice.apply(t,e(20)),t.splice.apply(t,e(26)),n=t.length,t.splice(5,0,"XXX"),n+1==t.length;if(n+1==t.length)return!0}())Array.prototype.splice=function(e,t){var n=this.length;e>0?e>n&&(e=n):e==void 0?e=0:e<0&&(e=Math.max(n+e,0)),e+ta)for(h=l;h--;)this[f+h]=this[a+h];if(s&&e===c)this.length=c,this.push.apply(this,i);else{this.length=c+s;for(h=0;h>>0;if(a(t)!="[object Function]")throw new TypeError;while(++s>>0,s=Array(i),o=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var u=0;u>>0,s=[],o,u=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var f=0;f>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var s=0,o;if(arguments.length>=2)o=arguments[1];else do{if(s in r){o=r[s++];break}if(++s>=i)throw new TypeError("reduce of empty array with no initial value")}while(!0);for(;s>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var s,o=i-1;if(arguments.length>=2)s=arguments[1];else do{if(o in r){s=r[o--];break}if(--o<0)throw new TypeError("reduceRight of empty array with no initial value")}while(!0);do o in this&&(s=t.call(void 0,s,r[o],o,n));while(o--);return s});if(!Array.prototype.indexOf||[0,1].indexOf(1,2)!=-1)Array.prototype.indexOf=function(t){var n=g&&a(this)=="[object String]"?this.split(""):F(this),r=n.length>>>0;if(!r)return-1;var i=0;arguments.length>1&&(i=H(arguments[1])),i=i>=0?i:Math.max(0,r+i);for(;i>>0;if(!r)return-1;var i=r-1;arguments.length>1&&(i=Math.min(i,H(arguments[1]))),i=i>=0?i:r-Math.abs(i);for(;i>=0;i--)if(i in n&&t===n[i])return i;return-1};Object.getPrototypeOf||(Object.getPrototypeOf=function(t){return t.__proto__||(t.constructor?t.constructor.prototype:o)});if(!Object.getOwnPropertyDescriptor){var y="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(t,n){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(y+t);if(!f(t,n))return;var r,i,s;r={enumerable:!0,configurable:!0};if(d){var u=t.__proto__;t.__proto__=o;var i=h(t,n),s=p(t,n);t.__proto__=u;if(i||s)return i&&(r.get=i),s&&(r.set=s),r}return r.value=t[n],r}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(t){return Object.keys(t)});if(!Object.create){var b;Object.prototype.__proto__===null?b=function(){return{__proto__:null}}:b=function(){var e={};for(var t in e)e[t]=null;return e.constructor=e.hasOwnProperty=e.propertyIsEnumerable=e.isPrototypeOf=e.toLocaleString=e.toString=e.valueOf=e.__proto__=null,e},Object.create=function(t,n){var r;if(t===null)r=b();else{if(typeof t!="object")throw new TypeError("typeof prototype["+typeof t+"] != 'object'");var i=function(){};i.prototype=t,r=new i,r.__proto__=t}return n!==void 0&&Object.defineProperties(r,n),r}}if(Object.defineProperty){var E=w({}),S=typeof document=="undefined"||w(document.createElement("div"));if(!E||!S)var x=Object.defineProperty}if(!Object.defineProperty||x){var T="Property description must be an object: ",N="Object.defineProperty called on non-object: ",C="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(t,n,r){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(N+t);if(typeof r!="object"&&typeof r!="function"||r===null)throw new TypeError(T+r);if(x)try{return x.call(Object,t,n,r)}catch(i){}if(f(r,"value"))if(d&&(h(t,n)||p(t,n))){var s=t.__proto__;t.__proto__=o,delete t[n],t[n]=r.value,t.__proto__=s}else t[n]=r.value;else{if(!d)throw new TypeError(C);f(r,"get")&&l(t,n,r.get),f(r,"set")&&c(t,n,r.set)}return t}}Object.defineProperties||(Object.defineProperties=function(t,n){for(var r in n)f(n,r)&&Object.defineProperty(t,r,n[r]);return t}),Object.seal||(Object.seal=function(t){return t}),Object.freeze||(Object.freeze=function(t){return t});try{Object.freeze(function(){})}catch(k){Object.freeze=function(t){return function(n){return typeof n=="function"?n:t(n)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(t){return t}),Object.isSealed||(Object.isSealed=function(t){return!1}),Object.isFrozen||(Object.isFrozen=function(t){return!1}),Object.isExtensible||(Object.isExtensible=function(t){if(Object(t)===t)throw new TypeError;var n="";while(f(t,n))n+="?";t[n]=!0;var r=f(t,n);return delete t[n],r});if(!Object.keys){var L=!0,A=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],O=A.length;for(var M in{toString:null})L=!1;Object.keys=function I(e){if(typeof e!="object"&&typeof e!="function"||e===null)throw new TypeError("Object.keys called on a non-object");var I=[];for(var t in e)f(e,t)&&I.push(t);if(L)for(var n=0,r=O;n=0?parseFloat((i.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]):parseFloat((i.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]),t.isOldIE=t.isIE&&t.isIE<9,t.isGecko=t.isMozilla=i.match(/ Gecko\/\d+/),t.isOpera=window.opera&&Object.prototype.toString.call(window.opera)=="[object Opera]",t.isWebKit=parseFloat(i.split("WebKit/")[1])||undefined,t.isChrome=parseFloat(i.split(" Chrome/")[1])||undefined,t.isEdge=parseFloat(i.split(" Edge/")[1])||undefined,t.isAIR=i.indexOf("AdobeAIR")>=0,t.isIPad=i.indexOf("iPad")>=0,t.isAndroid=i.indexOf("Android")>=0,t.isChromeOS=i.indexOf(" CrOS ")>=0,t.isIOS=/iPad|iPhone|iPod/.test(i)&&!window.MSStream,t.isIOS&&(t.isMac=!0),t.isMobile=t.isIPad||t.isAndroid}),ace.define("ace/lib/dom",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("./useragent"),i="http://www.w3.org/1999/xhtml";t.buildDom=function o(e,t,n){if(typeof e=="string"&&e){var r=document.createTextNode(e);return t&&t.appendChild(r),r}if(!Array.isArray(e))return e;if(typeof e[0]!="string"||!e[0]){var i=[];for(var s=0;s=1.5:!0;if(typeof document!="undefined"){var s=document.createElement("div");t.HI_DPI&&s.style.transform!==undefined&&(t.HAS_CSS_TRANSFORMS=!0),!r.isEdge&&typeof s.style.animationName!="undefined"&&(t.HAS_CSS_ANIMATION=!0),s=null}t.HAS_CSS_TRANSFORMS?t.translate=function(e,t,n){e.style.transform="translate("+Math.round(t)+"px, "+Math.round(n)+"px)"}:t.translate=function(e,t,n){e.style.top=Math.round(n)+"px",e.style.left=Math.round(t)+"px"}}),ace.define("ace/lib/oop",["require","exports","module"],function(e,t,n){"use strict";t.inherits=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},t.mixin=function(e,t){for(var n in t)e[n]=t[n];return e},t.implement=function(e,n){t.mixin(e,n)}}),ace.define("ace/lib/keys",["require","exports","module","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./oop"),i=function(){var e={MODIFIER_KEYS:{16:"Shift",17:"Ctrl",18:"Alt",224:"Meta"},KEY_MODS:{ctrl:1,alt:2,option:2,shift:4,"super":8,meta:8,command:8,cmd:8},FUNCTION_KEYS:{8:"Backspace",9:"Tab",13:"Return",19:"Pause",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"Print",45:"Insert",46:"Delete",96:"Numpad0",97:"Numpad1",98:"Numpad2",99:"Numpad3",100:"Numpad4",101:"Numpad5",102:"Numpad6",103:"Numpad7",104:"Numpad8",105:"Numpad9","-13":"NumpadEnter",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"Numlock",145:"Scrolllock"},PRINTABLE_KEYS:{32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"a",66:"b",67:"c",68:"d",69:"e",70:"f",71:"g",72:"h",73:"i",74:"j",75:"k",76:"l",77:"m",78:"n",79:"o",80:"p",81:"q",82:"r",83:"s",84:"t",85:"u",86:"v",87:"w",88:"x",89:"y",90:"z",107:"+",109:"-",110:".",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",111:"/",106:"*"}},t,n;for(n in e.FUNCTION_KEYS)t=e.FUNCTION_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);for(n in e.PRINTABLE_KEYS)t=e.PRINTABLE_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);return r.mixin(e,e.MODIFIER_KEYS),r.mixin(e,e.PRINTABLE_KEYS),r.mixin(e,e.FUNCTION_KEYS),e.enter=e["return"],e.escape=e.esc,e.del=e["delete"],e[173]="-",function(){var t=["cmd","ctrl","alt","shift"];for(var n=Math.pow(2,t.length);n--;)e.KEY_MODS[n]=t.filter(function(t){return n&e.KEY_MODS[t]}).join("-")+"-"}(),e.KEY_MODS[0]="",e.KEY_MODS[-1]="input-",e}();r.mixin(t,i),t.keyCodeToString=function(e){var t=i[e];return typeof t!="string"&&(t=String.fromCharCode(e)),t.toLowerCase()}}),ace.define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function a(e,t,n){var a=u(t);if(!i.isMac&&s){t.getModifierState&&(t.getModifierState("OS")||t.getModifierState("Win"))&&(a|=8);if(s.altGr){if((3&a)==3)return;s.altGr=0}if(n===18||n===17){var f="location"in t?t.location:t.keyLocation;if(n===17&&f===1)s[n]==1&&(o=t.timeStamp);else if(n===18&&a===3&&f===2){var l=t.timeStamp-o;l<50&&(s.altGr=!0)}}}n in r.MODIFIER_KEYS&&(n=-1),a&8&&n>=91&&n<=93&&(n=-1);if(!a&&n===13){var f="location"in t?t.location:t.keyLocation;if(f===3){e(t,a,-n);if(t.defaultPrevented)return}}if(i.isChromeOS&&a&8){e(t,a,n);if(t.defaultPrevented)return;a&=-9}return!!a||n in r.FUNCTION_KEYS||n in r.PRINTABLE_KEYS?e(t,a,n):!1}function f(){s=Object.create(null)}var r=e("./keys"),i=e("./useragent"),s=null,o=0;t.addListener=function(e,t,n){if(e.addEventListener)return e.addEventListener(t,n,!1);if(e.attachEvent){var r=function(){n.call(e,window.event)};n._wrapper=r,e.attachEvent("on"+t,r)}},t.removeListener=function(e,t,n){if(e.removeEventListener)return e.removeEventListener(t,n,!1);e.detachEvent&&e.detachEvent("on"+t,n._wrapper||n)},t.stopEvent=function(e){return t.stopPropagation(e),t.preventDefault(e),!1},t.stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},t.preventDefault=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1},t.getButton=function(e){return e.type=="dblclick"?0:e.type=="contextmenu"||i.isMac&&e.ctrlKey&&!e.altKey&&!e.shiftKey?2:e.preventDefault?e.button:{1:0,2:2,4:1}[e.button]},t.capture=function(e,n,r){function i(e){n&&n(e),r&&r(e),t.removeListener(document,"mousemove",n,!0),t.removeListener(document,"mouseup",i,!0),t.removeListener(document,"dragstart",i,!0)}return t.addListener(document,"mousemove",n,!0),t.addListener(document,"mouseup",i,!0),t.addListener(document,"dragstart",i,!0),i},t.addTouchMoveListener=function(e,n){var r,i;t.addListener(e,"touchstart",function(e){var t=e.touches,n=t[0];r=n.clientX,i=n.clientY}),t.addListener(e,"touchmove",function(e){var t=e.touches;if(t.length>1)return;var s=t[0];e.wheelX=r-s.clientX,e.wheelY=i-s.clientY,r=s.clientX,i=s.clientY,n(e)})},t.addMouseWheelListener=function(e,n){"onmousewheel"in e?t.addListener(e,"mousewheel",function(e){var t=8;e.wheelDeltaX!==undefined?(e.wheelX=-e.wheelDeltaX/t,e.wheelY=-e.wheelDeltaY/t):(e.wheelX=0,e.wheelY=-e.wheelDelta/t),n(e)}):"onwheel"in e?t.addListener(e,"wheel",function(e){var t=.35;switch(e.deltaMode){case e.DOM_DELTA_PIXEL:e.wheelX=e.deltaX*t||0,e.wheelY=e.deltaY*t||0;break;case e.DOM_DELTA_LINE:case e.DOM_DELTA_PAGE:e.wheelX=(e.deltaX||0)*5,e.wheelY=(e.deltaY||0)*5}n(e)}):t.addListener(e,"DOMMouseScroll",function(e){e.axis&&e.axis==e.HORIZONTAL_AXIS?(e.wheelX=(e.detail||0)*5,e.wheelY=0):(e.wheelX=0,e.wheelY=(e.detail||0)*5),n(e)})},t.addMultiMouseDownListener=function(e,n,r,s){function c(e){t.getButton(e)!==0?o=0:e.detail>1?(o++,o>4&&(o=1)):o=1;if(i.isIE){var c=Math.abs(e.clientX-u)>5||Math.abs(e.clientY-a)>5;if(!f||c)o=1;f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),o==1&&(u=e.clientX,a=e.clientY)}e._clicks=o,r[s]("mousedown",e);if(o>4)o=0;else if(o>1)return r[s](l[o],e)}function h(e){o=2,f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),r[s]("mousedown",e),r[s](l[o],e)}var o=0,u,a,f,l={2:"dblclick",3:"tripleclick",4:"quadclick"};Array.isArray(e)||(e=[e]),e.forEach(function(e){t.addListener(e,"mousedown",c),i.isOldIE&&t.addListener(e,"dblclick",h)})};var u=!i.isMac||!i.isOpera||"KeyboardEvent"in window?function(e){return 0|(e.ctrlKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.metaKey?8:0)}:function(e){return 0|(e.metaKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.ctrlKey?8:0)};t.getModifierString=function(e){return r.KEY_MODS[u(e)]},t.addCommandKeyListener=function(e,n){var r=t.addListener;if(i.isOldGecko||i.isOpera&&!("KeyboardEvent"in window)){var o=null;r(e,"keydown",function(e){o=e.keyCode}),r(e,"keypress",function(e){return a(n,e,o)})}else{var u=null;r(e,"keydown",function(e){s[e.keyCode]=(s[e.keyCode]||0)+1;var t=a(n,e,e.keyCode);return u=e.defaultPrevented,t}),r(e,"keypress",function(e){u&&(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey)&&(t.stopEvent(e),u=null)}),r(e,"keyup",function(e){s[e.keyCode]=null}),s||(f(),r(window,"focus",f))}};if(typeof window=="object"&&window.postMessage&&!i.isOldIE){var l=1;t.nextTick=function(e,n){n=n||window;var r="zero-timeout-message-"+l++,i=function(s){s.data==r&&(t.stopPropagation(s),t.removeListener(n,"message",i),e())};t.addListener(n,"message",i),n.postMessage(r,"*")}}t.$idleBlocked=!1,t.onIdle=function(e,n){return setTimeout(function r(){t.$idleBlocked?setTimeout(r,100):e()},n)},t.$idleBlockId=null,t.blockIdle=function(e){t.$idleBlockId&&clearTimeout(t.$idleBlockId),t.$idleBlocked=!0,t.$idleBlockId=setTimeout(function(){t.$idleBlocked=!1},e||100)},t.nextFrame=typeof window=="object"&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame),t.nextFrame?t.nextFrame=t.nextFrame.bind(window):t.nextFrame=function(e){setTimeout(e,17)}}),ace.define("ace/range",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t){return e.row-t.row||e.column-t.column},i=function(e,t,n,r){this.start={row:e,column:t},this.end={row:n,column:r}};(function(){this.isEqual=function(e){return this.start.row===e.start.row&&this.end.row===e.end.row&&this.start.column===e.start.column&&this.end.column===e.end.column},this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?tthis.end.column?1:0:ethis.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.rowt)var r={row:t+1,column:0};else if(this.start.row0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n63,l=400,c=e("../lib/keys"),h=c.KEY_MODS,p=i.isIOS,d=p?/\s/:/\n/,v=function(e,t){function W(){x=!0,n.blur(),n.focus(),x=!1}function V(e){e.keyCode==27&&n.value.lengthC&&T[s]=="\n")o=c.end;else if(rC&&T.slice(0,s).split("\n").length>2)o=c.down;else if(s>C&&T[s-1]==" ")o=c.right,u=h.option;else if(s>C||s==C&&C!=N&&r==s)o=c.right;r!==s&&(u|=h.shift),o&&(t.onCommandKey(null,u,o),N=r,C=s,A(""))};document.addEventListener("selectionchange",s),t.on("destroy",function(){document.removeEventListener("selectionchange",s)})}var n=s.createElement("textarea");n.className="ace_text-input",n.setAttribute("wrap","off"),n.setAttribute("autocorrect","off"),n.setAttribute("autocapitalize","off"),n.setAttribute("spellcheck",!1),n.style.opacity="0",e.insertBefore(n,e.firstChild);var v=!1,m=!1,g=!1,y=!1,b="",w=!0,E=!1;i.isMobile||(n.style.fontSize="1px");var S=!1,x=!1,T="",N=0,C=0;try{var k=document.activeElement===n}catch(L){}r.addListener(n,"blur",function(e){if(x)return;t.onBlur(e),k=!1}),r.addListener(n,"focus",function(e){if(x)return;k=!0;if(i.isEdge)try{if(!document.hasFocus())return}catch(e){}t.onFocus(e),i.isEdge?setTimeout(A):A()}),this.$focusScroll=!1,this.focus=function(){if(b||f||this.$focusScroll=="browser")return n.focus({preventScroll:!0});var e=n.style.top;n.style.position="fixed",n.style.top="0px";try{var t=n.getBoundingClientRect().top!=0}catch(r){return}var i=[];if(t){var s=n.parentElement;while(s&&s.nodeType==1)i.push(s),s.setAttribute("ace_nocontext",!0),!s.parentElement&&s.getRootNode?s=s.getRootNode().host:s=s.parentElement}n.focus({preventScroll:!0}),t&&i.forEach(function(e){e.removeAttribute("ace_nocontext")}),setTimeout(function(){n.style.position="",n.style.top=="0px"&&(n.style.top=e)},0)},this.blur=function(){n.blur()},this.isFocused=function(){return k},t.on("beforeEndOperation",function(){if(t.curOp&&t.curOp.command.name=="insertstring")return;g&&(T=n.value="",z()),A()});var A=p?function(e){if(!k||v&&!e)return;e||(e="");var r="\n ab"+e+"cde fg\n";r!=n.value&&(n.value=T=r);var i=4,s=4+(e.length||(t.selection.isEmpty()?0:1));(N!=i||C!=s)&&n.setSelectionRange(i,s),N=i,C=s}:function(){if(g||y)return;if(!k&&!D)return;g=!0;var e=t.selection,r=e.getRange(),i=e.cursor.row,s=r.start.column,o=r.end.column,u=t.session.getLine(i);if(r.start.row!=i){var a=t.session.getLine(i-1);s=r.start.rowi+1?f.length:o,o+=u.length+1,u=u+"\n"+f}u.length>l&&(s=T.length&&e.value===T&&T&&e.selectionEnd!==C},M=function(e){if(g)return;v?v=!1:O(n)&&(t.selectAll(),A())},_=null;this.setInputHandler=function(e){_=e},this.getInputHandler=function(){return _};var D=!1,P=function(e,r){D&&(D=!1);if(m)return A(),e&&t.onPaste(e),m=!1,"";var i=n.selectionStart,s=n.selectionEnd,o=N,u=T.length-C,a=e,f=e.length-i,l=e.length-s,c=0;while(o>0&&T[c]==e[c])c++,o--;a=a.slice(c),c=1;while(u>0&&T.length-c>N-1&&T[T.length-c]==e[e.length-c])c++,u--;return f-=c-1,l-=c-1,a=a.slice(0,a.length-c+1),!r&&f==a.length&&!o&&!u&&!l?"":(y=!0,a&&!o&&!u&&!f&&!l||S?t.onTextInput(a):t.onTextInput(a,{extendLeft:o,extendRight:u,restoreStart:f,restoreEnd:l}),y=!1,T=e,N=i,C=s,a)},H=function(e){if(g)return U();var t=n.value,r=P(t,!0);(t.length>l+100||d.test(r))&&A()},B=function(e,t,n){var r=e.clipboardData||window.clipboardData;if(!r||u)return;var i=a||n?"Text":"text/plain";try{return t?r.setData(i,t)!==!1:r.getData(i)}catch(e){if(!n)return B(e,t,!0)}},j=function(e,i){var s=t.getCopyText();if(!s)return r.preventDefault(e);B(e,s)?(p&&(A(s),v=s,setTimeout(function(){v=!1},10)),i?t.onCut():t.onCopy(),r.preventDefault(e)):(v=!0,n.value=s,n.select(),setTimeout(function(){v=!1,A(),i?t.onCut():t.onCopy()}))},F=function(e){j(e,!0)},I=function(e){j(e,!1)},q=function(e){var s=B(e);typeof s=="string"?(s&&t.onPaste(s,e),i.isIE&&setTimeout(A),r.preventDefault(e)):(n.value="",m=!0)};r.addCommandKeyListener(n,t.onCommandKey.bind(t)),r.addListener(n,"select",M),r.addListener(n,"input",H),r.addListener(n,"cut",F),r.addListener(n,"copy",I),r.addListener(n,"paste",q),(!("oncut"in n)||!("oncopy"in n)||!("onpaste"in n))&&r.addListener(e,"keydown",function(e){if(i.isMac&&!e.metaKey||!e.ctrlKey)return;switch(e.keyCode){case 67:I(e);break;case 86:q(e);break;case 88:F(e)}});var R=function(e){if(g||!t.onCompositionStart||t.$readOnly)return;g={};if(S)return;setTimeout(U,0),t.on("mousedown",W);var r=t.getSelectionRange();r.end.row=r.start.row,r.end.column=r.start.column,g.markerRange=r,g.selectionStart=N,t.onCompositionStart(g),g.useTextareaForIME?(n.value="",T="",N=0,C=0):(n.msGetInputContext&&(g.context=n.msGetInputContext()),n.getInputContext&&(g.context=n.getInputContext()))},U=function(){if(!g||!t.onCompositionUpdate||t.$readOnly)return;if(S)return W();if(g.useTextareaForIME)t.onCompositionUpdate(n.value);else{var e=n.value;P(e),g.markerRange&&(g.context&&(g.markerRange.start.column=g.selectionStart=g.context.compositionStartOffset),g.markerRange.end.column=g.markerRange.start.column+C-g.selectionStart)}},z=function(e){if(!t.onCompositionEnd||t.$readOnly)return;g=!1,t.onCompositionEnd(),t.off("mousedown",W),e&&H()},X=o.delayedCall(U,50).schedule.bind(null,null);r.addListener(n,"compositionstart",R),r.addListener(n,"compositionupdate",U),r.addListener(n,"keyup",V),r.addListener(n,"keydown",X),r.addListener(n,"compositionend",z),this.getElement=function(){return n},this.setCommandMode=function(e){S=e,n.readOnly=!1},this.setReadOnly=function(e){S||(n.readOnly=e)},this.setCopyWithEmptySelection=function(e){E=e},this.onContextMenu=function(e){D=!0,A(),t._emit("nativecontextmenu",{target:t,domEvent:e}),this.moveToMouse(e,!0)},this.moveToMouse=function(e,o){b||(b=n.style.cssText),n.style.cssText=(o?"z-index:100000;":"")+(i.isIE?"opacity:0.1;":"")+"text-indent: -"+(N+C)*t.renderer.characterWidth*.5+"px;";var u=t.container.getBoundingClientRect(),a=s.computedStyle(t.container),f=u.top+(parseInt(a.borderTopWidth)||0),l=u.left+(parseInt(u.borderLeftWidth)||0),c=u.bottom-f-n.clientHeight-2,h=function(e){n.style.left=e.clientX-l-2+"px",n.style.top=Math.min(e.clientY-f-2,c)+"px"};h(e);if(e.type!="mousedown")return;t.renderer.$keepTextAreaAtCursor&&(t.renderer.$keepTextAreaAtCursor=null),clearTimeout($),i.isWin&&r.capture(t.container,h,J)},this.onContextMenuClose=J;var $,K=function(e){t.textInput.onContextMenu(e),J()};r.addListener(n,"mouseup",K),r.addListener(n,"mousedown",function(e){e.preventDefault(),J()}),r.addListener(t.renderer.scroller,"contextmenu",K),r.addListener(n,"contextmenu",K),p&&Q(e,t,n)};t.TextInput=v}),ace.define("ace/mouse/default_handlers",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";function o(e){e.$clickSelection=null;var t=e.editor;t.setDefaultHandler("mousedown",this.onMouseDown.bind(e)),t.setDefaultHandler("dblclick",this.onDoubleClick.bind(e)),t.setDefaultHandler("tripleclick",this.onTripleClick.bind(e)),t.setDefaultHandler("quadclick",this.onQuadClick.bind(e)),t.setDefaultHandler("mousewheel",this.onMouseWheel.bind(e)),t.setDefaultHandler("touchmove",this.onTouchMove.bind(e));var n=["select","startSelect","selectEnd","selectAllEnd","selectByWordsEnd","selectByLinesEnd","dragWait","dragWaitEnd","focusWait"];n.forEach(function(t){e[t]=this[t]},this),e.selectByLines=this.extendSelectionBy.bind(e,"getLineRange"),e.selectByWords=this.extendSelectionBy.bind(e,"getWordRange")}function u(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}function a(e,t){if(e.start.row==e.end.row)var n=2*t.column-e.start.column-e.end.column;else if(e.start.row==e.end.row-1&&!e.start.column&&!e.end.column)var n=t.column-4;else var n=2*t.row-e.start.row-e.end.row;return n<0?{cursor:e.start,anchor:e.end}:{cursor:e.end,anchor:e.start}}var r=e("../lib/useragent"),i=0,s=550;(function(){this.onMouseDown=function(e){var t=e.inSelection(),n=e.getDocumentPosition();this.mousedownEvent=e;var i=this.editor,s=e.getButton();if(s!==0){var o=i.getSelectionRange(),u=o.isEmpty();(u||s==1)&&i.selection.moveToPosition(n),s==2&&(i.textInput.onContextMenu(e.domEvent),r.isMozilla||e.preventDefault());return}this.mousedownEvent.time=Date.now();if(t&&!i.isFocused()){i.focus();if(this.$focusTimeout&&!this.$clickSelection&&!i.inMultiSelectMode){this.setState("focusWait"),this.captureMouse(e);return}}return this.captureMouse(e),this.startSelect(n,e.domEvent._clicks>1),e.preventDefault()},this.startSelect=function(e,t){e=e||this.editor.renderer.screenToTextCoordinates(this.x,this.y);var n=this.editor;if(!this.mousedownEvent)return;this.mousedownEvent.getShiftKey()?n.selection.selectToPosition(e):t||n.selection.moveToPosition(e),t||this.select(),n.renderer.scroller.setCapture&&n.renderer.scroller.setCapture(),n.setStyle("ace_selecting"),this.setState("select")},this.select=function(){var e,t=this.editor,n=t.renderer.screenToTextCoordinates(this.x,this.y);if(this.$clickSelection){var r=this.$clickSelection.comparePoint(n);if(r==-1)e=this.$clickSelection.end;else if(r==1)e=this.$clickSelection.start;else{var i=a(this.$clickSelection,n);n=i.cursor,e=i.anchor}t.selection.setSelectionAnchor(e.row,e.column)}t.selection.selectToPosition(n),t.renderer.scrollCursorIntoView()},this.extendSelectionBy=function(e){var t,n=this.editor,r=n.renderer.screenToTextCoordinates(this.x,this.y),i=n.selection[e](r.row,r.column);if(this.$clickSelection){var s=this.$clickSelection.comparePoint(i.start),o=this.$clickSelection.comparePoint(i.end);if(s==-1&&o<=0){t=this.$clickSelection.end;if(i.end.row!=r.row||i.end.column!=r.column)r=i.start}else if(o==1&&s>=0){t=this.$clickSelection.start;if(i.start.row!=r.row||i.start.column!=r.column)r=i.end}else if(s==-1&&o==1)r=i.end,t=i.start;else{var u=a(this.$clickSelection,r);r=u.cursor,t=u.anchor}n.selection.setSelectionAnchor(t.row,t.column)}n.selection.selectToPosition(r),n.renderer.scrollCursorIntoView()},this.selectEnd=this.selectAllEnd=this.selectByWordsEnd=this.selectByLinesEnd=function(){this.$clickSelection=null,this.editor.unsetStyle("ace_selecting"),this.editor.renderer.scroller.releaseCapture&&this.editor.renderer.scroller.releaseCapture()},this.focusWait=function(){var e=u(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y),t=Date.now();(e>i||t-this.mousedownEvent.time>this.$focusTimeout)&&this.startSelect(this.mousedownEvent.getDocumentPosition())},this.onDoubleClick=function(e){var t=e.getDocumentPosition(),n=this.editor,r=n.session,i=r.getBracketRange(t);i?(i.isEmpty()&&(i.start.column--,i.end.column++),this.setState("select")):(i=n.selection.getWordRange(t.row,t.column),this.setState("selectByWords")),this.$clickSelection=i,this.select()},this.onTripleClick=function(e){var t=e.getDocumentPosition(),n=this.editor;this.setState("selectByLines");var r=n.getSelectionRange();r.isMultiLine()&&r.contains(t.row,t.column)?(this.$clickSelection=n.selection.getLineRange(r.start.row),this.$clickSelection.end=n.selection.getLineRange(r.end.row).end):this.$clickSelection=n.selection.getLineRange(t.row),this.select()},this.onQuadClick=function(e){var t=this.editor;t.selectAll(),this.$clickSelection=t.getSelectionRange(),this.setState("selectAll")},this.onMouseWheel=function(e){if(e.getAccelKey())return;e.getShiftKey()&&e.wheelY&&!e.wheelX&&(e.wheelX=e.wheelY,e.wheelY=0);var t=this.editor;this.$lastScroll||(this.$lastScroll={t:0,vx:0,vy:0,allowed:0});var n=this.$lastScroll,r=e.domEvent.timeStamp,i=r-n.t,o=i?e.wheelX/i:n.vx,u=i?e.wheelY/i:n.vy;i=1&&t.renderer.isScrollableBy(e.wheelX*e.speed,0)&&(f=!0),a<=1&&t.renderer.isScrollableBy(0,e.wheelY*e.speed)&&(f=!0);if(f)n.allowed=r;else if(r-n.allowedt.session.documentToScreenRow(l.row,l.column))return c()}if(f==s)return;f=s.text.join("
"),i.setHtml(f),i.show(),t._signal("showGutterTooltip",i),t.on("mousewheel",c);if(e.$tooltipFollowsMouse)h(u);else{var p=u.domEvent.target,d=p.getBoundingClientRect(),v=i.getElement().style;v.left=d.right+"px",v.top=d.bottom+"px"}}function c(){o&&(o=clearTimeout(o)),f&&(i.hide(),f=null,t._signal("hideGutterTooltip",i),t.removeEventListener("mousewheel",c))}function h(e){i.setPosition(e.x,e.y)}var t=e.editor,n=t.renderer.$gutterLayer,i=new a(t.container);e.editor.setDefaultHandler("guttermousedown",function(r){if(!t.isFocused()||r.getButton()!=0)return;var i=n.getRegion(r);if(i=="foldWidgets")return;var s=r.getDocumentPosition().row,o=t.session.selection;if(r.getShiftKey())o.selectTo(s,0);else{if(r.domEvent.detail==2)return t.selectAll(),r.preventDefault();e.$clickSelection=t.selection.getLineRange(s)}return e.setState("selectByLines"),e.captureMouse(r),r.preventDefault()});var o,u,f;e.editor.setDefaultHandler("guttermousemove",function(t){var n=t.domEvent.target||t.domEvent.srcElement;if(r.hasCssClass(n,"ace_fold-widget"))return c();f&&e.$tooltipFollowsMouse&&h(t),u=t;if(o)return;o=setTimeout(function(){o=null,u&&!e.isMousePressed?l():c()},50)}),s.addListener(t.renderer.$gutter,"mouseout",function(e){u=null;if(!f||o)return;o=setTimeout(function(){o=null,c()},50)}),t.on("changeSession",c)}function a(e){o.call(this,e)}var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event"),o=e("../tooltip").Tooltip;i.inherits(a,o),function(){this.setPosition=function(e,t){var n=window.innerWidth||document.documentElement.clientWidth,r=window.innerHeight||document.documentElement.clientHeight,i=this.getWidth(),s=this.getHeight();e+=15,t+=15,e+i>n&&(e-=e+i-n),t+s>r&&(t-=20+s),o.prototype.setPosition.call(this,e,t)}}.call(a.prototype),t.GutterHandler=u}),ace.define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=t.MouseEvent=function(e,t){this.domEvent=e,this.editor=t,this.x=this.clientX=e.clientX,this.y=this.clientY=e.clientY,this.$pos=null,this.$inSelection=null,this.propagationStopped=!1,this.defaultPrevented=!1};(function(){this.stopPropagation=function(){r.stopPropagation(this.domEvent),this.propagationStopped=!0},this.preventDefault=function(){r.preventDefault(this.domEvent),this.defaultPrevented=!0},this.stop=function(){this.stopPropagation(),this.preventDefault()},this.getDocumentPosition=function(){return this.$pos?this.$pos:(this.$pos=this.editor.renderer.screenToTextCoordinates(this.clientX,this.clientY),this.$pos)},this.inSelection=function(){if(this.$inSelection!==null)return this.$inSelection;var e=this.editor,t=e.getSelectionRange();if(t.isEmpty())this.$inSelection=!1;else{var n=this.getDocumentPosition();this.$inSelection=t.contains(n.row,n.column)}return this.$inSelection},this.getButton=function(){return r.getButton(this.domEvent)},this.getShiftKey=function(){return this.domEvent.shiftKey},this.getAccelKey=i.isMac?function(){return this.domEvent.metaKey}:function(){return this.domEvent.ctrlKey}}).call(s.prototype)}),ace.define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function f(e){function T(e,n){var r=Date.now(),i=!n||e.row!=n.row,s=!n||e.column!=n.column;if(!S||i||s)t.moveCursorToPosition(e),S=r,x={x:p,y:d};else{var o=l(x.x,x.y,p,d);o>a?S=null:r-S>=u&&(t.renderer.scrollCursorIntoView(),S=null)}}function N(e,n){var r=Date.now(),i=t.renderer.layerConfig.lineHeight,s=t.renderer.layerConfig.characterWidth,u=t.renderer.scroller.getBoundingClientRect(),a={x:{left:p-u.left,right:u.right-p},y:{top:d-u.top,bottom:u.bottom-d}},f=Math.min(a.x.left,a.x.right),l=Math.min(a.y.top,a.y.bottom),c={row:e.row,column:e.column};f/s<=2&&(c.column+=a.x.left=o&&t.renderer.scrollCursorIntoView(c):E=r:E=null}function C(){var e=g;g=t.renderer.screenToTextCoordinates(p,d),T(g,e),N(g,e)}function k(){m=t.selection.toOrientedRange(),h=t.session.addMarker(m,"ace_selection",t.getSelectionStyle()),t.clearSelection(),t.isFocused()&&t.renderer.$cursorLayer.setBlinking(!1),clearInterval(v),C(),v=setInterval(C,20),y=0,i.addListener(document,"mousemove",O)}function L(){clearInterval(v),t.session.removeMarker(h),h=null,t.selection.fromOrientedRange(m),t.isFocused()&&!w&&t.renderer.$cursorLayer.setBlinking(!t.getReadOnly()),m=null,g=null,y=0,E=null,S=null,i.removeListener(document,"mousemove",O)}function O(){A==null&&(A=setTimeout(function(){A!=null&&h&&L()},20))}function M(e){var t=e.types;return!t||Array.prototype.some.call(t,function(e){return e=="text/plain"||e=="Text"})}function _(e){var t=["copy","copymove","all","uninitialized"],n=["move","copymove","linkmove","all","uninitialized"],r=s.isMac?e.altKey:e.ctrlKey,i="uninitialized";try{i=e.dataTransfer.effectAllowed.toLowerCase()}catch(e){}var o="none";return r&&t.indexOf(i)>=0?o="copy":n.indexOf(i)>=0?o="move":t.indexOf(i)>=0&&(o="copy"),o}var t=e.editor,n=r.createElement("img");n.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",s.isOpera&&(n.style.cssText="width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;");var f=["dragWait","dragWaitEnd","startDrag","dragReadyEnd","onMouseDrag"];f.forEach(function(t){e[t]=this[t]},this),t.addEventListener("mousedown",this.onMouseDown.bind(e));var c=t.container,h,p,d,v,m,g,y=0,b,w,E,S,x;this.onDragStart=function(e){if(this.cancelDrag||!c.draggable){var r=this;return setTimeout(function(){r.startSelect(),r.captureMouse(e)},0),e.preventDefault()}m=t.getSelectionRange();var i=e.dataTransfer;i.effectAllowed=t.getReadOnly()?"copy":"copyMove",s.isOpera&&(t.container.appendChild(n),n.scrollTop=0),i.setDragImage&&i.setDragImage(n,0,0),s.isOpera&&t.container.removeChild(n),i.clearData(),i.setData("Text",t.session.getTextRange()),w=!0,this.setState("drag")},this.onDragEnd=function(e){c.draggable=!1,w=!1,this.setState(null);if(!t.getReadOnly()){var n=e.dataTransfer.dropEffect;!b&&n=="move"&&t.session.remove(t.getSelectionRange()),t.renderer.$cursorLayer.setBlinking(!0)}this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle("")},this.onDragEnter=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||k(),y++,e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragOver=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||(k(),y++),A!==null&&(A=null),e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragLeave=function(e){y--;if(y<=0&&h)return L(),b=null,i.preventDefault(e)},this.onDrop=function(e){if(!g)return;var n=e.dataTransfer;if(w)switch(b){case"move":m.contains(g.row,g.column)?m={start:g,end:g}:m=t.moveText(m,g);break;case"copy":m=t.moveText(m,g,!0)}else{var r=n.getData("Text");m={start:g,end:t.session.insert(g,r)},t.focus(),b=null}return L(),i.preventDefault(e)},i.addListener(c,"dragstart",this.onDragStart.bind(e)),i.addListener(c,"dragend",this.onDragEnd.bind(e)),i.addListener(c,"dragenter",this.onDragEnter.bind(e)),i.addListener(c,"dragover",this.onDragOver.bind(e)),i.addListener(c,"dragleave",this.onDragLeave.bind(e)),i.addListener(c,"drop",this.onDrop.bind(e));var A=null}function l(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=200,u=200,a=5;(function(){this.dragWait=function(){var e=Date.now()-this.mousedownEvent.time;e>this.editor.getDragDelay()&&this.startDrag()},this.dragWaitEnd=function(){var e=this.editor.container;e.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()),this.selectEnd()},this.dragReadyEnd=function(e){this.editor.renderer.$cursorLayer.setBlinking(!this.editor.getReadOnly()),this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle(""),this.dragWaitEnd()},this.startDrag=function(){this.cancelDrag=!1;var e=this.editor,t=e.container;t.draggable=!0,e.renderer.$cursorLayer.setBlinking(!1),e.setStyle("ace_dragging");var n=s.isWin?"default":"move";e.renderer.setCursorStyle(n),this.setState("dragReady")},this.onMouseDrag=function(e){var t=this.editor.container;if(s.isIE&&this.state=="dragReady"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>3&&t.dragDrop()}if(this.state==="dragWait"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>0&&(t.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()))}},this.onMouseDown=function(e){if(!this.$dragEnabled)return;this.mousedownEvent=e;var t=this.editor,n=e.inSelection(),r=e.getButton(),i=e.domEvent.detail||1;if(i===1&&r===0&&n){if(e.editor.inMultiSelectMode&&(e.getAccelKey()||e.getShiftKey()))return;this.mousedownEvent.time=Date.now();var o=e.domEvent.target||e.domEvent.srcElement;"unselectable"in o&&(o.unselectable="on");if(t.getDragDelay()){if(s.isWebKit){this.cancelDrag=!0;var u=t.container;u.draggable=!0}this.setState("dragWait")}else this.startDrag();this.captureMouse(e,this.onMouseDrag.bind(this)),e.defaultPrevented=!0}}}).call(f.prototype),t.DragdropHandler=f}),ace.define("ace/lib/net",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./dom");t.get=function(e,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&t(n.responseText)},n.send(null)},t.loadScript=function(e,t){var n=r.getDocumentHead(),i=document.createElement("script");i.src=e,n.appendChild(i),i.onload=i.onreadystatechange=function(e,n){if(n||!i.readyState||i.readyState=="loaded"||i.readyState=="complete")i=i.onload=i.onreadystatechange=null,n||t()}},t.qualifyURL=function(e){var t=document.createElement("a");return t.href=e,t.href}}),ace.define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;o1&&(i=n[n.length-2]);var o=a[t+"Path"];return o==null?o=a.basePath:r=="/"&&(t=r=""),o&&o.slice(-1)!="/"&&(o+="/"),o+t+r+i+this.get("suffix")},t.setModuleUrl=function(e,t){return a.$moduleUrls[e]=t},t.$loading={},t.loadModule=function(n,r){var i,o;Array.isArray(n)&&(o=n[0],n=n[1]);try{i=e(n)}catch(u){}if(i&&!t.$loading[n])return r&&r(i);t.$loading[n]||(t.$loading[n]=[]),t.$loading[n].push(r);if(t.$loading[n].length>1)return;var a=function(){e([n],function(e){t._emit("load.module",{name:n,module:e});var r=t.$loading[n];t.$loading[n]=null,r.forEach(function(t){t&&t(e)})})};if(!t.get("packaged"))return a();s.loadScript(t.moduleUrl(n,o),a),f()};var f=function(){!a.basePath&&!a.workerPath&&!a.modePath&&!a.themePath&&!Object.keys(a.$moduleUrls).length&&(console.error("Unable to infer path to ace from script src,","use ace.config.set('basePath', 'path') to enable dynamic loading of modes and themes","or with webpack use ace/webpack-resolver"),f=function(){})};t.init=l}),ace.define("ace/mouse/mouse_handler",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/mouse/default_handlers","ace/mouse/default_gutter_handler","ace/mouse/mouse_event","ace/mouse/dragdrop_handler","ace/config"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("./default_handlers").DefaultHandlers,o=e("./default_gutter_handler").GutterHandler,u=e("./mouse_event").MouseEvent,a=e("./dragdrop_handler").DragdropHandler,f=e("../config"),l=function(e){var t=this;this.editor=e,new s(this),new o(this),new a(this);var n=function(t){var n=!document.hasFocus||!document.hasFocus()||!e.isFocused()&&document.activeElement==(e.textInput&&e.textInput.getElement());n&&window.focus(),e.focus()},u=e.renderer.getMouseEventTarget();r.addListener(u,"click",this.onMouseEvent.bind(this,"click")),r.addListener(u,"mousemove",this.onMouseMove.bind(this,"mousemove")),r.addMultiMouseDownListener([u,e.renderer.scrollBarV&&e.renderer.scrollBarV.inner,e.renderer.scrollBarH&&e.renderer.scrollBarH.inner,e.textInput&&e.textInput.getElement()].filter(Boolean),[400,300,250],this,"onMouseEvent"),r.addMouseWheelListener(e.container,this.onMouseWheel.bind(this,"mousewheel")),r.addTouchMoveListener(e.container,this.onTouchMove.bind(this,"touchmove"));var f=e.renderer.$gutter;r.addListener(f,"mousedown",this.onMouseEvent.bind(this,"guttermousedown")),r.addListener(f,"click",this.onMouseEvent.bind(this,"gutterclick")),r.addListener(f,"dblclick",this.onMouseEvent.bind(this,"gutterdblclick")),r.addListener(f,"mousemove",this.onMouseEvent.bind(this,"guttermousemove")),r.addListener(u,"mousedown",n),r.addListener(f,"mousedown",n),i.isIE&&e.renderer.scrollBarV&&(r.addListener(e.renderer.scrollBarV.element,"mousedown",n),r.addListener(e.renderer.scrollBarH.element,"mousedown",n)),e.on("mousemove",function(n){if(t.state||t.$dragDelay||!t.$dragEnabled)return;var r=e.renderer.screenToTextCoordinates(n.x,n.y),i=e.session.selection.getRange(),s=e.renderer;!i.isEmpty()&&i.insideStart(r.row,r.column)?s.setCursorStyle("default"):s.setCursorStyle("")})};(function(){this.onMouseEvent=function(e,t){this.editor._emit(e,new u(t,this.editor))},this.onMouseMove=function(e,t){var n=this.editor._eventRegistry&&this.editor._eventRegistry.mousemove;if(!n||!n.length)return;this.editor._emit(e,new u(t,this.editor))},this.onMouseWheel=function(e,t){var n=new u(t,this.editor);n.speed=this.$scrollSpeed*2,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.onTouchMove=function(e,t){var n=new u(t,this.editor);n.speed=1,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.setState=function(e){this.state=e},this.captureMouse=function(e,t){this.x=e.x,this.y=e.y,this.isMousePressed=!0;var n=this.editor,s=this.editor.renderer;s.$keepTextAreaAtCursor&&(s.$keepTextAreaAtCursor=null);var o=this,a=function(e){if(!e)return;if(i.isWebKit&&!e.which&&o.releaseMouse)return o.releaseMouse();o.x=e.clientX,o.y=e.clientY,t&&t(e),o.mouseEvent=new u(e,o.editor),o.$mouseMoved=!0},f=function(e){n.off("beforeEndOperation",c),clearInterval(h),l(),o[o.state+"End"]&&o[o.state+"End"](e),o.state="",s.$keepTextAreaAtCursor==null&&(s.$keepTextAreaAtCursor=!0,s.$moveTextAreaToCursor()),o.isMousePressed=!1,o.$onCaptureMouseMove=o.releaseMouse=null,e&&o.onMouseEvent("mouseup",e),n.endOperation()},l=function(){o[o.state]&&o[o.state](),o.$mouseMoved=!1};if(i.isOldIE&&e.domEvent.type=="dblclick")return setTimeout(function(){f(e)});var c=function(e){if(!o.releaseMouse)return;n.curOp.command.name&&n.curOp.selectionChanged&&(o[o.state+"End"]&&o[o.state+"End"](),o.state="",o.releaseMouse())};n.on("beforeEndOperation",c),n.startOperation({command:{name:"mouse"}}),o.$onCaptureMouseMove=a,o.releaseMouse=r.capture(this.editor.container,a,f);var h=setInterval(l,20)},this.releaseMouse=null,this.cancelContextMenu=function(){var e=function(t){if(t&&t.domEvent&&t.domEvent.type!="contextmenu")return;this.editor.off("nativecontextmenu",e),t&&t.domEvent&&r.stopEvent(t.domEvent)}.bind(this);setTimeout(e,10),this.editor.on("nativecontextmenu",e)}}).call(l.prototype),f.defineOptions(l.prototype,"mouseHandler",{scrollSpeed:{initialValue:2},dragDelay:{initialValue:i.isMac?150:0},dragEnabled:{initialValue:!0},focusTimeout:{initialValue:0},tooltipFollowsMouse:{initialValue:!0}}),t.MouseHandler=l}),ace.define("ace/mouse/fold_handler",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";function i(e){e.on("click",function(t){var n=t.getDocumentPosition(),i=e.session,s=i.getFoldAt(n.row,n.column,1);s&&(t.getAccelKey()?i.removeFold(s):i.expandFold(s),t.stop());var o=t.domEvent&&t.domEvent.target;o&&r.hasCssClass(o,"ace_inline_button")&&r.hasCssClass(o,"ace_toggle_wrap")&&(i.setOption("wrap",!0),e.renderer.scrollCursorIntoView())}),e.on("gutterclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session;i.foldWidgets&&i.foldWidgets[r]&&e.session.onFoldWidgetClick(r,t),e.isFocused()||e.focus(),t.stop()}}),e.on("gutterdblclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session,s=i.getParentFoldRangeData(r,!0),o=s.range||s.firstRange;if(o){r=o.start.row;var u=i.getFoldAt(r,i.getLine(r).length,1);u?i.removeFold(u):(i.addFold("...",o),e.renderer.scrollCursorIntoView({row:o.start.row,column:0}))}t.stop()}})}var r=e("../lib/dom");t.FoldHandler=i}),ace.define("ace/keyboard/keybinding",["require","exports","module","ace/lib/keys","ace/lib/event"],function(e,t,n){"use strict";var r=e("../lib/keys"),i=e("../lib/event"),s=function(e){this.$editor=e,this.$data={editor:e},this.$handlers=[],this.setDefaultHandler(e.commands)};(function(){this.setDefaultHandler=function(e){this.removeKeyboardHandler(this.$defaultHandler),this.$defaultHandler=e,this.addKeyboardHandler(e,0)},this.setKeyboardHandler=function(e){var t=this.$handlers;if(t[t.length-1]==e)return;while(t[t.length-1]&&t[t.length-1]!=this.$defaultHandler)this.removeKeyboardHandler(t[t.length-1]);this.addKeyboardHandler(e,1)},this.addKeyboardHandler=function(e,t){if(!e)return;typeof e=="function"&&!e.handleKeyboard&&(e.handleKeyboard=e);var n=this.$handlers.indexOf(e);n!=-1&&this.$handlers.splice(n,1),t==undefined?this.$handlers.push(e):this.$handlers.splice(t,0,e),n==-1&&e.attach&&e.attach(this.$editor)},this.removeKeyboardHandler=function(e){var t=this.$handlers.indexOf(e);return t==-1?!1:(this.$handlers.splice(t,1),e.detach&&e.detach(this.$editor),!0)},this.getKeyboardHandler=function(){return this.$handlers[this.$handlers.length-1]},this.getStatusText=function(){var e=this.$data,t=e.editor;return this.$handlers.map(function(n){return n.getStatusText&&n.getStatusText(t,e)||""}).filter(Boolean).join(" ")},this.$callKeyboardHandlers=function(e,t,n,r){var s,o=!1,u=this.$editor.commands;for(var a=this.$handlers.length;a--;){s=this.$handlers[a].handleKeyboard(this.$data,e,t,n,r);if(!s||!s.command)continue;s.command=="null"?o=!0:o=u.exec(s.command,this.$editor,s.args,r),o&&r&&e!=-1&&s.passEvent!=1&&s.command.passEvent!=1&&i.stopEvent(r);if(o)break}return!o&&e==-1&&(s={command:"insertstring"},o=u.exec("insertstring",this.$editor,t)),o&&this.$editor._signal&&this.$editor._signal("keyboardActivity",s),o},this.onCommandKey=function(e,t,n){var i=r.keyCodeToString(n);this.$callKeyboardHandlers(t,i,n,e)},this.onTextInput=function(e){this.$callKeyboardHandlers(-1,e)}}).call(s.prototype),t.KeyBinding=s}),ace.define("ace/lib/bidiutil",["require","exports","module"],function(e,t,n){"use strict";function F(e,t,n,r){var i=s?d:p,c=null,h=null,v=null,m=0,g=null,y=null,b=-1,w=null,E=null,T=[];if(!r)for(w=0,r=[];w0)if(g==16){for(w=b;w-1){for(w=b;w=0;C--){if(r[C]!=N)break;t[C]=s}}}function I(e,t,n){if(o=e){u=i+1;while(u=e)u++;for(a=i,l=u-1;a=t.length||(o=n[r-1])!=b&&o!=w||(c=t[r+1])!=b&&c!=w)return E;return u&&(c=w),c==o?c:E;case k:o=r>0?n[r-1]:S;if(o==b&&r+10&&n[r-1]==b)return b;if(u)return E;p=r+1,h=t.length;while(p=1425&&d<=2303||d==64286;o=t[p];if(v&&(o==y||o==T))return y}if(r<1||(o=t[r-1])==S)return E;return n[r-1];case S:return u=!1,f=!0,s;case x:return l=!0,E;case O:case M:case D:case P:case _:u=!1;case H:return E}}function R(e){var t=e.charCodeAt(0),n=t>>8;return n==0?t>191?g:B[t]:n==5?/[\u0591-\u05f4]/.test(e)?y:g:n==6?/[\u0610-\u061a\u064b-\u065f\u06d6-\u06e4\u06e7-\u06ed]/.test(e)?A:/[\u0660-\u0669\u066b-\u066c]/.test(e)?w:t==1642?L:/[\u06f0-\u06f9]/.test(e)?b:T:n==32&&t<=8287?j[t&255]:n==254?t>=65136?T:E:E}function U(e){return e>="\u064b"&&e<="\u0655"}var r=["\u0621","\u0641"],i=["\u063a","\u064a"],s=0,o=0,u=!1,a=!1,f=!1,l=!1,c=!1,h=!1,p=[[0,3,0,1,0,0,0],[0,3,0,1,2,2,0],[0,3,0,17,2,0,1],[0,3,5,5,4,1,0],[0,3,21,21,4,0,1],[0,3,5,5,4,2,0]],d=[[2,0,1,1,0,1,0],[2,0,1,1,0,2,0],[2,0,2,1,3,2,0],[2,0,2,33,3,1,1]],v=0,m=1,g=0,y=1,b=2,w=3,E=4,S=5,x=6,T=7,N=8,C=9,k=10,L=11,A=12,O=13,M=14,_=15,D=16,P=17,H=18,B=[H,H,H,H,H,H,H,H,H,x,S,x,N,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,S,S,S,x,N,E,E,L,L,L,E,E,E,E,E,k,C,k,C,C,b,b,b,b,b,b,b,b,b,b,C,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,H,H,H,H,H,H,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,C,E,L,L,L,L,E,E,E,E,g,E,E,H,E,E,L,L,b,b,E,g,E,E,E,b,g,E,E,E,E,E],j=[N,N,N,N,N,N,N,N,N,N,N,H,H,H,g,y,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N,S,O,M,_,D,P,C,L,L,L,L,L,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,C,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N];t.L=g,t.R=y,t.EN=b,t.ON_R=3,t.AN=4,t.R_H=5,t.B=6,t.RLE=7,t.DOT="\u00b7",t.doBidiReorder=function(e,n,r){if(e.length<2)return{};var i=e.split(""),o=new Array(i.length),u=new Array(i.length),a=[];s=r?m:v,F(i,a,i.length,n);for(var f=0;fT&&n[f]0&&i[f-1]==="\u0644"&&/\u0622|\u0623|\u0625|\u0627/.test(i[f])&&(a[f-1]=a[f]=t.R_H,f++);i[i.length-1]===t.DOT&&(a[i.length-1]=t.B),i[0]==="\u202b"&&(a[0]=t.RLE);for(var f=0;f=0&&(e=this.session.$docRowCache[n])}return e},this.getSplitIndex=function(){var e=0,t=this.session.$screenRowCache;if(t.length){var n,r=this.session.$getRowCacheIndex(t,this.currentRow);while(this.currentRow-e>0){n=this.session.$getRowCacheIndex(t,this.currentRow-e-1);if(n!==r)break;r=n,e++}}else e=this.currentRow;return e},this.updateRowLine=function(e,t){e===undefined&&(e=this.getDocumentRow());var n=e===this.session.getLength()-1,s=n?this.EOF:this.EOL;this.wrapIndent=0,this.line=this.session.getLine(e),this.isRtlDir=this.line.charAt(0)===this.RLE;if(this.session.$useWrapMode){var o=this.session.$wrapData[e];o&&(t===undefined&&(t=this.getSplitIndex()),t>0&&o.length?(this.wrapIndent=o.indent,this.wrapOffset=this.wrapIndent*this.charWidths[r.L],this.line=tt?this.session.getOverwrite()?e:e-1:t,i=r.getVisualFromLogicalIdx(n,this.bidiMap),s=this.bidiMap.bidiLevels,o=0;!this.session.getOverwrite()&&e<=t&&s[i]%2!==0&&i++;for(var u=0;ut&&s[i]%2===0&&(o+=this.charWidths[s[i]]),this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset),this.isRtlDir&&(o+=this.rtlLineOffset),o},this.getSelections=function(e,t){var n=this.bidiMap,r=n.bidiLevels,i,s=[],o=0,u=Math.min(e,t)-this.wrapIndent,a=Math.max(e,t)-this.wrapIndent,f=!1,l=!1,c=0;this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset);for(var h,p=0;p=u&&hn+s/2){n+=s;if(r===i.length-1){s=0;break}s=this.charWidths[i[++r]]}return r>0&&i[r-1]%2!==0&&i[r]%2===0?(e0&&i[r-1]%2===0&&i[r]%2!==0?t=1+(e>n?this.bidiMap.logicalFromVisual[r]:this.bidiMap.logicalFromVisual[r-1]):this.isRtlDir&&r===i.length-1&&s===0&&i[r-1]%2===0||!this.isRtlDir&&r===0&&i[r]%2!==0?t=1+this.bidiMap.logicalFromVisual[r]:(r>0&&i[r-1]%2!==0&&s!==0&&r--,t=this.bidiMap.logicalFromVisual[r]),t===0&&this.isRtlDir&&t++,t+this.wrapIndent}}).call(o.prototype),t.BidiHandler=o}),ace.define("ace/selection",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/range"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=function(e){this.session=e,this.doc=e.getDocument(),this.clearSelection(),this.cursor=this.lead=this.doc.createAnchor(0,0),this.anchor=this.doc.createAnchor(0,0),this.$silent=!1;var t=this;this.cursor.on("change",function(e){t.$cursorChanged=!0,t.$silent||t._emit("changeCursor"),!t.$isEmpty&&!t.$silent&&t._emit("changeSelection"),!t.$keepDesiredColumnOnChange&&e.old.column!=e.value.column&&(t.$desiredColumn=null)}),this.anchor.on("change",function(){t.$anchorChanged=!0,!t.$isEmpty&&!t.$silent&&t._emit("changeSelection")})};(function(){r.implement(this,s),this.isEmpty=function(){return this.$isEmpty||this.anchor.row==this.lead.row&&this.anchor.column==this.lead.column},this.isMultiLine=function(){return!this.$isEmpty&&this.anchor.row!=this.cursor.row},this.getCursor=function(){return this.lead.getPosition()},this.setSelectionAnchor=function(e,t){this.$isEmpty=!1,this.anchor.setPosition(e,t)},this.getAnchor=this.getSelectionAnchor=function(){return this.$isEmpty?this.getSelectionLead():this.anchor.getPosition()},this.getSelectionLead=function(){return this.lead.getPosition()},this.isBackwards=function(){var e=this.anchor,t=this.lead;return e.row>t.row||e.row==t.row&&e.column>t.column},this.getRange=function(){var e=this.anchor,t=this.lead;return this.$isEmpty?o.fromPoints(t,t):this.isBackwards()?o.fromPoints(t,e):o.fromPoints(e,t)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._emit("changeSelection"))},this.selectAll=function(){this.$setSelection(0,0,Number.MAX_VALUE,Number.MAX_VALUE)},this.setRange=this.setSelectionRange=function(e,t){var n=t?e.end:e.start,r=t?e.start:e.end;this.$setSelection(n.row,n.column,r.row,r.column)},this.$setSelection=function(e,t,n,r){var i=this.$isEmpty,s=this.inMultiSelectMode;this.$silent=!0,this.$cursorChanged=this.$anchorChanged=!1,this.anchor.setPosition(e,t),this.cursor.setPosition(n,r),this.$isEmpty=!o.comparePoints(this.anchor,this.cursor),this.$silent=!1,this.$cursorChanged&&this._emit("changeCursor"),(this.$cursorChanged||this.$anchorChanged||i!=this.$isEmpty||s)&&this._emit("changeSelection")},this.$moveSelection=function(e){var t=this.lead;this.$isEmpty&&this.setSelectionAnchor(t.row,t.column),e.call(this)},this.selectTo=function(e,t){this.$moveSelection(function(){this.moveCursorTo(e,t)})},this.selectToPosition=function(e){this.$moveSelection(function(){this.moveCursorToPosition(e)})},this.moveTo=function(e,t){this.clearSelection(),this.moveCursorTo(e,t)},this.moveToPosition=function(e){this.clearSelection(),this.moveCursorToPosition(e)},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.getWordRange=function(e,t){if(typeof t=="undefined"){var n=e||this.lead;e=n.row,t=n.column}return this.session.getWordRange(e,t)},this.selectWord=function(){this.setSelectionRange(this.getWordRange())},this.selectAWord=function(){var e=this.getCursor(),t=this.session.getAWordRange(e.row,e.column);this.setSelectionRange(t)},this.getLineRange=function(e,t){var n=typeof e=="number"?e:this.lead.row,r,i=this.session.getFoldLine(n);return i?(n=i.start.row,r=i.end.row):r=n,t===!0?new o(n,0,r,this.session.getLine(r).length):new o(n,0,r+1,0)},this.selectLine=function(){this.setSelectionRange(this.getLineRange())},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.wouldMoveIntoSoftTab=function(e,t,n){var r=e.column,i=e.column+t;return n<0&&(r=e.column-t,i=e.column),this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(r,i).split(" ").length-1==t},this.moveCursorLeft=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,-1))this.moveCursorTo(t.start.row,t.start.column);else if(e.column===0)e.row>0&&this.moveCursorTo(e.row-1,this.doc.getLine(e.row-1).length);else{var n=this.session.getTabSize();this.wouldMoveIntoSoftTab(e,n,-1)&&!this.session.getNavigateWithinSoftTabs()?this.moveCursorBy(0,-n):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,1))this.moveCursorTo(t.end.row,t.end.column);else if(this.lead.column==this.doc.getLine(this.lead.row).length)this.lead.row0&&(t.column=r)}}this.moveCursorTo(t.row,t.column)},this.moveCursorFileEnd=function(){var e=this.doc.getLength()-1,t=this.doc.getLine(e).length;this.moveCursorTo(e,t)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorLongWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t);this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var i=this.session.getFoldAt(e,t,1);if(i){this.moveCursorTo(i.end.row,i.end.column);return}this.session.nonTokenRe.exec(r)&&(t+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0,r=n.substring(t));if(t>=n.length){this.moveCursorTo(e,n.length),this.moveCursorRight(),e0&&this.moveCursorWordLeft();return}this.session.tokenRe.exec(s)&&(t-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0),this.moveCursorTo(e,t)},this.$shortWordEndIndex=function(e){var t=0,n,r=/\s/,i=this.session.tokenRe;i.lastIndex=0;if(this.session.tokenRe.exec(e))t=this.session.tokenRe.lastIndex;else{while((n=e[t])&&r.test(n))t++;if(t<1){i.lastIndex=0;while((n=e[t])&&!i.test(n)){i.lastIndex=0,t++;if(r.test(n)){if(t>2){t--;break}while((n=e[t])&&r.test(n))t++;if(t>2)break}}}}return i.lastIndex=0,t},this.moveCursorShortWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i=this.session.getFoldAt(e,t,1);if(i)return this.moveCursorTo(i.end.row,i.end.column);if(t==n.length){var s=this.doc.getLength();do e++,r=this.doc.getLine(e);while(e0&&/^\s*$/.test(r));t=r.length,/\s+$/.test(r)||(r="")}var s=i.stringReverse(r),o=this.$shortWordEndIndex(s);return this.moveCursorTo(e,t-o)},this.moveCursorWordRight=function(){this.session.$selectLongWords?this.moveCursorLongWordRight():this.moveCursorShortWordRight()},this.moveCursorWordLeft=function(){this.session.$selectLongWords?this.moveCursorLongWordLeft():this.moveCursorShortWordLeft()},this.moveCursorBy=function(e,t){var n=this.session.documentToScreenPosition(this.lead.row,this.lead.column),r;t===0&&(e!==0&&(this.session.$bidiHandler.isBidiRow(n.row,this.lead.row)?(r=this.session.$bidiHandler.getPosLeft(n.column),n.column=Math.round(r/this.session.$bidiHandler.charWidths[0])):r=n.column*this.session.$bidiHandler.charWidths[0]),this.$desiredColumn?n.column=this.$desiredColumn:this.$desiredColumn=n.column);var i=this.session.screenToDocumentPosition(n.row+e,n.column,r);e!==0&&t===0&&i.row===this.lead.row&&i.column===this.lead.column&&this.session.lineWidgets&&this.session.lineWidgets[i.row]&&(i.row>0||e>0)&&i.row++,this.moveCursorTo(i.row,i.column+t,t===0)},this.moveCursorToPosition=function(e){this.moveCursorTo(e.row,e.column)},this.moveCursorTo=function(e,t,n){var r=this.session.getFoldAt(e,t,1);r&&(e=r.start.row,t=r.start.column),this.$keepDesiredColumnOnChange=!0;var i=this.session.getLine(e);/[\uDC00-\uDFFF]/.test(i.charAt(t))&&i.charAt(t-1)&&(this.lead.row==e&&this.lead.column==t+1?t-=1:t+=1),this.lead.setPosition(e,t),this.$keepDesiredColumnOnChange=!1,n||(this.$desiredColumn=null)},this.moveCursorToScreen=function(e,t,n){var r=this.session.screenToDocumentPosition(e,t);this.moveCursorTo(r.row,r.column,n)},this.detach=function(){this.lead.detach(),this.anchor.detach(),this.session=this.doc=null},this.fromOrientedRange=function(e){this.setSelectionRange(e,e.cursor==e.start),this.$desiredColumn=e.desiredColumn||this.$desiredColumn},this.toOrientedRange=function(e){var t=this.getRange();return e?(e.start.column=t.start.column,e.start.row=t.start.row,e.end.column=t.end.column,e.end.row=t.end.row):e=t,e.cursor=this.isBackwards()?e.start:e.end,e.desiredColumn=this.$desiredColumn,e},this.getRangeOfMovements=function(e){var t=this.getCursor();try{e(this);var n=this.getCursor();return o.fromPoints(t,n)}catch(r){return o.fromPoints(t,t)}finally{this.moveCursorToPosition(t)}},this.toJSON=function(){if(this.rangeCount)var e=this.ranges.map(function(e){var t=e.clone();return t.isBackwards=e.cursor==e.start,t});else{var e=this.getRange();e.isBackwards=this.isBackwards()}return e},this.fromJSON=function(e){if(e.start==undefined){if(this.rangeList){this.toSingleRange(e[0]);for(var t=e.length;t--;){var n=o.fromPoints(e[t].start,e[t].end);e[t].isBackwards&&(n.cursor=n.start),this.addRange(n,!0)}return}e=e[0]}this.rangeList&&this.toSingleRange(e),this.setSelectionRange(e,e.isBackwards)},this.isEqual=function(e){if((e.length||this.rangeCount)&&e.length!=this.rangeCount)return!1;if(!e.length||!this.ranges)return this.getRange().isEqual(e);for(var t=this.ranges.length;t--;)if(!this.ranges[t].isEqual(e[t]))return!1;return!0}}).call(u.prototype),t.Selection=u}),ace.define("ace/tokenizer",["require","exports","module","ace/config"],function(e,t,n){"use strict";var r=e("./config"),i=2e3,s=function(e){this.states=e,this.regExps={},this.matchMappings={};for(var t in this.states){var n=this.states[t],r=[],i=0,s=this.matchMappings[t]={defaultToken:"text"},o="g",u=[];for(var a=0;a1?f.onMatch=this.$applyToken:f.onMatch=f.token),c>1&&(/\\\d/.test(f.regex)?l=f.regex.replace(/\\([0-9]+)/g,function(e,t){return"\\"+(parseInt(t,10)+i+1)}):(c=1,l=this.removeCapturingGroups(f.regex)),!f.splitRegex&&typeof f.token!="string"&&u.push(f)),s[i]=a,i+=c,r.push(l),f.onMatch||(f.onMatch=null)}r.length||(s[0]=0,r.push("$")),u.forEach(function(e){e.splitRegex=this.createSplitterRegexp(e.regex,o)},this),this.regExps[t]=new RegExp("("+r.join(")|(")+")|($)",o)}};(function(){this.$setMaxTokenCount=function(e){i=e|0},this.$applyToken=function(e){var t=this.splitRegex.exec(e).slice(1),n=this.token.apply(this,t);if(typeof n=="string")return[{type:n,value:e}];var r=[];for(var i=0,s=n.length;il){var g=e.substring(l,m-v.length);h.type==p?h.value+=g:(h.type&&f.push(h),h={type:p,value:g})}for(var y=0;yi){c>2*e.length&&this.reportError("infinite loop with in ace tokenizer",{startState:t,line:e});while(l1&&n[0]!==r&&n.unshift("#tmp",r),{tokens:f,state:n.length?n:r}},this.reportError=r.reportError}).call(s.prototype),t.Tokenizer=s}),ace.define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang"),i=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{defaultToken:"text"}]}};(function(){this.addRules=function(e,t){if(!t){for(var n in e)this.$rules[n]=e[n];return}for(var n in e){var r=e[n];for(var i=0;i=this.$rowTokens.length){this.$row+=1,e||(e=this.$session.getLength());if(this.$row>=e)return this.$row=e-1,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=0}return this.$rowTokens[this.$tokenIndex]},this.getCurrentToken=function(){return this.$rowTokens[this.$tokenIndex]},this.getCurrentTokenRow=function(){return this.$row},this.getCurrentTokenColumn=function(){var e=this.$rowTokens,t=this.$tokenIndex,n=e[t].start;if(n!==undefined)return n;n=0;while(t>0)t-=1,n+=e[t].value.length;return n},this.getCurrentTokenPosition=function(){return{row:this.$row,column:this.getCurrentTokenColumn()}},this.getCurrentTokenRange=function(){var e=this.$rowTokens[this.$tokenIndex],t=this.getCurrentTokenColumn();return new r(this.$row,t,this.$row,t+e.value.length)}}).call(i.prototype),t.TokenIterator=i}),ace.define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c={'"':'"',"'":"'"},h=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},p=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},d=function(e){this.add("braces","insertion",function(t,n,r,i,s){var u=r.getCursorPosition(),a=i.doc.getLine(u.row);if(s=="{"){h(r);var l=r.getSelectionRange(),c=i.doc.getTextRange(l);if(c!==""&&c!=="{"&&r.getWrapBehavioursEnabled())return p(l,c,"{","}");if(d.isSaneInsertion(r,i))return/[\]\}\)]/.test(a[u.column])||r.inMultiSelectMode||e&&e.braces?(d.recordAutoInsert(r,i,"}"),{text:"{}",selection:[1,1]}):(d.recordMaybeInsert(r,i,"{"),{text:"{",selection:[1,1]})}else if(s=="}"){h(r);var v=a.substring(u.column,u.column+1);if(v=="}"){var m=i.$findOpeningBracket("}",{column:u.column+1,row:u.row});if(m!==null&&d.isAutoInsertedClosing(u,a,s))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(s=="\n"||s=="\r\n"){h(r);var g="";d.isMaybeInsertedClosing(u,a)&&(g=o.stringRepeat("}",f.maybeInsertedBrackets),d.clearMaybeInsertedClosing());var v=a.substring(u.column,u.column+1);if(v==="}"){var y=i.findMatchingBracket({row:u.row,column:u.column+1},"}");if(!y)return null;var b=this.$getIndent(i.getLine(y.row))}else{if(!g){d.clearMaybeInsertedClosing();return}var b=this.$getIndent(a)}var w=b+i.getTabString();return{text:"\n"+w+"\n"+b+g,selection:[1,w.length,1,w.length]}}d.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"(",")");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"[","]");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){var s=r.$mode.$quotes||c;if(i.length==1&&s[i]){if(this.lineCommentStart&&this.lineCommentStart.indexOf(i)!=-1)return;h(n);var o=i,u=n.getSelectionRange(),a=r.doc.getTextRange(u);if(a!==""&&(a.length!=1||!s[a])&&n.getWrapBehavioursEnabled())return p(u,a,o,o);if(!a){var f=n.getCursorPosition(),l=r.doc.getLine(f.row),d=l.substring(f.column-1,f.column),v=l.substring(f.column,f.column+1),m=r.getTokenAt(f.row,f.column),g=r.getTokenAt(f.row,f.column+1);if(d=="\\"&&m&&/escape/.test(m.type))return null;var y=m&&/string|escape/.test(m.type),b=!g||/string|escape/.test(g.type),w;if(v==o)w=y!==b,w&&/string\.end/.test(g.type)&&(w=!1);else{if(y&&!b)return null;if(y&&b)return null;var E=r.$mode.tokenRe;E.lastIndex=0;var S=E.test(d);E.lastIndex=0;var x=E.test(d);if(S||x)return null;if(v&&!/[\s;,.})\]\\]/.test(v))return null;w=!0}return{text:w?o+o:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.$mode.$quotes||c,o=r.doc.getTextRange(i);if(!i.isMultiLine()&&s.hasOwnProperty(o)){h(n);var u=r.doc.getLine(i.start.row),a=u.substring(i.start.column+1,i.start.column+2);if(a==o)return i.end.column++,i}})};d.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},d.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},d.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},d.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},d.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},d.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},d.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},d.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(d,i),t.CstyleBehaviour=d}),ace.define("ace/unicode",["require","exports","module"],function(e,t,n){"use strict";var r=[48,9,8,25,5,0,2,25,48,0,11,0,5,0,6,22,2,30,2,457,5,11,15,4,8,0,2,0,18,116,2,1,3,3,9,0,2,2,2,0,2,19,2,82,2,138,2,4,3,155,12,37,3,0,8,38,10,44,2,0,2,1,2,1,2,0,9,26,6,2,30,10,7,61,2,9,5,101,2,7,3,9,2,18,3,0,17,58,3,100,15,53,5,0,6,45,211,57,3,18,2,5,3,11,3,9,2,1,7,6,2,2,2,7,3,1,3,21,2,6,2,0,4,3,3,8,3,1,3,3,9,0,5,1,2,4,3,11,16,2,2,5,5,1,3,21,2,6,2,1,2,1,2,1,3,0,2,4,5,1,3,2,4,0,8,3,2,0,8,15,12,2,2,8,2,2,2,21,2,6,2,1,2,4,3,9,2,2,2,2,3,0,16,3,3,9,18,2,2,7,3,1,3,21,2,6,2,1,2,4,3,8,3,1,3,2,9,1,5,1,2,4,3,9,2,0,17,1,2,5,4,2,2,3,4,1,2,0,2,1,4,1,4,2,4,11,5,4,4,2,2,3,3,0,7,0,15,9,18,2,2,7,2,2,2,22,2,9,2,4,4,7,2,2,2,3,8,1,2,1,7,3,3,9,19,1,2,7,2,2,2,22,2,9,2,4,3,8,2,2,2,3,8,1,8,0,2,3,3,9,19,1,2,7,2,2,2,22,2,15,4,7,2,2,2,3,10,0,9,3,3,9,11,5,3,1,2,17,4,23,2,8,2,0,3,6,4,0,5,5,2,0,2,7,19,1,14,57,6,14,2,9,40,1,2,0,3,1,2,0,3,0,7,3,2,6,2,2,2,0,2,0,3,1,2,12,2,2,3,4,2,0,2,5,3,9,3,1,35,0,24,1,7,9,12,0,2,0,2,0,5,9,2,35,5,19,2,5,5,7,2,35,10,0,58,73,7,77,3,37,11,42,2,0,4,328,2,3,3,6,2,0,2,3,3,40,2,3,3,32,2,3,3,6,2,0,2,3,3,14,2,56,2,3,3,66,5,0,33,15,17,84,13,619,3,16,2,25,6,74,22,12,2,6,12,20,12,19,13,12,2,2,2,1,13,51,3,29,4,0,5,1,3,9,34,2,3,9,7,87,9,42,6,69,11,28,4,11,5,11,11,39,3,4,12,43,5,25,7,10,38,27,5,62,2,28,3,10,7,9,14,0,89,75,5,9,18,8,13,42,4,11,71,55,9,9,4,48,83,2,2,30,14,230,23,280,3,5,3,37,3,5,3,7,2,0,2,0,2,0,2,30,3,52,2,6,2,0,4,2,2,6,4,3,3,5,5,12,6,2,2,6,67,1,20,0,29,0,14,0,17,4,60,12,5,0,4,11,18,0,5,0,3,9,2,0,4,4,7,0,2,0,2,0,2,3,2,10,3,3,6,4,5,0,53,1,2684,46,2,46,2,132,7,6,15,37,11,53,10,0,17,22,10,6,2,6,2,6,2,6,2,6,2,6,2,6,2,6,2,31,48,0,470,1,36,5,2,4,6,1,5,85,3,1,3,2,2,89,2,3,6,40,4,93,18,23,57,15,513,6581,75,20939,53,1164,68,45,3,268,4,27,21,31,3,13,13,1,2,24,9,69,11,1,38,8,3,102,3,1,111,44,25,51,13,68,12,9,7,23,4,0,5,45,3,35,13,28,4,64,15,10,39,54,10,13,3,9,7,22,4,1,5,66,25,2,227,42,2,1,3,9,7,11171,13,22,5,48,8453,301,3,61,3,105,39,6,13,4,6,11,2,12,2,4,2,0,2,1,2,1,2,107,34,362,19,63,3,53,41,11,5,15,17,6,13,1,25,2,33,4,2,134,20,9,8,25,5,0,2,25,12,88,4,5,3,5,3,5,3,2],i=0,s=[];for(var o=0;o2?r%f!=f-1:r%f==0}}var E=Infinity;w(function(e,t){var n=e.search(/\S/);n!==-1?(ne.length&&(E=e.length)}),u==Infinity&&(u=E,s=!1,o=!1),l&&u%f!=0&&(u=Math.floor(u/f)*f),w(o?m:v)},this.toggleBlockComment=function(e,t,n,r){var i=this.blockComment;if(!i)return;!i.start&&i[0]&&(i=i[0]);var s=new f(t,r.row,r.column),o=s.getCurrentToken(),u=t.selection,a=t.selection.toOrientedRange(),c,h;if(o&&/comment/.test(o.type)){var p,d;while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.start);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;p=new l(m,g,m,g+i.start.length);break}o=s.stepBackward()}var s=new f(t,r.row,r.column),o=s.getCurrentToken();while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.end);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;d=new l(m,g,m,g+i.end.length);break}o=s.stepForward()}d&&t.remove(d),p&&(t.remove(p),c=p.start.row,h=-i.start.length)}else h=i.start.length,c=n.start.row,t.insert(n.end,i.end),t.insert(n.start,i.start);a.start.row==c&&(a.start.column+=h),a.end.row==c&&(a.end.column+=h),t.selection.fromOrientedRange(a)},this.getNextLineIndent=function(e,t,n){return this.$getIndent(t)},this.checkOutdent=function(e,t,n){return!1},this.autoOutdent=function(e,t,n){},this.$getIndent=function(e){return e.match(/^\s*/)[0]},this.createWorker=function(e){return null},this.createModeDelegates=function(e){this.$embeds=[],this.$modes={};for(var t in e)if(e[t]){var n=e[t],i=n.prototype.$id,s=r.$modes[i];s||(r.$modes[i]=s=new n),r.$modes[t]||(r.$modes[t]=s),this.$embeds.push(t),this.$modes[t]=s}var o=["toggleBlockComment","toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction","getCompletions"];for(var t=0;t=0&&t.row=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),ace.define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=t.Anchor=function(e,t,n){this.$onChange=this.onChange.bind(this),this.attach(e),typeof n=="undefined"?this.setPosition(t.row,t.column):this.setPosition(t,n)};(function(){function e(e,t,n){var r=n?e.column<=t.column:e.columnthis.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e0,r=t=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4?this.$splitAndapplyLargeDelta(e,2e4):(i(this.$lines,e,t),this._signal("change",e))},this.$splitAndapplyLargeDelta=function(e,t){var n=e.lines,r=n.length-t+1,i=e.start.row,s=e.start.column;for(var o=0,u=0;o20){n.running=setTimeout(n.$worker,20);break}}n.currentLine=t,r==-1&&(r=t),s<=r&&n.fireUpdateEvent(s,r)}};(function(){r.implement(this,i),this.setTokenizer=function(e){this.tokenizer=e,this.lines=[],this.states=[],this.start(0)},this.setDocument=function(e){this.doc=e,this.lines=[],this.states=[],this.stop()},this.fireUpdateEvent=function(e,t){var n={first:e,last:t};this._signal("update",{data:n})},this.start=function(e){this.currentLine=Math.min(e||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.states.splice(this.currentLine,this.states.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.scheduleStart=function(){this.running||(this.running=setTimeout(this.$worker,700))},this.$updateOnChange=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.lines[t]=null;else if(e.action=="remove")this.lines.splice(t,n+1,null),this.states.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.lines.splice.apply(this.lines,r),this.states.splice.apply(this.states,r)}this.currentLine=Math.min(t,this.currentLine,this.doc.getLength()),this.stop()},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(e){return this.lines[e]||this.$tokenizeRow(e)},this.getState=function(e){return this.currentLine==e&&this.$tokenizeRow(e),this.states[e]||"start"},this.$tokenizeRow=function(e){var t=this.doc.getLine(e),n=this.states[e-1],r=this.tokenizer.getLineTokens(t,n,e);return this.states[e]+""!=r.state+""?(this.states[e]=r.state,this.lines[e+1]=null,this.currentLine>e+1&&(this.currentLine=e+1)):this.currentLine==e&&(this.currentLine=e+1),this.lines[e]=r.tokens}}).call(s.prototype),t.BackgroundTokenizer=s}),ace.define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(e,t,n){this.setRegexp(e),this.clazz=t,this.type=n||"text"};(function(){this.MAX_RANGES=500,this.setRegexp=function(e){if(this.regExp+""==e+"")return;this.regExp=e,this.cache=[]},this.update=function(e,t,n,i){if(!this.regExp)return;var o=i.firstRow,u=i.lastRow;for(var a=o;a<=u;a++){var f=this.cache[a];f==null&&(f=r.getMatchOffsets(n.getLine(a),this.regExp),f.length>this.MAX_RANGES&&(f=f.slice(0,this.MAX_RANGES)),f=f.map(function(e){return new s(a,e.offset,a,e.offset+e.length)}),this.cache[a]=f.length?f:"");for(var l=f.length;l--;)t.drawSingleLineMarker(e,f[l].toScreenRange(n),this.clazz,i)}}}).call(o.prototype),t.SearchHighlight=o}),ace.define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){this.foldData=e,Array.isArray(t)?this.folds=t:t=this.folds=[t];var n=t[t.length-1];this.range=new r(t[0].start.row,t[0].start.column,n.end.row,n.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(e){e.setFoldLine(this)},this)}var r=e("../range").Range;(function(){this.shiftRow=function(e){this.start.row+=e,this.end.row+=e,this.folds.forEach(function(t){t.start.row+=e,t.end.row+=e})},this.addFold=function(e){if(e.sameRow){if(e.start.rowthis.endRow)throw new Error("Can't add a fold to this FoldLine as it has no connection");this.folds.push(e),this.folds.sort(function(e,t){return-e.range.compareEnd(t.start.row,t.start.column)}),this.range.compareEnd(e.start.row,e.start.column)>0?(this.end.row=e.end.row,this.end.column=e.end.column):this.range.compareStart(e.end.row,e.end.column)<0&&(this.start.row=e.start.row,this.start.column=e.start.column)}else if(e.start.row==this.end.row)this.folds.push(e),this.end.row=e.end.row,this.end.column=e.end.column;else{if(e.end.row!=this.start.row)throw new Error("Trying to add fold to FoldRow that doesn't have a matching row");this.folds.unshift(e),this.start.row=e.start.row,this.start.column=e.start.column}e.foldLine=this},this.containsRow=function(e){return e>=this.start.row&&e<=this.end.row},this.walk=function(e,t,n){var r=0,i=this.folds,s,o,u,a=!0;t==null&&(t=this.end.row,n=this.end.column);for(var f=0;f0)continue;var a=i(e,o.start);return u===0?t&&a!==0?-s-2:s:a>0||a===0&&!t?s:-s-1}return-s-1},this.add=function(e){var t=!e.isEmpty(),n=this.pointIndex(e.start,t);n<0&&(n=-n-1);var r=this.pointIndex(e.end,t,n);return r<0?r=-r-1:r++,this.ranges.splice(n,r-n,e)},this.addList=function(e){var t=[];for(var n=e.length;n--;)t.push.apply(t,this.add(e[n]));return t},this.substractPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges.splice(t,1)},this.merge=function(){var e=[],t=this.ranges;t=t.sort(function(e,t){return i(e.start,t.start)});var n=t[0],r;for(var s=1;s=0},this.containsPoint=function(e){return this.pointIndex(e)>=0},this.rangeAtPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges[t]},this.clipRows=function(e,t){var n=this.ranges;if(n[0].start.row>t||n[n.length-1].start.row=r)break}if(e.action=="insert"){var f=i-r,l=-t.column+n.column;for(;or)break;a.start.row==r&&a.start.column>=t.column&&(a.start.column!=t.column||!this.$insertRight)&&(a.start.column+=l,a.start.row+=f);if(a.end.row==r&&a.end.column>=t.column){if(a.end.column==t.column&&this.$insertRight)continue;a.end.column==t.column&&l>0&&oa.start.column&&a.end.column==s[o+1].start.column&&(a.end.column-=l),a.end.column+=l,a.end.row+=f}}}else{var f=r-i,l=t.column-n.column;for(;oi)break;if(a.end.rowt.column)a.end.column=t.column,a.end.row=t.row}else a.end.column+=l,a.end.row+=f;else a.end.row>i&&(a.end.row+=f);if(a.start.rowt.column)a.start.column=t.column,a.start.row=t.row}else a.start.column+=l,a.start.row+=f;else a.start.row>i&&(a.start.row+=f)}}if(f!=0&&o=e)return i;if(i.end.row>e)return null}return null},this.getNextFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r=e)return i}return null},this.getFoldedRowCount=function(e,t){var n=this.$foldData,r=t-e+1;for(var i=0;i=t){u=e?r-=t-u:r=0);break}o>=e&&(u>=e?r-=o-u:r-=o-e+1)}return r},this.$addFoldLine=function(e){return this.$foldData.push(e),this.$foldData.sort(function(e,t){return e.start.row-t.start.row}),e},this.addFold=function(e,t){var n=this.$foldData,r=!1,o;e instanceof s?o=e:(o=new s(t,e),o.collapseChildren=t.collapseChildren),this.$clipRangeToDocument(o.range);var u=o.start.row,a=o.start.column,f=o.end.row,l=o.end.column;if(u0&&(this.removeFolds(p),p.forEach(function(e){o.addSubFold(e)}));for(var d=0;d0&&this.foldAll(e.start.row+1,e.end.row,e.collapseChildren-1),e.subFolds=[]},this.expandFolds=function(e){e.forEach(function(e){this.expandFold(e)},this)},this.unfold=function(e,t){var n,i;e==null?(n=new r(0,0,this.getLength(),0),t=!0):typeof e=="number"?n=new r(e,0,e,this.getLine(e).length):"row"in e?n=r.fromPoints(e,e):n=e,i=this.getFoldsInRangeList(n);if(t)this.removeFolds(i);else{var s=i;while(s.length)this.expandFolds(s),s=this.getFoldsInRangeList(n)}if(i.length)return i},this.isRowFolded=function(e,t){return!!this.getFoldLine(e,t)},this.getRowFoldEnd=function(e,t){var n=this.getFoldLine(e,t);return n?n.end.row:e},this.getRowFoldStart=function(e,t){var n=this.getFoldLine(e,t);return n?n.start.row:e},this.getFoldDisplayLine=function(e,t,n,r,i){r==null&&(r=e.start.row),i==null&&(i=0),t==null&&(t=e.end.row),n==null&&(n=this.getLine(t).length);var s=this.doc,o="";return e.walk(function(e,t,n,u){if(tl)break}while(s&&a.test(s.type));s=i.stepBackward()}else s=i.getCurrentToken();return f.end.row=i.getCurrentTokenRow(),f.end.column=i.getCurrentTokenColumn()+s.value.length-2,f}},this.foldAll=function(e,t,n){n==undefined&&(n=1e5);var r=this.foldWidgets;if(!r)return;t=t||this.getLength(),e=e||0;for(var i=e;i=e){i=s.end.row;try{var o=this.addFold("...",s);o&&(o.collapseChildren=n)}catch(u){}}}},this.$foldStyles={manual:1,markbegin:1,markbeginend:1},this.$foldStyle="markbegin",this.setFoldStyle=function(e){if(!this.$foldStyles[e])throw new Error("invalid fold style: "+e+"["+Object.keys(this.$foldStyles).join(", ")+"]");if(this.$foldStyle==e)return;this.$foldStyle=e,e=="manual"&&this.unfold();var t=this.$foldMode;this.$setFolding(null),this.$setFolding(t)},this.$setFolding=function(e){if(this.$foldMode==e)return;this.$foldMode=e,this.off("change",this.$updateFoldWidgets),this.off("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets),this._signal("changeAnnotation");if(!e||this.$foldStyle=="manual"){this.foldWidgets=null;return}this.foldWidgets=[],this.getFoldWidget=e.getFoldWidget.bind(e,this,this.$foldStyle),this.getFoldWidgetRange=e.getFoldWidgetRange.bind(e,this,this.$foldStyle),this.$updateFoldWidgets=this.updateFoldWidgets.bind(this),this.$tokenizerUpdateFoldWidgets=this.tokenizerUpdateFoldWidgets.bind(this),this.on("change",this.$updateFoldWidgets),this.on("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets)},this.getParentFoldRangeData=function(e,t){var n=this.foldWidgets;if(!n||t&&n[e])return{};var r=e-1,i;while(r>=0){var s=n[r];s==null&&(s=n[r]=this.getFoldWidget(r));if(s=="start"){var o=this.getFoldWidgetRange(r);i||(i=o);if(o&&o.end.row>=e)break}r--}return{range:r!==-1&&o,firstRange:i}},this.onFoldWidgetClick=function(e,t){t=t.domEvent;var n={children:t.shiftKey,all:t.ctrlKey||t.metaKey,siblings:t.altKey},r=this.$toggleFoldWidget(e,n);if(!r){var i=t.target||t.srcElement;i&&/ace_fold-widget/.test(i.className)&&(i.className+=" ace_invalid")}},this.$toggleFoldWidget=function(e,t){if(!this.getFoldWidget)return;var n=this.getFoldWidget(e),r=this.getLine(e),i=n==="end"?-1:1,s=this.getFoldAt(e,i===-1?0:r.length,i);if(s)return t.children||t.all?this.removeFold(s):this.expandFold(s),s;var o=this.getFoldWidgetRange(e,!0);if(o&&!o.isMultiLine()){s=this.getFoldAt(o.start.row,o.start.column,1);if(s&&o.isEqual(s.range))return this.removeFold(s),s}if(t.siblings){var u=this.getParentFoldRangeData(e);if(u.range)var a=u.range.start.row+1,f=u.range.end.row;this.foldAll(a,f,t.all?1e4:0)}else t.children?(f=o?o.end.row:this.getLength(),this.foldAll(e+1,f,t.all?1e4:0)):o&&(t.all&&(o.collapseChildren=1e4),this.addFold("...",o));return o},this.toggleFoldWidget=function(e){var t=this.selection.getCursor().row;t=this.getRowFoldStart(t);var n=this.$toggleFoldWidget(t,{});if(n)return;var r=this.getParentFoldRangeData(t,!0);n=r.range||r.firstRange;if(n){t=n.start.row;var i=this.getFoldAt(t,this.getLine(t).length,1);i?this.removeFold(i):this.addFold("...",n)}},this.updateFoldWidgets=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.foldWidgets[t]=null;else if(e.action=="remove")this.foldWidgets.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.foldWidgets.splice.apply(this.foldWidgets,r)}},this.tokenizerUpdateFoldWidgets=function(e){var t=e.data;t.first!=t.last&&this.foldWidgets.length>t.first&&this.foldWidgets.splice(t.first,this.foldWidgets.length)}}var r=e("../range").Range,i=e("./fold_line").FoldLine,s=e("./fold").Fold,o=e("../token_iterator").TokenIterator;t.Folding=u}),ace.define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"],function(e,t,n){"use strict";function s(){this.findMatchingBracket=function(e,t){if(e.column==0)return null;var n=t||this.getLine(e.row).charAt(e.column-1);if(n=="")return null;var r=n.match(/([\(\[\{])|([\)\]\}])/);return r?r[1]?this.$findClosingBracket(r[1],e):this.$findOpeningBracket(r[2],e):null},this.getBracketRange=function(e){var t=this.getLine(e.row),n=!0,r,s=t.charAt(e.column-1),o=s&&s.match(/([\(\[\{])|([\)\]\}])/);o||(s=t.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(/([\(\[\{])|([\)\]\}])/),n=!1);if(!o)return null;if(o[1]){var u=this.$findClosingBracket(o[1],e);if(!u)return null;r=i.fromPoints(e,u),n||(r.end.column++,r.start.column--),r.cursor=r.end}else{var u=this.$findOpeningBracket(o[2],e);if(!u)return null;r=i.fromPoints(u,e),n||(r.start.column++,r.end.column--),r.cursor=r.start}return r},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{"},this.$findOpeningBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("rparen",".paren").replace(/\b(?:end)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn()-2,f=u.value;for(;;){while(a>=0){var l=f.charAt(a);if(l==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else l==e&&(s+=1);a-=1}do u=o.stepBackward();while(u&&!n.test(u.type));if(u==null)break;f=u.value,a=f.length-1}return null},this.$findClosingBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("lparen",".paren").replace(/\b(?:start|begin)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn();for(;;){var f=u.value,l=f.length;while(a=4352&&e<=4447||e>=4515&&e<=4519||e>=4602&&e<=4607||e>=9001&&e<=9002||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12283||e>=12288&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12589||e>=12593&&e<=12686||e>=12688&&e<=12730||e>=12736&&e<=12771||e>=12784&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=13054||e>=13056&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=55216&&e<=55238||e>=55243&&e<=55291||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=65281&&e<=65376||e>=65504&&e<=65510}r.implement(this,u),this.setDocument=function(e){this.doc&&this.doc.removeListener("change",this.$onChange),this.doc=e,e.on("change",this.$onChange),this.bgTokenizer&&this.bgTokenizer.setDocument(this.getDocument()),this.resetCaches()},this.getDocument=function(){return this.doc},this.$resetRowCache=function(e){if(!e){this.$docRowCache=[],this.$screenRowCache=[];return}var t=this.$docRowCache.length,n=this.$getRowCacheIndex(this.$docRowCache,e)+1;t>n&&(this.$docRowCache.splice(n,t),this.$screenRowCache.splice(n,t))},this.$getRowCacheIndex=function(e,t){var n=0,r=e.length-1;while(n<=r){var i=n+r>>1,s=e[i];if(t>s)n=i+1;else{if(!(t=t)break}return r=n[s],r?(r.index=s,r.start=i-r.value.length,r):null},this.setUndoManager=function(e){this.$undoManager=e,this.$informUndoManager&&this.$informUndoManager.cancel();if(e){var t=this;e.addSession(this),this.$syncInformUndoManager=function(){t.$informUndoManager.cancel(),t.mergeUndoDeltas=!1},this.$informUndoManager=i.delayedCall(this.$syncInformUndoManager)}else this.$syncInformUndoManager=function(){}},this.markUndoGroup=function(){this.$syncInformUndoManager&&this.$syncInformUndoManager()},this.$defaultUndoManager={undo:function(){},redo:function(){},reset:function(){},add:function(){},addSelection:function(){},startNewGroup:function(){},addSession:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?i.stringRepeat(" ",this.getTabSize()):" "},this.setUseSoftTabs=function(e){this.setOption("useSoftTabs",e)},this.getUseSoftTabs=function(){return this.$useSoftTabs&&!this.$mode.$indentWithTabs},this.setTabSize=function(e){this.setOption("tabSize",e)},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(e){return this.$useSoftTabs&&e.column%this.$tabSize===0},this.setNavigateWithinSoftTabs=function(e){this.setOption("navigateWithinSoftTabs",e)},this.getNavigateWithinSoftTabs=function(){return this.$navigateWithinSoftTabs},this.$overwrite=!1,this.setOverwrite=function(e){this.setOption("overwrite",e)},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.addGutterDecoration=function(e,t){this.$decorations[e]||(this.$decorations[e]=""),this.$decorations[e]+=" "+t,this._signal("changeBreakpoint",{})},this.removeGutterDecoration=function(e,t){this.$decorations[e]=(this.$decorations[e]||"").replace(" "+t,""),this._signal("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(e){this.$breakpoints=[];for(var t=0;t0&&(r=!!n.charAt(t-1).match(this.tokenRe)),r||(r=!!n.charAt(t).match(this.tokenRe));if(r)var i=this.tokenRe;else if(/^\s+$/.test(n.slice(t-1,t+1)))var i=/\s/;else var i=this.nonTokenRe;var s=t;if(s>0){do s--;while(s>=0&&n.charAt(s).match(i));s++}var o=t;while(oe&&(e=t.screenWidth)}),this.lineWidgetWidth=e},this.$computeWidth=function(e){if(this.$modified||e){this.$modified=!1;if(this.$useWrapMode)return this.screenWidth=this.$wrapLimit;var t=this.doc.getAllLines(),n=this.$rowLengthCache,r=0,i=0,s=this.$foldData[i],o=s?s.start.row:Infinity,u=t.length;for(var a=0;ao){a=s.end.row+1;if(a>=u)break;s=this.$foldData[i++],o=s?s.start.row:Infinity}n[a]==null&&(n[a]=this.$getStringScreenWidth(t[a])[0]),n[a]>r&&(r=n[a])}this.screenWidth=r}},this.getLine=function(e){return this.doc.getLine(e)},this.getLines=function(e,t){return this.doc.getLines(e,t)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(e){return this.doc.getTextRange(e||this.selection.getRange())},this.insert=function(e,t){return this.doc.insert(e,t)},this.remove=function(e){return this.doc.remove(e)},this.removeFullLines=function(e,t){return this.doc.removeFullLines(e,t)},this.undoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=e.length-1;n!=-1;n--){var r=e[n];r.action=="insert"||r.action=="remove"?this.doc.revertDelta(r):r.folds&&this.addFolds(r.folds)}!t&&this.$undoSelect&&(e.selectionBefore?this.selection.fromJSON(e.selectionBefore):this.selection.setRange(this.$getUndoSelection(e,!0))),this.$fromUndo=!1},this.redoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=0;ne.end.column&&(s.start.column+=u),s.end.row==e.end.row&&s.end.column>e.end.column&&(s.end.column+=u)),o&&s.start.row>=e.end.row&&(s.start.row+=o,s.end.row+=o)}s.end=this.insert(s.start,r);if(i.length){var a=e.start,f=s.start,o=f.row-a.row,u=f.column-a.column;this.addFolds(i.map(function(e){return e=e.clone(),e.start.row==a.row&&(e.start.column+=u),e.end.row==a.row&&(e.end.column+=u),e.start.row+=o,e.end.row+=o,e}))}return s},this.indentRows=function(e,t,n){n=n.replace(/\t/g,this.getTabString());for(var r=e;r<=t;r++)this.doc.insertInLine({row:r,column:0},n)},this.outdentRows=function(e){var t=e.collapseRows(),n=new l(0,0,0,0),r=this.getTabSize();for(var i=t.start.row;i<=t.end.row;++i){var s=this.getLine(i);n.start.row=i,n.end.row=i;for(var o=0;o0){var r=this.getRowFoldEnd(t+n);if(r>this.doc.getLength()-1)return 0;var i=r-t}else{e=this.$clipRowToDocument(e),t=this.$clipRowToDocument(t);var i=t-e+1}var s=new l(e,0,t,Number.MAX_VALUE),o=this.getFoldsInRange(s).map(function(e){return e=e.clone(),e.start.row+=i,e.end.row+=i,e}),u=n==0?this.doc.getLines(e,t):this.doc.removeFullLines(e,t);return this.doc.insertFullLines(e+i,u),o.length&&this.addFolds(o),i},this.moveLinesUp=function(e,t){return this.$moveLines(e,t,-1)},this.moveLinesDown=function(e,t){return this.$moveLines(e,t,1)},this.duplicateLines=function(e,t){return this.$moveLines(e,t,0)},this.$clipRowToDocument=function(e){return Math.max(0,Math.min(e,this.doc.getLength()-1))},this.$clipColumnToRow=function(e,t){return t<0?0:Math.min(this.doc.getLine(e).length,t)},this.$clipPositionToDocument=function(e,t){t=Math.max(0,t);if(e<0)e=0,t=0;else{var n=this.doc.getLength();e>=n?(e=n-1,t=this.doc.getLine(n-1).length):t=Math.min(this.doc.getLine(e).length,t)}return{row:e,column:t}},this.$clipRangeToDocument=function(e){e.start.row<0?(e.start.row=0,e.start.column=0):e.start.column=this.$clipColumnToRow(e.start.row,e.start.column);var t=this.doc.getLength()-1;return e.end.row>t?(e.end.row=t,e.end.column=this.doc.getLine(t).length):e.end.column=this.$clipColumnToRow(e.end.row,e.end.column),e},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(e){if(e!=this.$useWrapMode){this.$useWrapMode=e,this.$modified=!0,this.$resetRowCache(0);if(e){var t=this.getLength();this.$wrapData=Array(t),this.$updateWrapData(0,t-1)}this._signal("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(e,t){if(this.$wrapLimitRange.min!==e||this.$wrapLimitRange.max!==t)this.$wrapLimitRange={min:e,max:t},this.$modified=!0,this.$bidiHandler.markAsDirty(),this.$useWrapMode&&this._signal("changeWrapMode")},this.adjustWrapLimit=function(e,t){var n=this.$wrapLimitRange;n.max<0&&(n={min:t,max:t});var r=this.$constrainWrapLimit(e,n.min,n.max);return r!=this.$wrapLimit&&r>1?(this.$wrapLimit=r,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._signal("changeWrapLimit")),!0):!1},this.$constrainWrapLimit=function(e,t,n){return t&&(e=Math.max(t,e)),n&&(e=Math.min(n,e)),e},this.getWrapLimit=function(){return this.$wrapLimit},this.setWrapLimit=function(e){this.setWrapLimitRange(e,e)},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(e){var t=this.$useWrapMode,n=e.action,r=e.start,i=e.end,s=r.row,o=i.row,u=o-s,a=null;this.$updating=!0;if(u!=0)if(n==="remove"){this[t?"$wrapData":"$rowLengthCache"].splice(s,u);var f=this.$foldData;a=this.getFoldsInRange(e),this.removeFolds(a);var l=this.getFoldLine(i.row),c=0;if(l){l.addRemoveChars(i.row,i.column,r.column-i.column),l.shiftRow(-u);var h=this.getFoldLine(s);h&&h!==l&&(h.merge(l),l=h),c=f.indexOf(l)+1}for(c;c=i.row&&l.shiftRow(-u)}o=s}else{var p=Array(u);p.unshift(s,0);var d=t?this.$wrapData:this.$rowLengthCache;d.splice.apply(d,p);var f=this.$foldData,l=this.getFoldLine(s),c=0;if(l){var v=l.range.compareInside(r.row,r.column);v==0?(l=l.split(r.row,r.column),l&&(l.shiftRow(u),l.addRemoveChars(o,0,i.column-r.column))):v==-1&&(l.addRemoveChars(s,0,i.column-r.column),l.shiftRow(u)),c=f.indexOf(l)+1}for(c;c=s&&l.shiftRow(u)}}else{u=Math.abs(e.start.column-e.end.column),n==="remove"&&(a=this.getFoldsInRange(e),this.removeFolds(a),u=-u);var l=this.getFoldLine(s);l&&l.addRemoveChars(s,r.column,u)}return t&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),this.$updating=!1,t?this.$updateWrapData(s,o):this.$updateRowLengthCache(s,o),a},this.$updateRowLengthCache=function(e,t,n){this.$rowLengthCache[e]=null,this.$rowLengthCache[t]=null},this.$updateWrapData=function(e,t){var r=this.doc.getAllLines(),i=this.getTabSize(),o=this.$wrapData,u=this.$wrapLimit,a,f,l=e;t=Math.min(t,r.length-1);while(l<=t)f=this.getFoldLine(l,f),f?(a=[],f.walk(function(e,t,i,o){var u;if(e!=null){u=this.$getDisplayTokens(e,a.length),u[0]=n;for(var f=1;fr-b){var w=f+r-b;if(e[w-1]>=c&&e[w]>=c){y(w);continue}if(e[w]==n||e[w]==s){for(w;w!=f-1;w--)if(e[w]==n)break;if(w>f){y(w);continue}w=f+r;for(w;w>2)),f-1);while(w>E&&e[w]E&&e[w]E&&e[w]==a)w--}else while(w>E&&e[w]E){y(++w);continue}w=f+r,e[w]==t&&w--,y(w-b)}return o},this.$getDisplayTokens=function(n,r){var i=[],s;r=r||0;for(var o=0;o39&&u<48||u>57&&u<64?i.push(a):u>=4352&&m(u)?i.push(e,t):i.push(e)}return i},this.$getStringScreenWidth=function(e,t,n){if(t==0)return[0,0];t==null&&(t=Infinity),n=n||0;var r,i;for(i=0;i=4352&&m(r)?n+=2:n+=1;if(n>t)break}return[n,i]},this.lineWidgets=null,this.getRowLength=function(e){if(this.lineWidgets)var t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0;else t=0;return!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.getRowLineCount=function(e){return!this.$useWrapMode||!this.$wrapData[e]?1:this.$wrapData[e].length+1},this.getRowWrapIndent=function(e){if(this.$useWrapMode){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE),n=this.$wrapData[t.row];return n.length&&n[0]=0)var u=f[l],i=this.$docRowCache[l],h=e>f[c-1];else var h=!c;var p=this.getLength()-1,d=this.getNextFoldLine(i),v=d?d.start.row:Infinity;while(u<=e){a=this.getRowLength(i);if(u+a>e||i>=p)break;u+=a,i++,i>v&&(i=d.end.row+1,d=this.getNextFoldLine(i,d),v=d?d.start.row:Infinity),h&&(this.$docRowCache.push(i),this.$screenRowCache.push(u))}if(d&&d.start.row<=i)r=this.getFoldDisplayLine(d),i=d.start.row;else{if(u+a<=e||i>p)return{row:p,column:this.getLine(p).length};r=this.getLine(i),d=null}var m=0,g=Math.floor(e-u);if(this.$useWrapMode){var y=this.$wrapData[i];y&&(o=y[g],g>0&&y.length&&(m=y.indent,s=y[g-1]||y[y.length-1],r=r.substring(s)))}return n!==undefined&&this.$bidiHandler.isBidiRow(u+g,i,g)&&(t=this.$bidiHandler.offsetToCol(n)),s+=this.$getStringScreenWidth(r,t-m)[1],this.$useWrapMode&&s>=o&&(s=o-1),d?d.idxToPosition(s):{row:i,column:s}},this.documentToScreenPosition=function(e,t){if(typeof t=="undefined")var n=this.$clipPositionToDocument(e.row,e.column);else n=this.$clipPositionToDocument(e,t);e=n.row,t=n.column;var r=0,i=null,s=null;s=this.getFoldAt(e,t,1),s&&(e=s.start.row,t=s.start.column);var o,u=0,a=this.$docRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var u=a[f],r=this.$screenRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getNextFoldLine(u),p=h?h.start.row:Infinity;while(u=p){o=h.end.row+1;if(o>e)break;h=this.getNextFoldLine(o,h),p=h?h.start.row:Infinity}else o=u+1;r+=this.getRowLength(u),u=o,c&&(this.$docRowCache.push(u),this.$screenRowCache.push(r))}var d="";h&&u>=p?(d=this.getFoldDisplayLine(h,e,t),i=h.start.row):(d=this.getLine(e).substring(0,t),i=e);var v=0;if(this.$useWrapMode){var m=this.$wrapData[i];if(m){var g=0;while(d.length>=m[g])r++,g++;d=d.substring(m[g-1]||0,d.length),v=g>0?m.indent:0}}return{row:r,column:v+this.$getStringScreenWidth(d)[0]}},this.documentToScreenColumn=function(e,t){return this.documentToScreenPosition(e,t).column},this.documentToScreenRow=function(e,t){return this.documentToScreenPosition(e,t).row},this.getScreenLength=function(){var e=0,t=null;if(!this.$useWrapMode){e=this.getLength();var n=this.$foldData;for(var r=0;ro&&(s=t.end.row+1,t=this.$foldData[r++],o=t?t.start.row:Infinity)}}return this.lineWidgets&&(e+=this.$getWidgetScreenLength()),e},this.$setFontMetrics=function(e){if(!this.$enableVarChar)return;this.$getStringScreenWidth=function(t,n,r){if(n===0)return[0,0];n||(n=Infinity),r=r||0;var i,s;for(s=0;sn)break}return[r,s]}},this.destroy=function(){this.bgTokenizer&&(this.bgTokenizer.setDocument(null),this.bgTokenizer=null),this.$stopWorker()},this.isFullWidth=m}.call(d.prototype),e("./edit_session/folding").Folding.call(d.prototype),e("./edit_session/bracket_match").BracketMatch.call(d.prototype),o.defineOptions(d.prototype,"session",{wrap:{set:function(e){!e||e=="off"?e=!1:e=="free"?e=!0:e=="printMargin"?e=-1:typeof e=="string"&&(e=parseInt(e,10)||!1);if(this.$wrap==e)return;this.$wrap=e;if(!e)this.setUseWrapMode(!1);else{var t=typeof e=="number"?e:null;this.setWrapLimitRange(t,t),this.setUseWrapMode(!0)}},get:function(){return this.getUseWrapMode()?this.$wrap==-1?"printMargin":this.getWrapLimitRange().min?this.$wrap:"free":"off"},handlesSet:!0},wrapMethod:{set:function(e){e=e=="auto"?this.$mode.type!="text":e!="text",e!=this.$wrapAsCode&&(this.$wrapAsCode=e,this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0)))},initialValue:"auto"},indentedSoftWrap:{set:function(){this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0))},initialValue:!0},firstLineNumber:{set:function(){this._signal("changeBreakpoint")},initialValue:1},useWorker:{set:function(e){this.$useWorker=e,this.$stopWorker(),e&&this.$startWorker()},initialValue:!0},useSoftTabs:{initialValue:!0},tabSize:{set:function(e){if(isNaN(e)||this.$tabSize===e)return;this.$modified=!0,this.$rowLengthCache=[],this.$tabSize=e,this._signal("changeTabSize")},initialValue:4,handlesSet:!0},navigateWithinSoftTabs:{initialValue:!1},foldStyle:{set:function(e){this.setFoldStyle(e)},handlesSet:!0},overwrite:{set:function(e){this._signal("changeOverwrite")},initialValue:!1},newLineMode:{set:function(e){this.doc.setNewLineMode(e)},get:function(){return this.doc.getNewLineMode()},handlesSet:!0},mode:{set:function(e){this.setMode(e)},get:function(){return this.$modeId},handlesSet:!0}}),t.EditSession=d}),ace.define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";function u(e,t){function n(e){return/\w/.test(e)||t.regExp?"\\b":""}return n(e[0])+e+n(e[e.length-1])}var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(){this.$options={}};(function(){this.set=function(e){return i.mixin(this.$options,e),this},this.getOptions=function(){return r.copyObject(this.$options)},this.setOptions=function(e){this.$options=e},this.find=function(e){var t=this.$options,n=this.$matchIterator(e,t);if(!n)return!1;var r=null;return n.forEach(function(e,n,i,o){return r=new s(e,n,i,o),n==o&&t.start&&t.start.start&&t.skipCurrent!=0&&r.isEqual(t.start)?(r=null,!1):!0}),r},this.findAll=function(e){var t=this.$options;if(!t.needle)return[];this.$assembleRegExp(t);var n=t.range,i=n?e.getLines(n.start.row,n.end.row):e.doc.getAllLines(),o=[],u=t.re;if(t.$isMultiLine){var a=u.length,f=i.length-a,l;e:for(var c=u.offset||0;c<=f;c++){for(var h=0;hv)continue;o.push(l=new s(c,v,c+a-1,m)),a>2&&(c=c+a-2)}}else for(var g=0;gE&&o[h].end.row==n.end.row)h--;o=o.slice(g,h+1);for(g=0,h=o.length;g=u;n--)if(c(n,Number.MAX_VALUE,e))return;if(t.wrap==0)return;for(n=a,u=o.row;n>=u;n--)if(c(n,Number.MAX_VALUE,e))return};else var f=function(e){var n=o.row;if(c(n,o.column,e))return;for(n+=1;n<=a;n++)if(c(n,0,e))return;if(t.wrap==0)return;for(n=u,a=o.row;n<=a;n++)if(c(n,0,e))return};if(t.$isMultiLine)var l=n.length,c=function(t,i,s){var o=r?t-l+1:t;if(o<0)return;var u=e.getLine(o),a=u.search(n[0]);if(!r&&ai)return;if(s(o,a,o+l-1,c))return!0};else if(r)var c=function(t,r,i){var s=e.getLine(t),o=[],u,a=0;n.lastIndex=0;while(u=n.exec(s)){var f=u[0].length;a=u.index;if(!f){if(a>=s.length)break;n.lastIndex=a+=1}if(u.index+f>r)break;o.push(u.index,f)}for(var l=o.length-1;l>=0;l-=2){var c=o[l-1],f=o[l];if(i(t,c,t,c+f))return!0}};else var c=function(t,r,i){var s=e.getLine(t),o,u;n.lastIndex=r;while(u=n.exec(s)){var a=u[0].length;o=u.index;if(i(t,o,t,o+a))return!0;if(!a){n.lastIndex=o+=1;if(o>=s.length)return!1}}};return{forEach:f}}}).call(o.prototype),t.Search=o}),ace.define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function o(e,t){this.platform=t||(i.isMac?"mac":"win"),this.commands={},this.commandKeyBinding={},this.addCommands(e),this.$singleCommand=!0}function u(e,t){o.call(this,e,t),this.$singleCommand=!1}var r=e("../lib/keys"),i=e("../lib/useragent"),s=r.KEY_MODS;u.prototype=o.prototype,function(){function e(e){return typeof e=="object"&&e.bindKey&&e.bindKey.position||(e.isDefault?-100:0)}this.addCommand=function(e){this.commands[e.name]&&this.removeCommand(e),this.commands[e.name]=e,e.bindKey&&this._buildKeyHash(e)},this.removeCommand=function(e,t){var n=e&&(typeof e=="string"?e:e.name);e=this.commands[n],t||delete this.commands[n];var r=this.commandKeyBinding;for(var i in r){var s=r[i];if(s==e)delete r[i];else if(Array.isArray(s)){var o=s.indexOf(e);o!=-1&&(s.splice(o,1),s.length==1&&(r[i]=s[0]))}}},this.bindKey=function(e,t,n){typeof e=="object"&&e&&(n==undefined&&(n=e.position),e=e[this.platform]);if(!e)return;if(typeof t=="function")return this.addCommand({exec:t,bindKey:e,name:t.name||e});e.split("|").forEach(function(e){var r="";if(e.indexOf(" ")!=-1){var i=e.split(/\s+/);e=i.pop(),i.forEach(function(e){var t=this.parseKeys(e),n=s[t.hashId]+t.key;r+=(r?" ":"")+n,this._addCommandToBinding(r,"chainKeys")},this),r+=" "}var o=this.parseKeys(e),u=s[o.hashId]+o.key;this._addCommandToBinding(r+u,t,n)},this)},this._addCommandToBinding=function(t,n,r){var i=this.commandKeyBinding,s;if(!n)delete i[t];else if(!i[t]||this.$singleCommand)i[t]=n;else{Array.isArray(i[t])?(s=i[t].indexOf(n))!=-1&&i[t].splice(s,1):i[t]=[i[t]],typeof r!="number"&&(r=e(n));var o=i[t];for(s=0;sr)break}o.splice(s,0,n)}},this.addCommands=function(e){e&&Object.keys(e).forEach(function(t){var n=e[t];if(!n)return;if(typeof n=="string")return this.bindKey(n,t);typeof n=="function"&&(n={exec:n});if(typeof n!="object")return;n.name||(n.name=t),this.addCommand(n)},this)},this.removeCommands=function(e){Object.keys(e).forEach(function(t){this.removeCommand(e[t])},this)},this.bindKeys=function(e){Object.keys(e).forEach(function(t){this.bindKey(t,e[t])},this)},this._buildKeyHash=function(e){this.bindKey(e.bindKey,e)},this.parseKeys=function(e){var t=e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(e){return e}),n=t.pop(),i=r[n];if(r.FUNCTION_KEYS[i])n=r.FUNCTION_KEYS[i].toLowerCase();else{if(!t.length)return{key:n,hashId:-1};if(t.length==1&&t[0]=="shift")return{key:n.toUpperCase(),hashId:-1}}var s=0;for(var o=t.length;o--;){var u=r.KEY_MODS[t[o]];if(u==null)return typeof console!="undefined"&&console.error("invalid modifier "+t[o]+" in "+e),!1;s|=u}return{key:n,hashId:s}},this.findKeyCommand=function(t,n){var r=s[t]+n;return this.commandKeyBinding[r]},this.handleKeyboard=function(e,t,n,r){if(r<0)return;var i=s[t]+n,o=this.commandKeyBinding[i];e.$keyChain&&(e.$keyChain+=" "+i,o=this.commandKeyBinding[e.$keyChain]||o);if(o)if(o=="chainKeys"||o[o.length-1]=="chainKeys")return e.$keyChain=e.$keyChain||i,{command:"null"};if(e.$keyChain)if(!!t&&t!=4||n.length!=1){if(t==-1||r>0)e.$keyChain=""}else e.$keyChain=e.$keyChain.slice(0,-i.length-1);return{command:o}},this.getStatusText=function(e,t){return t.$keyChain||""}}.call(o.prototype),t.HashHandler=o,t.MultiHashHandler=u}),ace.define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.command.exec(e.editor,e.args||{})})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;if(this.$checkCommandState!=0&&e.isAvailable&&!e.isAvailable(t))return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.removeEventListener("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}),ace.define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"],function(e,t,n){"use strict";function o(e,t){return{win:e,mac:t}}var r=e("../lib/lang"),i=e("../config"),s=e("../range").Range;t.commands=[{name:"showSettingsMenu",bindKey:o("Ctrl-,","Command-,"),exec:function(e){i.loadModule("ace/ext/settings_menu",function(t){t.init(e),e.showSettingsMenu()})},readOnly:!0},{name:"goToNextError",bindKey:o("Alt-E","F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,1)})},scrollIntoView:"animate",readOnly:!0},{name:"goToPreviousError",bindKey:o("Alt-Shift-E","Shift-F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,-1)})},scrollIntoView:"animate",readOnly:!0},{name:"selectall",bindKey:o("Ctrl-A","Command-A"),exec:function(e){e.selectAll()},readOnly:!0},{name:"centerselection",bindKey:o(null,"Ctrl-L"),exec:function(e){e.centerSelection()},readOnly:!0},{name:"gotoline",bindKey:o("Ctrl-L","Command-L"),exec:function(e,t){typeof t!="number"&&(t=parseInt(prompt("Enter line number:"),10)),isNaN(t)||e.gotoLine(t)},readOnly:!0},{name:"fold",bindKey:o("Alt-L|Ctrl-F1","Command-Alt-L|Command-F1"),exec:function(e){e.session.toggleFold(!1)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"unfold",bindKey:o("Alt-Shift-L|Ctrl-Shift-F1","Command-Alt-Shift-L|Command-Shift-F1"),exec:function(e){e.session.toggleFold(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleFoldWidget",bindKey:o("F2","F2"),exec:function(e){e.session.toggleFoldWidget()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleParentFoldWidget",bindKey:o("Alt-F2","Alt-F2"),exec:function(e){e.session.toggleFoldWidget(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"foldall",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAll()},scrollIntoView:"center",readOnly:!0},{name:"foldOther",bindKey:o("Alt-0","Command-Option-0"),exec:function(e){e.session.foldAll(),e.session.unfold(e.selection.getAllRanges())},scrollIntoView:"center",readOnly:!0},{name:"unfoldall",bindKey:o("Alt-Shift-0","Command-Option-Shift-0"),exec:function(e){e.session.unfold()},scrollIntoView:"center",readOnly:!0},{name:"findnext",bindKey:o("Ctrl-K","Command-G"),exec:function(e){e.findNext()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"findprevious",bindKey:o("Ctrl-Shift-K","Command-Shift-G"),exec:function(e){e.findPrevious()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"selectOrFindNext",bindKey:o("Alt-K","Ctrl-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findNext()},readOnly:!0},{name:"selectOrFindPrevious",bindKey:o("Alt-Shift-K","Ctrl-Shift-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findPrevious()},readOnly:!0},{name:"find",bindKey:o("Ctrl-F","Command-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e)})},readOnly:!0},{name:"overwrite",bindKey:"Insert",exec:function(e){e.toggleOverwrite()},readOnly:!0},{name:"selecttostart",bindKey:o("Ctrl-Shift-Home","Command-Shift-Home|Command-Shift-Up"),exec:function(e){e.getSelection().selectFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotostart",bindKey:o("Ctrl-Home","Command-Home|Command-Up"),exec:function(e){e.navigateFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectup",bindKey:o("Shift-Up","Shift-Up|Ctrl-Shift-P"),exec:function(e){e.getSelection().selectUp()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golineup",bindKey:o("Up","Up|Ctrl-P"),exec:function(e,t){e.navigateUp(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttoend",bindKey:o("Ctrl-Shift-End","Command-Shift-End|Command-Shift-Down"),exec:function(e){e.getSelection().selectFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotoend",bindKey:o("Ctrl-End","Command-End|Command-Down"),exec:function(e){e.navigateFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectdown",bindKey:o("Shift-Down","Shift-Down|Ctrl-Shift-N"),exec:function(e){e.getSelection().selectDown()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golinedown",bindKey:o("Down","Down|Ctrl-N"),exec:function(e,t){e.navigateDown(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordleft",bindKey:o("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(e){e.getSelection().selectWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordleft",bindKey:o("Ctrl-Left","Option-Left"),exec:function(e){e.navigateWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolinestart",bindKey:o("Alt-Shift-Left","Command-Shift-Left|Ctrl-Shift-A"),exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolinestart",bindKey:o("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(e){e.navigateLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectleft",bindKey:o("Shift-Left","Shift-Left|Ctrl-Shift-B"),exec:function(e){e.getSelection().selectLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoleft",bindKey:o("Left","Left|Ctrl-B"),exec:function(e,t){e.navigateLeft(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordright",bindKey:o("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(e){e.getSelection().selectWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordright",bindKey:o("Ctrl-Right","Option-Right"),exec:function(e){e.navigateWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolineend",bindKey:o("Alt-Shift-Right","Command-Shift-Right|Shift-End|Ctrl-Shift-E"),exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolineend",bindKey:o("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(e){e.navigateLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectright",bindKey:o("Shift-Right","Shift-Right"),exec:function(e){e.getSelection().selectRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoright",bindKey:o("Right","Right|Ctrl-F"),exec:function(e,t){e.navigateRight(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectpagedown",bindKey:"Shift-PageDown",exec:function(e){e.selectPageDown()},readOnly:!0},{name:"pagedown",bindKey:o(null,"Option-PageDown"),exec:function(e){e.scrollPageDown()},readOnly:!0},{name:"gotopagedown",bindKey:o("PageDown","PageDown|Ctrl-V"),exec:function(e){e.gotoPageDown()},readOnly:!0},{name:"selectpageup",bindKey:"Shift-PageUp",exec:function(e){e.selectPageUp()},readOnly:!0},{name:"pageup",bindKey:o(null,"Option-PageUp"),exec:function(e){e.scrollPageUp()},readOnly:!0},{name:"gotopageup",bindKey:"PageUp",exec:function(e){e.gotoPageUp()},readOnly:!0},{name:"scrollup",bindKey:o("Ctrl-Up",null),exec:function(e){e.renderer.scrollBy(0,-2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"scrolldown",bindKey:o("Ctrl-Down",null),exec:function(e){e.renderer.scrollBy(0,2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"selectlinestart",bindKey:"Shift-Home",exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectlineend",bindKey:"Shift-End",exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"togglerecording",bindKey:o("Ctrl-Alt-E","Command-Option-E"),exec:function(e){e.commands.toggleRecording(e)},readOnly:!0},{name:"replaymacro",bindKey:o("Ctrl-Shift-E","Command-Shift-E"),exec:function(e){e.commands.replay(e)},readOnly:!0},{name:"jumptomatching",bindKey:o("Ctrl-P","Ctrl-P"),exec:function(e){e.jumpToMatching()},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"selecttomatching",bindKey:o("Ctrl-Shift-P","Ctrl-Shift-P"),exec:function(e){e.jumpToMatching(!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"expandToMatching",bindKey:o("Ctrl-Shift-M","Ctrl-Shift-M"),exec:function(e){e.jumpToMatching(!0,!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"passKeysToBrowser",bindKey:o(null,null),exec:function(){},passEvent:!0,readOnly:!0},{name:"copy",exec:function(e){},readOnly:!0},{name:"cut",exec:function(e){var t=e.$copyWithEmptySelection&&e.selection.isEmpty(),n=t?e.selection.getLineRange():e.selection.getRange();e._emit("cut",n),n.isEmpty()||e.session.remove(n),e.clearSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"paste",exec:function(e,t){e.$handlePaste(t)},scrollIntoView:"cursor"},{name:"removeline",bindKey:o("Ctrl-D","Command-D"),exec:function(e){e.removeLines()},scrollIntoView:"cursor",multiSelectAction:"forEachLine"},{name:"duplicateSelection",bindKey:o("Ctrl-Shift-D","Command-Shift-D"),exec:function(e){e.duplicateSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"sortlines",bindKey:o("Ctrl-Alt-S","Command-Alt-S"),exec:function(e){e.sortLines()},scrollIntoView:"selection",multiSelectAction:"forEachLine"},{name:"togglecomment",bindKey:o("Ctrl-/","Command-/"),exec:function(e){e.toggleCommentLines()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"toggleBlockComment",bindKey:o("Ctrl-Shift-/","Command-Shift-/"),exec:function(e){e.toggleBlockComment()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"modifyNumberUp",bindKey:o("Ctrl-Shift-Up","Alt-Shift-Up"),exec:function(e){e.modifyNumber(1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"modifyNumberDown",bindKey:o("Ctrl-Shift-Down","Alt-Shift-Down"),exec:function(e){e.modifyNumber(-1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"replace",bindKey:o("Ctrl-H","Command-Option-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e,!0)})}},{name:"undo",bindKey:o("Ctrl-Z","Command-Z"),exec:function(e){e.undo()}},{name:"redo",bindKey:o("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(e){e.redo()}},{name:"copylinesup",bindKey:o("Alt-Shift-Up","Command-Option-Up"),exec:function(e){e.copyLinesUp()},scrollIntoView:"cursor"},{name:"movelinesup",bindKey:o("Alt-Up","Option-Up"),exec:function(e){e.moveLinesUp()},scrollIntoView:"cursor"},{name:"copylinesdown",bindKey:o("Alt-Shift-Down","Command-Option-Down"),exec:function(e){e.copyLinesDown()},scrollIntoView:"cursor"},{name:"movelinesdown",bindKey:o("Alt-Down","Option-Down"),exec:function(e){e.moveLinesDown()},scrollIntoView:"cursor"},{name:"del",bindKey:o("Delete","Delete|Ctrl-D|Shift-Delete"),exec:function(e){e.remove("right")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"backspace",bindKey:o("Shift-Backspace|Backspace","Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(e){e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"cut_or_delete",bindKey:o("Shift-Delete",null),exec:function(e){if(!e.selection.isEmpty())return!1;e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestart",bindKey:o("Alt-Backspace","Command-Backspace"),exec:function(e){e.removeToLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineend",bindKey:o("Alt-Delete","Ctrl-K|Command-Delete"),exec:function(e){e.removeToLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestarthard",bindKey:o("Ctrl-Shift-Backspace",null),exec:function(e){var t=e.selection.getRange();t.start.column=0,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineendhard",bindKey:o("Ctrl-Shift-Delete",null),exec:function(e){var t=e.selection.getRange();t.end.column=Number.MAX_VALUE,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordleft",bindKey:o("Ctrl-Backspace","Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(e){e.removeWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordright",bindKey:o("Ctrl-Delete","Alt-Delete"),exec:function(e){e.removeWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"outdent",bindKey:o("Shift-Tab","Shift-Tab"),exec:function(e){e.blockOutdent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"indent",bindKey:o("Tab","Tab"),exec:function(e){e.indent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"blockoutdent",bindKey:o("Ctrl-[","Ctrl-["),exec:function(e){e.blockOutdent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"blockindent",bindKey:o("Ctrl-]","Ctrl-]"),exec:function(e){e.blockIndent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"insertstring",exec:function(e,t){e.insert(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"inserttext",exec:function(e,t){e.insert(r.stringRepeat(t.text||"",t.times||1))},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"splitline",bindKey:o(null,"Ctrl-O"),exec:function(e){e.splitLine()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"transposeletters",bindKey:o("Alt-Shift-X","Ctrl-T"),exec:function(e){e.transposeLetters()},multiSelectAction:function(e){e.transposeSelections(1)},scrollIntoView:"cursor"},{name:"touppercase",bindKey:o("Ctrl-U","Ctrl-U"),exec:function(e){e.toUpperCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"tolowercase",bindKey:o("Ctrl-Shift-U","Ctrl-Shift-U"),exec:function(e){e.toLowerCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"expandtoline",bindKey:o("Ctrl-Shift-L","Command-Shift-L"),exec:function(e){var t=e.selection.getRange();t.start.column=t.end.column=0,t.end.row++,e.selection.setRange(t,!1)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"joinlines",bindKey:o(null,null),exec:function(e){var t=e.selection.isBackwards(),n=t?e.selection.getSelectionLead():e.selection.getSelectionAnchor(),i=t?e.selection.getSelectionAnchor():e.selection.getSelectionLead(),o=e.session.doc.getLine(n.row).length,u=e.session.doc.getTextRange(e.selection.getRange()),a=u.replace(/\n\s*/," ").length,f=e.session.doc.getLine(n.row);for(var l=n.row+1;l<=i.row+1;l++){var c=r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l)));c.length!==0&&(c=" "+c),f+=c}i.row+10?(e.selection.moveCursorTo(n.row,n.column),e.selection.selectTo(n.row,n.column+a)):(o=e.session.doc.getLine(n.row).length>o?o+1:o,e.selection.moveCursorTo(n.row,o))},multiSelectAction:"forEach",readOnly:!0},{name:"invertSelection",bindKey:o(null,null),exec:function(e){var t=e.session.doc.getLength()-1,n=e.session.doc.getLine(t).length,r=e.selection.rangeList.ranges,i=[];r.length<1&&(r=[e.selection.getRange()]);for(var o=0;o=i.lastRow||r.end.row<=i.firstRow)&&this.renderer.scrollSelectionIntoView(this.selection.anchor,this.selection.lead);break;default:}n=="animate"&&this.renderer.animateScrolling(this.curOp.scrollTop)}var s=this.selection.toJSON();this.curOp.selectionAfter=s,this.$lastSel=this.selection.toJSON(),this.session.getUndoManager().addSelection(s),this.prevOp=this.curOp,this.curOp=null}},this.$mergeableCommands=["backspace","del","insertstring"],this.$historyTracker=function(e){if(!this.$mergeUndoDeltas)return;var t=this.prevOp,n=this.$mergeableCommands,r=t.command&&e.command.name==t.command.name;if(e.command.name=="insertstring"){var i=e.args;this.mergeNextCommand===undefined&&(this.mergeNextCommand=!0),r=r&&this.mergeNextCommand&&(!/\s/.test(i)||/\s/.test(t.args)),this.mergeNextCommand=!0}else r=r&&n.indexOf(e.command.name)!==-1;this.$mergeUndoDeltas!="always"&&Date.now()-this.sequenceStartTime>2e3&&(r=!1),r?this.session.mergeUndoDeltas=!0:n.indexOf(e.command.name)!==-1&&(this.sequenceStartTime=Date.now())},this.setKeyboardHandler=function(e,t){if(e&&typeof e=="string"&&e!="ace"){this.$keybindingId=e;var n=this;g.loadModule(["keybinding",e],function(r){n.$keybindingId==e&&n.keyBinding.setKeyboardHandler(r&&r.handler),t&&t()})}else this.$keybindingId=null,this.keyBinding.setKeyboardHandler(e),t&&t()},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(e){if(this.session==e)return;this.curOp&&this.endOperation(),this.curOp={};var t=this.session;if(t){this.session.off("change",this.$onDocumentChange),this.session.off("changeMode",this.$onChangeMode),this.session.off("tokenizerUpdate",this.$onTokenizerUpdate),this.session.off("changeTabSize",this.$onChangeTabSize),this.session.off("changeWrapLimit",this.$onChangeWrapLimit),this.session.off("changeWrapMode",this.$onChangeWrapMode),this.session.off("changeFold",this.$onChangeFold),this.session.off("changeFrontMarker",this.$onChangeFrontMarker),this.session.off("changeBackMarker",this.$onChangeBackMarker),this.session.off("changeBreakpoint",this.$onChangeBreakpoint),this.session.off("changeAnnotation",this.$onChangeAnnotation),this.session.off("changeOverwrite",this.$onCursorChange),this.session.off("changeScrollTop",this.$onScrollTopChange),this.session.off("changeScrollLeft",this.$onScrollLeftChange);var n=this.session.getSelection();n.off("changeCursor",this.$onCursorChange),n.off("changeSelection",this.$onSelectionChange)}this.session=e,e?(this.$onDocumentChange=this.onDocumentChange.bind(this),e.on("change",this.$onDocumentChange),this.renderer.setSession(e),this.$onChangeMode=this.onChangeMode.bind(this),e.on("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),e.on("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.onChangeTabSize.bind(this.renderer),e.on("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),e.on("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),e.on("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),e.on("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.on("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.on("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.on("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.on("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.on("changeOverwrite",this.$onCursorChange),this.$onScrollTopChange=this.onScrollTopChange.bind(this),this.session.on("changeScrollTop",this.$onScrollTopChange),this.$onScrollLeftChange=this.onScrollLeftChange.bind(this),this.session.on("changeScrollLeft",this.$onScrollLeftChange),this.selection=e.getSelection(),this.selection.on("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.on("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.onCursorChange(),this.onScrollTopChange(),this.onScrollLeftChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.session.getUseWrapMode()&&this.renderer.adjustWrapLimit(),this.renderer.updateFull()):(this.selection=null,this.renderer.setSession(e)),this._signal("changeSession",{session:e,oldSession:t}),this.curOp=null,t&&t._signal("changeEditor",{oldEditor:this}),e&&e._signal("changeEditor",{editor:this}),e&&e.bgTokenizer&&e.bgTokenizer.scheduleStart()},this.getSession=function(){return this.session},this.setValue=function(e,t){return this.session.doc.setValue(e),t?t==1?this.navigateFileEnd():t==-1&&this.navigateFileStart():this.selectAll(),e},this.getValue=function(){return this.session.getValue()},this.getSelection=function(){return this.selection},this.resize=function(e){this.renderer.onResize(e)},this.setTheme=function(e,t){this.renderer.setTheme(e,t)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(e){this.renderer.setStyle(e)},this.unsetStyle=function(e){this.renderer.unsetStyle(e)},this.getFontSize=function(){return this.getOption("fontSize")||i.computedStyle(this.container).fontSize},this.setFontSize=function(e){this.setOption("fontSize",e)},this.$highlightBrackets=function(){this.session.$bracketHighlight&&(this.session.removeMarker(this.session.$bracketHighlight),this.session.$bracketHighlight=null);if(this.$highlightPending)return;var e=this;this.$highlightPending=!0,setTimeout(function(){e.$highlightPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=t.findMatchingBracket(e.getCursorPosition());if(n)var r=new p(n.row,n.column,n.row,n.column+1);else if(t.$mode.getMatching)var r=t.$mode.getMatching(e.session);r&&(t.$bracketHighlight=t.addMarker(r,"ace_bracket","text"))},50)},this.$highlightTags=function(){if(this.$highlightTagPending)return;var e=this;this.$highlightTagPending=!0,setTimeout(function(){e.$highlightTagPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=e.getCursorPosition(),r=new y(e.session,n.row,n.column),i=r.getCurrentToken();if(!i||!/\b(?:tag-open|tag-name)/.test(i.type)){t.removeMarker(t.$tagHighlight),t.$tagHighlight=null;return}if(i.type.indexOf("tag-open")!=-1){i=r.stepForward();if(!i)return}var s=i.value,o=0,u=r.stepBackward();if(u.value=="<"){do u=i,i=r.stepForward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="=0)}else{do i=u,u=r.stepBackward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="1)&&(t=!1)}if(e.$highlightLineMarker&&!t)e.removeMarker(e.$highlightLineMarker.id),e.$highlightLineMarker=null;else if(!e.$highlightLineMarker&&t){var n=new p(t.row,t.column,t.row,Infinity);n.id=e.addMarker(n,"ace_active-line","screenLine"),e.$highlightLineMarker=n}else t&&(e.$highlightLineMarker.start.row=t.row,e.$highlightLineMarker.end.row=t.row,e.$highlightLineMarker.start.column=t.column,e._signal("changeBackMarker"))},this.onSelectionChange=function(e){var t=this.session;t.$selectionMarker&&t.removeMarker(t.$selectionMarker),t.$selectionMarker=null;if(!this.selection.isEmpty()){var n=this.selection.getRange(),r=this.getSelectionStyle();t.$selectionMarker=t.addMarker(n,"ace_selection",r)}else this.$updateHighlightActiveLine();var i=this.$highlightSelectedWord&&this.$getSelectionHighLightRegexp();this.session.highlight(i),this._signal("changeSelection")},this.$getSelectionHighLightRegexp=function(){var e=this.session,t=this.getSelectionRange();if(t.isEmpty()||t.isMultiLine())return;var n=t.start.column,r=t.end.column,i=e.getLine(t.start.row),s=i.substring(n,r);if(s.length>5e3||!/[\w\d]/.test(s))return;var o=this.$search.$assembleRegExp({wholeWord:!0,caseSensitive:!0,needle:s}),u=i.substring(n-1,r+1);if(!o.test(u))return;return o},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.updateBreakpoints()},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(e){this.renderer.updateText(),this._emit("changeMode",e)},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getSelectedText=function(){return this.session.getTextRange(this.getSelectionRange())},this.getCopyText=function(){var e=this.getSelectedText(),t=this.session.doc.getNewLineCharacter(),n=!1;if(!e&&this.$copyWithEmptySelection){n=!0;var r=this.selection.getAllRanges();for(var i=0;is.length||i.length<2||!i[1])return this.commands.exec("insertstring",this,t);for(var o=s.length;o--;){var u=s[o];u.isEmpty()||r.remove(u),r.insert(u.start,i[o])}}},this.execCommand=function(e,t){return this.commands.exec(e,this,t)},this.insert=function(e,t){var n=this.session,r=n.getMode(),i=this.getCursorPosition();if(this.getBehavioursEnabled()&&!t){var s=r.transformAction(n.getState(i.row),"insertion",this,n,e);s&&(e!==s.text&&(this.inVirtualSelectionMode||(this.session.mergeUndoDeltas=!1,this.mergeNextCommand=!1)),e=s.text)}e==" "&&(e=this.session.getTabString());if(!this.selection.isEmpty()){var o=this.getSelectionRange();i=this.session.remove(o),this.clearSelection()}else if(this.session.getOverwrite()&&e.indexOf("\n")==-1){var o=new p.fromPoints(i,i);o.end.column+=e.length,this.session.remove(o)}if(e=="\n"||e=="\r\n"){var u=n.getLine(i.row);if(i.column>u.search(/\S|$/)){var a=u.substr(i.column).search(/\S|$/);n.doc.removeInLine(i.row,i.column,i.column+a)}}this.clearSelection();var f=i.column,l=n.getState(i.row),u=n.getLine(i.row),c=r.checkOutdent(l,u,e),h=n.insert(i,e);s&&s.selection&&(s.selection.length==2?this.selection.setSelectionRange(new p(i.row,f+s.selection[0],i.row,f+s.selection[1])):this.selection.setSelectionRange(new p(i.row+s.selection[0],s.selection[1],i.row+s.selection[2],s.selection[3])));if(n.getDocument().isNewLine(e)){var d=r.getNextLineIndent(l,u.slice(0,i.column),n.getTabString());n.insert({row:i.row+1,column:0},d)}c&&r.autoOutdent(l,n,i.row)},this.onTextInput=function(e,t){if(!t)return this.keyBinding.onTextInput(e);this.startOperation({command:{name:"insertstring"}});var n=this.applyComposition.bind(this,e,t);this.selection.rangeCount?this.forEachSelection(n):n(),this.endOperation()},this.applyComposition=function(e,t){if(t.extendLeft||t.extendRight){var n=this.selection.getRange();n.start.column-=t.extendLeft,n.end.column+=t.extendRight,this.selection.setRange(n),!e&&!n.isEmpty()&&this.remove()}(e||!this.selection.isEmpty())&&this.insert(e,!0);if(t.restoreStart||t.restoreEnd){var n=this.selection.getRange();n.start.column-=t.restoreStart,n.end.column-=t.restoreEnd,this.selection.setRange(n)}},this.onCommandKey=function(e,t,n){this.keyBinding.onCommandKey(e,t,n)},this.setOverwrite=function(e){this.session.setOverwrite(e)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(e){this.setOption("scrollSpeed",e)},this.getScrollSpeed=function(){return this.getOption("scrollSpeed")},this.setDragDelay=function(e){this.setOption("dragDelay",e)},this.getDragDelay=function(){return this.getOption("dragDelay")},this.setSelectionStyle=function(e){this.setOption("selectionStyle",e)},this.getSelectionStyle=function(){return this.getOption("selectionStyle")},this.setHighlightActiveLine=function(e){this.setOption("highlightActiveLine",e)},this.getHighlightActiveLine=function(){return this.getOption("highlightActiveLine")},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.setHighlightSelectedWord=function(e){this.setOption("highlightSelectedWord",e)},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setAnimatedScroll=function(e){this.renderer.setAnimatedScroll(e)},this.getAnimatedScroll=function(){return this.renderer.getAnimatedScroll()},this.setShowInvisibles=function(e){this.renderer.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setDisplayIndentGuides=function(e){this.renderer.setDisplayIndentGuides(e)},this.getDisplayIndentGuides=function(){return this.renderer.getDisplayIndentGuides()},this.setShowPrintMargin=function(e){this.renderer.setShowPrintMargin(e)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(e){this.renderer.setPrintMarginColumn(e)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.setReadOnly=function(e){this.setOption("readOnly",e)},this.getReadOnly=function(){return this.getOption("readOnly")},this.setBehavioursEnabled=function(e){this.setOption("behavioursEnabled",e)},this.getBehavioursEnabled=function(){return this.getOption("behavioursEnabled")},this.setWrapBehavioursEnabled=function(e){this.setOption("wrapBehavioursEnabled",e)},this.getWrapBehavioursEnabled=function(){return this.getOption("wrapBehavioursEnabled")},this.setShowFoldWidgets=function(e){this.setOption("showFoldWidgets",e)},this.getShowFoldWidgets=function(){return this.getOption("showFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.remove=function(e){this.selection.isEmpty()&&(e=="left"?this.selection.selectLeft():this.selection.selectRight());var t=this.getSelectionRange();if(this.getBehavioursEnabled()){var n=this.session,r=n.getState(t.start.row),i=n.getMode().transformAction(r,"deletion",this,n,t);if(t.end.column===0){var s=n.getTextRange(t);if(s[s.length-1]=="\n"){var o=n.getLine(t.end.row);/^\s+$/.test(o)&&(t.end.column=o.length)}}i&&(t=i)}this.session.remove(t),this.clearSelection()},this.removeWordRight=function(){this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeWordLeft=function(){this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineStart=function(){this.selection.isEmpty()&&this.selection.selectLineStart(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineEnd=function(){this.selection.isEmpty()&&this.selection.selectLineEnd();var e=this.getSelectionRange();e.start.column==e.end.column&&e.start.row==e.end.row&&(e.end.column=0,e.end.row++),this.session.remove(e),this.clearSelection()},this.splitLine=function(){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var e=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(e)},this.transposeLetters=function(){if(!this.selection.isEmpty())return;var e=this.getCursorPosition(),t=e.column;if(t===0)return;var n=this.session.getLine(e.row),r,i;tt.toLowerCase()?1:0});var i=new p(0,0,0,0);for(var r=e.first;r<=e.last;r++){var s=t.getLine(r);i.start.row=r,i.end.row=r,i.end.column=s.length,t.replace(i,n[r-e.first])}},this.toggleCommentLines=function(){var e=this.session.getState(this.getCursorPosition().row),t=this.$getSelectedRows();this.session.getMode().toggleCommentLines(e,this.session,t.first,t.last)},this.toggleBlockComment=function(){var e=this.getCursorPosition(),t=this.session.getState(e.row),n=this.getSelectionRange();this.session.getMode().toggleBlockComment(t,this.session,n,e)},this.getNumberAt=function(e,t){var n=/[\-]?[0-9]+(?:\.[0-9]+)?/g;n.lastIndex=0;var r=this.session.getLine(e);while(n.lastIndex=t){var s={value:i[0],start:i.index,end:i.index+i[0].length};return s}}return null},this.modifyNumber=function(e){var t=this.selection.getCursor().row,n=this.selection.getCursor().column,r=new p(t,n-1,t,n),i=this.session.getTextRange(r);if(!isNaN(parseFloat(i))&&isFinite(i)){var s=this.getNumberAt(t,n);if(s){var o=s.value.indexOf(".")>=0?s.start+s.value.indexOf(".")+1:s.end,u=s.start+s.value.length-o,a=parseFloat(s.value);a*=Math.pow(10,u),o!==s.end&&n=u&&o<=a&&(n=t,f.selection.clearSelection(),f.moveCursorTo(e,u+r),f.selection.selectTo(e,a+r)),u=a});var l=this.$toggleWordPairs,c;for(var h=0;hp+1)break;p=d.last}l--,u=this.session.$moveLines(h,p,t?0:e),t&&e==-1&&(c=l+1);while(c<=l)o[c].moveBy(u,0),c++;t||(u=0),a+=u}i.fromOrientedRange(i.ranges[0]),i.rangeList.attach(this.session),this.inVirtualSelectionMode=!1}},this.$getSelectedRows=function(e){return e=(e||this.getSelectionRange()).collapseRows(),{first:this.session.getRowFoldStart(e.start.row),last:this.session.getRowFoldEnd(e.end.row)}},this.onCompositionStart=function(e){this.renderer.showComposition(e)},this.onCompositionUpdate=function(e){this.renderer.setCompositionText(e)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(e){return e>=this.getFirstVisibleRow()&&e<=this.getLastVisibleRow()},this.isRowFullyVisible=function(e){return e>=this.renderer.getFirstFullyVisibleRow()&&e<=this.renderer.getLastFullyVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$moveByPage=function(e,t){var n=this.renderer,r=this.renderer.layerConfig,i=e*Math.floor(r.height/r.lineHeight);t===!0?this.selection.$moveSelection(function(){this.moveCursorBy(i,0)}):t===!1&&(this.selection.moveCursorBy(i,0),this.selection.clearSelection());var s=n.scrollTop;n.scrollBy(0,i*r.lineHeight),t!=null&&n.scrollCursorIntoView(null,.5),n.animateScrolling(s)},this.selectPageDown=function(){this.$moveByPage(1,!0)},this.selectPageUp=function(){this.$moveByPage(-1,!0)},this.gotoPageDown=function(){this.$moveByPage(1,!1)},this.gotoPageUp=function(){this.$moveByPage(-1,!1)},this.scrollPageDown=function(){this.$moveByPage(1)},this.scrollPageUp=function(){this.$moveByPage(-1)},this.scrollToRow=function(e){this.renderer.scrollToRow(e)},this.scrollToLine=function(e,t,n,r){this.renderer.scrollToLine(e,t,n,r)},this.centerSelection=function(){var e=this.getSelectionRange(),t={row:Math.floor(e.start.row+(e.end.row-e.start.row)/2),column:Math.floor(e.start.column+(e.end.column-e.start.column)/2)};this.renderer.alignCursor(t,.5)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.selection.selectAll()},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(e,t){this.selection.moveCursorTo(e,t)},this.moveCursorToPosition=function(e){this.selection.moveCursorToPosition(e)},this.jumpToMatching=function(e,t){var n=this.getCursorPosition(),r=new y(this.session,n.row,n.column),i=r.getCurrentToken(),s=i||r.stepForward();if(!s)return;var o,u=!1,a={},f=n.column-s.start,l,c={")":"(","(":"(","]":"[","[":"[","{":"{","}":"{"};do{if(s.value.match(/[{}()\[\]]/g))for(;f=0;--s)this.$tryReplace(n[s],e)&&r++;return this.selection.setSelectionRange(i),r},this.$tryReplace=function(e,t){var n=this.session.getTextRange(e);return t=this.$search.replace(n,t),t!==null?(e.end=this.session.replace(e,t),e):null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(e,t,n){t||(t={}),typeof e=="string"||e instanceof RegExp?t.needle=e:typeof e=="object"&&r.mixin(t,e);var i=this.selection.getRange();t.needle==null&&(e=this.session.getTextRange(i)||this.$search.$options.needle,e||(i=this.session.getWordRange(i.start.row,i.start.column),e=this.session.getTextRange(i)),this.$search.set({needle:e})),this.$search.set(t),t.start||this.$search.set({start:i});var s=this.$search.find(this.session);if(t.preventScroll)return s;if(s)return this.revealRange(s,n),s;t.backwards?i.start=i.end:i.end=i.start,this.selection.setRange(i)},this.findNext=function(e,t){this.find({skipCurrent:!0,backwards:!1},e,t)},this.findPrevious=function(e,t){this.find(e,{skipCurrent:!0,backwards:!0},t)},this.revealRange=function(e,t){this.session.unfold(e),this.selection.setSelectionRange(e);var n=this.renderer.scrollTop;this.renderer.scrollSelectionIntoView(e.start,e.end,.5),t!==!1&&this.renderer.animateScrolling(n)},this.undo=function(){this.session.getUndoManager().undo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.redo=function(){this.session.getUndoManager().redo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.destroy=function(){this.renderer.destroy(),this._signal("destroy",this),this.session&&this.session.destroy()},this.setAutoScrollEditorIntoView=function(e){if(!e)return;var t,n=this,r=!1;this.$scrollAnchor||(this.$scrollAnchor=document.createElement("div"));var i=this.$scrollAnchor;i.style.cssText="position:absolute",this.container.insertBefore(i,this.container.firstChild);var s=this.on("changeSelection",function(){r=!0}),o=this.renderer.on("beforeRender",function(){r&&(t=n.renderer.container.getBoundingClientRect())}),u=this.renderer.on("afterRender",function(){if(r&&t&&(n.isFocused()||n.searchBox&&n.searchBox.isFocused())){var e=n.renderer,s=e.$cursorLayer.$pixelPos,o=e.layerConfig,u=s.top-o.offset;s.top>=0&&u+t.top<0?r=!0:s.topwindow.innerHeight?r=!1:r=null,r!=null&&(i.style.top=u+"px",i.style.left=s.left+"px",i.style.height=o.lineHeight+"px",i.scrollIntoView(r)),r=t=null}});this.setAutoScrollEditorIntoView=function(e){if(e)return;delete this.setAutoScrollEditorIntoView,this.off("changeSelection",s),this.renderer.off("afterRender",u),this.renderer.off("beforeRender",o)}},this.$resetCursorStyle=function(){var e=this.$cursorStyle||"ace",t=this.renderer.$cursorLayer;if(!t)return;t.setSmoothBlinking(/smooth/.test(e)),t.isBlinking=!this.$readOnly&&e!="wide",i.setCssClass(t.element,"ace_slim-cursors",/slim/.test(e))}}.call(w.prototype),g.defineOptions(w.prototype,"editor",{selectionStyle:{set:function(e){this.onSelectionChange(),this._signal("changeSelectionStyle",{data:e})},initialValue:"line"},highlightActiveLine:{set:function(){this.$updateHighlightActiveLine()},initialValue:!0},highlightSelectedWord:{set:function(e){this.$onSelectionChange()},initialValue:!0},readOnly:{set:function(e){this.textInput.setReadOnly(e),this.$resetCursorStyle()},initialValue:!1},copyWithEmptySelection:{set:function(e){this.textInput.setCopyWithEmptySelection(e)},initialValue:!1},cursorStyle:{set:function(e){this.$resetCursorStyle()},values:["ace","slim","smooth","wide"],initialValue:"ace"},mergeUndoDeltas:{values:[!1,!0,"always"],initialValue:!0},behavioursEnabled:{initialValue:!0},wrapBehavioursEnabled:{initialValue:!0},autoScrollEditorIntoView:{set:function(e){this.setAutoScrollEditorIntoView(e)}},keyboardHandler:{set:function(e){this.setKeyboardHandler(e)},get:function(){return this.$keybindingId},handlesSet:!0},value:{set:function(e){this.session.setValue(e)},get:function(){return this.getValue()},handlesSet:!0,hidden:!0},session:{set:function(e){this.setSession(e)},get:function(){return this.session},handlesSet:!0,hidden:!0},showLineNumbers:{set:function(e){this.renderer.$gutterLayer.setShowLineNumbers(e),this.renderer.$loop.schedule(this.renderer.CHANGE_GUTTER),e&&this.$relativeLineNumbers?E.attach(this):E.detach(this)},initialValue:!0},relativeLineNumbers:{set:function(e){this.$showLineNumbers&&e?E.attach(this):E.detach(this)}},hScrollBarAlwaysVisible:"renderer",vScrollBarAlwaysVisible:"renderer",highlightGutterLine:"renderer",animatedScroll:"renderer",showInvisibles:"renderer",showPrintMargin:"renderer",printMarginColumn:"renderer",printMargin:"renderer",fadeFoldWidgets:"renderer",showFoldWidgets:"renderer",displayIndentGuides:"renderer",showGutter:"renderer",fontSize:"renderer",fontFamily:"renderer",maxLines:"renderer",minLines:"renderer",scrollPastEnd:"renderer",fixedWidthGutter:"renderer",theme:"renderer",hasCssTransforms:"renderer",maxPixelHeight:"renderer",useTextareaForIME:"renderer",scrollSpeed:"$mouseHandler",dragDelay:"$mouseHandler",dragEnabled:"$mouseHandler",focusTimeout:"$mouseHandler",tooltipFollowsMouse:"$mouseHandler",firstLineNumber:"session",overwrite:"session",newLineMode:"session",useWorker:"session",useSoftTabs:"session",navigateWithinSoftTabs:"session",tabSize:"session",wrap:"session",indentedSoftWrap:"session",foldStyle:"session",mode:"session"});var E={getText:function(e,t){return(Math.abs(e.selection.lead.row-t)||t+1+(t<9?"\u00b7":""))+""},getWidth:function(e,t,n){return Math.max(t.toString().length,(n.lastRow+1).toString().length,2)*n.characterWidth},update:function(e,t){t.renderer.$loop.schedule(t.renderer.CHANGE_GUTTER)},attach:function(e){e.renderer.$gutterLayer.$renderer=this,e.on("changeSelection",this.update),this.update(null,e)},detach:function(e){e.renderer.$gutterLayer.$renderer==this&&(e.renderer.$gutterLayer.$renderer=null),e.off("changeSelection",this.update),this.update(null,e)}};t.Editor=w}),ace.define("ace/undomanager",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){for(var n=t;n--;){var r=e[n];if(r&&!r[0].ignore){while(n0){a.row+=i,a.column+=a.row==r.row?s:0;continue}!t&&l<=0&&(a.row=n.row,a.column=n.column,l===0&&(a.bias=1))}}function f(e){return{row:e.row,column:e.column}}function l(e){return{start:f(e.start),end:f(e.end),action:e.action,lines:e.lines.slice()}}function c(e){e=e||this;if(Array.isArray(e))return e.map(c).join("\n");var t="";e.action?(t=e.action=="insert"?"+":"-",t+="["+e.lines+"]"):e.value&&(Array.isArray(e.value)?t=e.value.map(h).join("\n"):t=h(e.value)),e.start&&(t+=h(e));if(e.id||e.rev)t+=" ("+(e.id||e.rev)+")";return t}function h(e){return e.start.row+":"+e.start.column+"=>"+e.end.row+":"+e.end.column}function p(e,t){var n=e.action=="insert",r=t.action=="insert";if(n&&r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}else if(!n&&r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(!n&&!r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}return[t,e]}function d(e,t){for(var n=e.length;n--;)for(var r=0;r=0?m(e,t,-1):o(e.start,t.start)<=0?m(t,e,1):(m(e,s.fromPoints(t.start,e.start),-1),m(t,e,1));else if(!n&&r)o(t.start,e.end)>=0?m(t,e,-1):o(t.start,e.start)<=0?m(e,t,1):(m(t,s.fromPoints(e.start,t.start),-1),m(e,t,1));else if(!n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0)){var i,u;return o(e.start,t.start)<0&&(i=e,e=y(e,t.start)),o(e.end,t.end)>0&&(u=y(e,t.end)),g(t.end,e.start,e.end,-1),u&&!i&&(e.lines=u.lines,e.start=u.start,e.end=u.end,u=e),[t,i,u].filter(Boolean)}m(e,t,-1)}return[t,e]}function m(e,t,n){g(e.start,t.start,t.end,n),g(e.end,t.start,t.end,n)}function g(e,t,n,r){e.row==(r==1?t:n).row&&(e.column+=r*(n.column-t.column)),e.row+=r*(n.row-t.row)}function y(e,t){var n=e.lines,r=e.end;e.end=f(t);var i=e.end.row-e.start.row,s=n.splice(i,n.length),o=i?t.column:t.column-e.start.column;n.push(s[0].substring(0,o)),s[0]=s[0].substr(o);var u={start:f(t),end:r,lines:s,action:e.action};return u}function b(e,t){t=l(t);for(var n=e.length;n--;){var r=e[n];for(var i=0;i0},this.canRedo=function(){return this.$redoStack.length>0},this.bookmark=function(e){e==undefined&&(e=this.$rev),this.mark=e},this.isAtBookmark=function(){return this.$rev===this.mark},this.toJSON=function(){},this.fromJSON=function(){},this.hasUndo=this.canUndo,this.hasRedo=this.canRedo,this.isClean=this.isAtBookmark,this.markClean=this.bookmark,this.$prettyPrint=function(e){return e?c(e):c(this.$undoStack)+"\n---\n"+c(this.$redoStack)}}).call(r.prototype);var s=e("./range").Range,o=s.comparePoints,u=s.comparePoints;t.UndoManager=r}),ace.define("ace/layer/lines",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e,t){this.element=e,this.canvasHeight=t||5e5,this.element.style.height=this.canvasHeight*2+"px",this.cells=[],this.cellCache=[],this.$offsetCoefficient=0};(function(){this.moveContainer=function(e){r.translate(this.element,0,-(e.firstRowScreen*e.lineHeight%this.canvasHeight)-e.offset*this.$offsetCoefficient)},this.pageChanged=function(e,t){return Math.floor(e.firstRowScreen*e.lineHeight/this.canvasHeight)!==Math.floor(t.firstRowScreen*t.lineHeight/this.canvasHeight)},this.computeLineTop=function(e,t,n){var r=t.firstRowScreen*t.lineHeight,i=Math.floor(r/this.canvasHeight),s=n.documentToScreenRow(e,0)*t.lineHeight;return s-i*this.canvasHeight},this.computeLineHeight=function(e,t,n){return t.lineHeight*n.getRowLength(e)},this.getLength=function(){return this.cells.length},this.get=function(e){return this.cells[e]},this.shift=function(){this.$cacheCell(this.cells.shift())},this.pop=function(){this.$cacheCell(this.cells.pop())},this.push=function(e){if(Array.isArray(e)){this.cells.push.apply(this.cells,e);var t=r.createFragment(this.element);for(var n=0;ns&&(a=i.end.row+1,i=t.getNextFoldLine(a,i),s=i?i.start.row:Infinity);if(a>r){while(this.$lines.getLength()>u+1)this.$lines.pop();break}o=this.$lines.get(++u),o?o.row=a:(o=this.$lines.createCell(a,e,this.session,f),this.$lines.push(o)),this.$renderCell(o,e,i,a),a++}this._signal("afterRender"),this.$updateGutterWidth(e)},this.$updateGutterWidth=function(e){var t=this.session,n=t.gutterRenderer||this.$renderer,r=t.$firstLineNumber,i=this.$lines.last()?this.$lines.last().text:"";if(this.$fixedWidth||t.$useWrapMode)i=t.getLength()+r-1;var s=n?n.getWidth(t,i,e):i.toString().length*e.characterWidth,o=this.$padding||this.$computePadding();s+=o.left+o.right,s!==this.gutterWidth&&!isNaN(s)&&(this.gutterWidth=s,this.element.parentNode.style.width=this.element.style.width=Math.ceil(this.gutterWidth)+"px",this._signal("changeGutterWidth",s))},this.$updateCursorRow=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.getCursor();if(this.$cursorRow===e.row)return;this.$cursorRow=e.row},this.updateLineHighlight=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.cursor.row;this.$cursorRow=e;if(this.$cursorCell&&this.$cursorCell.row==e)return;this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ",""));var t=this.$lines.cells;this.$cursorCell=null;for(var n=0;n=this.$cursorRow){if(r.row>this.$cursorRow){var i=this.session.getFoldLine(this.$cursorRow);if(!(n>0&&i&&i.start.row==t[n-1].row))break;r=t[n-1]}r.element.className="ace_gutter-active-line "+r.element.className,this.$cursorCell=r;break}}},this.scrollLines=function(e){var t=this.config;this.config=e,this.$updateCursorRow();if(this.$lines.pageChanged(t,e))return this.update(e);this.$lines.moveContainer(e);var n=Math.min(e.lastRow+e.gutterOffset,this.session.getLength()-1),r=this.oldLastRow;this.oldLastRow=n;if(!t||r0;i--)this.$lines.shift();if(r>n)for(var i=this.session.getFoldedRowCount(n+1,r);i>0;i--)this.$lines.pop();e.firstRowr&&this.$lines.push(this.$renderLines(e,r+1,n)),this.updateLineHighlight(),this._signal("afterRender"),this.$updateGutterWidth(e)},this.$renderLines=function(e,t,n){var r=[],i=t,s=this.session.getNextFoldLine(i),o=s?s.start.row:Infinity;for(;;){i>o&&(i=s.end.row+1,s=this.session.getNextFoldLine(i,s),o=s?s.start.row:Infinity);if(i>n)break;var u=this.$lines.createCell(i,e,this.session,f);this.$renderCell(u,e,s,i),r.push(u),i++}return r},this.$renderCell=function(e,t,n,i){var s=e.element,o=this.session,u=s.childNodes[0],a=s.childNodes[1],f=o.$firstLineNumber,l=o.$breakpoints,c=o.$decorations,h=o.gutterRenderer||this.$renderer,p=this.$showFoldWidgets&&o.foldWidgets,d=n?n.start.row:Number.MAX_VALUE,v="ace_gutter-cell ";this.$highlightGutterLine&&(i==this.$cursorRow||n&&i=d&&this.$cursorRow<=n.end.row)&&(v+="ace_gutter-active-line ",this.$cursorCell!=e&&(this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ","")),this.$cursorCell=e)),l[i]&&(v+=l[i]),c[i]&&(v+=c[i]),this.$annotations[i]&&(v+=this.$annotations[i].className),s.className!=v&&(s.className=v);if(p){var m=p[i];m==null&&(m=p[i]=o.getFoldWidget(i))}if(m){var v="ace_fold-widget ace_"+m;m=="start"&&i==d&&in.right-t.right)return"foldWidgets"}}).call(a.prototype),t.Gutter=a}),ace.define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../range").Range,i=e("../lib/dom"),s=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_marker-layer",e.appendChild(this.element)};(function(){function e(e,t,n,r){return(e?1:0)|(t?2:0)|(n?4:0)|(r?8:0)}this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setMarkers=function(e){this.markers=e},this.elt=function(e,t){var n=this.i!=-1&&this.element.childNodes[this.i];n?this.i++:(n=document.createElement("div"),this.element.appendChild(n),this.i=-1),n.style.cssText=t,n.className=e},this.update=function(e){if(!e)return;this.config=e,this.i=0;var t;for(var n in this.markers){var r=this.markers[n];if(!r.range){r.update(t,this,this.session,e);continue}var i=r.range.clipRows(e.firstRow,e.lastRow);if(i.isEmpty())continue;i=i.toScreenRange(this.session);if(r.renderer){var s=this.$getTop(i.start.row,e),o=this.$padding+i.start.column*e.characterWidth;r.renderer(t,i,o,s,e)}else r.type=="fullLine"?this.drawFullLineMarker(t,i,r.clazz,e):r.type=="screenLine"?this.drawScreenLineMarker(t,i,r.clazz,e):i.isMultiLine()?r.type=="text"?this.drawTextMarker(t,i,r.clazz,e):this.drawMultiLineMarker(t,i,r.clazz,e):this.drawSingleLineMarker(t,i,r.clazz+" ace_start"+" ace_br15",e)}if(this.i!=-1)while(this.ip,l==f),s,l==f?0:1,o)},this.drawMultiLineMarker=function(e,t,n,r,i){var s=this.$padding,o=r.lineHeight,u=this.$getTop(t.start.row,r),a=s+t.start.column*r.characterWidth;i=i||"";if(this.session.$bidiHandler.isBidiRow(t.start.row)){var f=t.clone();f.end.row=f.start.row,f.end.column=this.session.getLine(f.start.row).length,this.drawBidiSingleLineMarker(e,f,n+" ace_br1 ace_start",r,null,i)}else this.elt(n+" ace_br1 ace_start","height:"+o+"px;"+"right:0;"+"top:"+u+"px;left:"+a+"px;"+(i||""));if(this.session.$bidiHandler.isBidiRow(t.end.row)){var f=t.clone();f.start.row=f.end.row,f.start.column=0,this.drawBidiSingleLineMarker(e,f,n+" ace_br12",r,null,i)}else{u=this.$getTop(t.end.row,r);var l=t.end.column*r.characterWidth;this.elt(n+" ace_br12","height:"+o+"px;"+"width:"+l+"px;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))}o=(t.end.row-t.start.row-1)*r.lineHeight;if(o<=0)return;u=this.$getTop(t.start.row+1,r);var c=(t.start.column?1:0)|(t.end.column?0:8);this.elt(n+(c?" ace_br"+c:""),"height:"+o+"px;"+"right:0;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))},this.drawSingleLineMarker=function(e,t,n,r,i,s){if(this.session.$bidiHandler.isBidiRow(t.start.row))return this.drawBidiSingleLineMarker(e,t,n,r,i,s);var o=r.lineHeight,u=(t.end.column+(i||0)-t.start.column)*r.characterWidth,a=this.$getTop(t.start.row,r),f=this.$padding+t.start.column*r.characterWidth;this.elt(n,"height:"+o+"px;"+"width:"+u+"px;"+"top:"+a+"px;"+"left:"+f+"px;"+(s||""))},this.drawBidiSingleLineMarker=function(e,t,n,r,i,s){var o=r.lineHeight,u=this.$getTop(t.start.row,r),a=this.$padding,f=this.session.$bidiHandler.getSelections(t.start.column,t.end.column);f.forEach(function(e){this.elt(n,"height:"+o+"px;"+"width:"+e.width+(i||0)+"px;"+"top:"+u+"px;"+"left:"+(a+e.left)+"px;"+(s||""))},this)},this.drawFullLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;t.start.row!=t.end.row&&(o+=this.$getTop(t.end.row,r)-s),this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))},this.drawScreenLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))}}).call(s.prototype),t.Marker=s}),ace.define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/layer/lines","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("./lines").Lines,u=e("../lib/event_emitter").EventEmitter,a=function(e){this.dom=i,this.element=this.dom.createElement("div"),this.element.className="ace_layer ace_text-layer",e.appendChild(this.element),this.$updateEolChar=this.$updateEolChar.bind(this),this.$lines=new o(this.element)};(function(){r.implement(this,u),this.EOF_CHAR="\u00b6",this.EOL_CHAR_LF="\u00ac",this.EOL_CHAR_CRLF="\u00a4",this.EOL_CHAR=this.EOL_CHAR_LF,this.TAB_CHAR="\u2014",this.SPACE_CHAR="\u00b7",this.$padding=0,this.MAX_LINE_LENGTH=1e4,this.$updateEolChar=function(){var e=this.session.doc,t=e.getNewLineCharacter()=="\n"&&e.getNewLineMode()!="windows",n=t?this.EOL_CHAR_LF:this.EOL_CHAR_CRLF;if(this.EOL_CHAR!=n)return this.EOL_CHAR=n,!0},this.setPadding=function(e){this.$padding=e,this.element.style.margin="0 "+e+"px"},this.getLineHeight=function(){return this.$fontMetrics.$characterSize.height||0},this.getCharacterWidth=function(){return this.$fontMetrics.$characterSize.width||0},this.$setFontMetrics=function(e){this.$fontMetrics=e,this.$fontMetrics.on("changeCharacterSize",function(e){this._signal("changeCharacterSize",e)}.bind(this)),this.$pollSizeChanges()},this.checkForSizeChanges=function(){this.$fontMetrics.checkForSizeChanges()},this.$pollSizeChanges=function(){return this.$pollSizeChangesTimer=this.$fontMetrics.$pollSizeChanges()},this.setSession=function(e){this.session=e,e&&this.$computeTabString()},this.showInvisibles=!1,this.setShowInvisibles=function(e){return this.showInvisibles==e?!1:(this.showInvisibles=e,this.$computeTabString(),!0)},this.displayIndentGuides=!0,this.setDisplayIndentGuides=function(e){return this.displayIndentGuides==e?!1:(this.displayIndentGuides=e,this.$computeTabString(),!0)},this.$tabStrings=[],this.onChangeTabSize=this.$computeTabString=function(){var e=this.session.getTabSize();this.tabSize=e;var t=this.$tabStrings=[0];for(var n=1;nl&&(u=a.end.row+1,a=this.session.getNextFoldLine(u,a),l=a?a.start.row:Infinity);if(u>i)break;var c=s[o++];if(c){this.dom.removeChildren(c),this.$renderLine(c,u,u==l?a:!1);var h=e.lineHeight*this.session.getRowLength(u)+"px";c.style.height!=h&&(f=!0,c.style.height=h)}u++}if(f)while(o0;i--)this.$lines.shift();if(t.lastRow>e.lastRow)for(var i=this.session.getFoldedRowCount(e.lastRow+1,t.lastRow);i>0;i--)this.$lines.pop();e.firstRowt.lastRow&&this.$lines.push(this.$renderLinesFragment(e,t.lastRow+1,e.lastRow))},this.$renderLinesFragment=function(e,t,n){var r=[],s=t,o=this.session.getNextFoldLine(s),u=o?o.start.row:Infinity;for(;;){s>u&&(s=o.end.row+1,o=this.session.getNextFoldLine(s,o),u=o?o.start.row:Infinity);if(s>n)break;var a=this.$lines.createCell(s,e,this.session),f=a.element;this.dom.removeChildren(f),i.setStyle(f.style,"height",this.$lines.computeLineHeight(s,e,this.session)+"px"),i.setStyle(f.style,"top",this.$lines.computeLineTop(s,e,this.session)+"px"),this.$renderLine(f,s,s==u?o:!1),this.$useLineGroups()?f.className="ace_line_group":f.className="ace_line",r.push(a),s++}return r},this.update=function(e){this.$lines.moveContainer(e),this.config=e;var t=e.firstRow,n=e.lastRow,r=this.$lines;while(r.getLength())r.pop();r.push(this.$renderLinesFragment(e,t,n))},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderToken=function(e,t,n,r){var o=this,u=/(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g,a=this.dom.createFragment(this.element),f,l=0;while(f=u.exec(r)){var c=f[1],h=f[2],p=f[3],d=f[4],v=f[5];if(!o.showInvisibles&&h)continue;var m=l!=f.index?r.slice(l,f.index):"";l=f.index+f[0].length,m&&a.appendChild(this.dom.createTextNode(m,this.element));if(c){var g=o.session.getScreenTabSize(t+f.index);a.appendChild(o.$tabStrings[g].cloneNode(!0)),t+=g-1}else if(h)if(o.showInvisibles){var y=this.dom.createElement("span");y.className="ace_invisible ace_invisible_space",y.textContent=s.stringRepeat(o.SPACE_CHAR,h.length),a.appendChild(y)}else a.appendChild(this.com.createTextNode(h,this.element));else if(p){var y=this.dom.createElement("span");y.className="ace_invisible ace_invisible_space ace_invalid",y.textContent=s.stringRepeat(o.SPACE_CHAR,p.length),a.appendChild(y)}else if(d){var b=o.showInvisibles?o.SPACE_CHAR:"";t+=1;var y=this.dom.createElement("span");y.style.width=o.config.characterWidth*2+"px",y.className=o.showInvisibles?"ace_cjk ace_invisible ace_invisible_space":"ace_cjk",y.textContent=o.showInvisibles?o.SPACE_CHAR:"",a.appendChild(y)}else if(v){t+=1;var y=i.createElement("span");y.style.width=o.config.characterWidth*2+"px",y.className="ace_cjk",y.textContent=v,a.appendChild(y)}}a.appendChild(this.dom.createTextNode(l?r.slice(l):r,this.element));if(!this.$textToken[n.type]){var w="ace_"+n.type.replace(/\./g," ace_"),y=this.dom.createElement("span");n.type=="fold"&&(y.style.width=n.value.length*this.config.characterWidth+"px"),y.className=w,y.appendChild(a),e.appendChild(y)}else e.appendChild(a);return t+r.length},this.renderIndentGuide=function(e,t,n){var r=t.search(this.$indentGuideRe);if(r<=0||r>=n)return t;if(t[0]==" "){r-=r%this.tabSize;var i=r/this.tabSize;for(var s=0;s=o)u=this.$renderToken(a,u,l,c.substring(0,o-r)),c=c.substring(o-r),r=o,a=this.$createLineElement(),e.appendChild(a),a.appendChild(this.dom.createTextNode(s.stringRepeat("\u00a0",n.indent),this.element)),i++,u=0,o=n[i]||Number.MAX_VALUE;c.length!=0&&(r+=c.length,u=this.$renderToken(a,u,l,c))}}},this.$renderSimpleLine=function(e,t){var n=0,r=t[0],i=r.value;this.displayIndentGuides&&(i=this.renderIndentGuide(e,i)),i&&(n=this.$renderToken(e,n,r,i));for(var s=1;sthis.MAX_LINE_LENGTH)return this.$renderOverflowMessage(e,n,r,i);n=this.$renderToken(e,n,r,i)}},this.$renderOverflowMessage=function(e,t,n,r){this.$renderToken(e,t,n,r.slice(0,this.MAX_LINE_LENGTH-t));var i=this.dom.createElement("span");i.className="ace_inline_button ace_keyword ace_toggle_wrap",i.style.position="absolute",i.style.right="0",i.textContent="",e.appendChild(i)},this.$renderLine=function(e,t,n){!n&&n!=0&&(n=this.session.getFoldLine(t));if(n)var r=this.$getFoldLineTokens(t,n);else var r=this.session.getTokens(t);var i=e;if(r.length){var s=this.session.getRowSplitData(t);if(s&&s.length){this.$renderWrappedLine(e,r,s);var i=e.lastChild}else{var i=e;this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i)),this.$renderSimpleLine(i,r)}}else this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i));if(this.showInvisibles&&i){n&&(t=n.end.row);var o=this.dom.createElement("span");o.className="ace_invisible ace_invisible_eol",o.textContent=t==this.session.getLength()-1?this.EOF_CHAR:this.EOL_CHAR,i.appendChild(o)}},this.$getFoldLineTokens=function(e,t){function i(e,t,n){var i=0,s=0;while(s+e[i].value.lengthn-t&&(o=o.substring(0,n-t)),r.push({type:e[i].type,value:o}),s=t+o.length,i+=1}while(sn?r.push({type:e[i].type,value:o.substring(0,n-s)}):r.push(e[i]),s+=o.length,i+=1}}var n=this.session,r=[],s=n.getTokens(e);return t.walk(function(e,t,o,u,a){e!=null?r.push({type:"fold",value:e}):(a&&(s=n.getTokens(t)),s.length&&i(s,u,o))},t.end.row,this.session.getLine(t.end.row).length),r},this.$useLineGroups=function(){return this.session.getUseWrapMode()},this.destroy=function(){}}).call(a.prototype),t.Text=a}),ace.define("ace/layer/cursor",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_cursor-layer",e.appendChild(this.element),this.isVisible=!1,this.isBlinking=!0,this.blinkInterval=1e3,this.smoothBlinking=!1,this.cursors=[],this.cursor=this.addCursor(),r.addCssClass(this.element,"ace_hidden-cursors"),this.$updateCursors=this.$updateOpacity.bind(this)};(function(){this.$updateOpacity=function(e){var t=this.cursors;for(var n=t.length;n--;)r.setStyle(t[n].style,"opacity",e?"":"0")},this.$startCssAnimation=function(){var e=this.cursors;for(var t=e.length;t--;)e[t].style.animationDuration=this.blinkInterval+"ms";setTimeout(function(){r.addCssClass(this.element,"ace_animate-blinking")}.bind(this))},this.$stopCssAnimation=function(){r.removeCssClass(this.element,"ace_animate-blinking")},this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setBlinking=function(e){e!=this.isBlinking&&(this.isBlinking=e,this.restartTimer())},this.setBlinkInterval=function(e){e!=this.blinkInterval&&(this.blinkInterval=e,this.restartTimer())},this.setSmoothBlinking=function(e){e!=this.smoothBlinking&&(this.smoothBlinking=e,r.setCssClass(this.element,"ace_smooth-blinking",e),this.$updateCursors(!0),this.restartTimer())},this.addCursor=function(){var e=r.createElement("div");return e.className="ace_cursor",this.element.appendChild(e),this.cursors.push(e),e},this.removeCursor=function(){if(this.cursors.length>1){var e=this.cursors.pop();return e.parentNode.removeChild(e),e}},this.hideCursor=function(){this.isVisible=!1,r.addCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.showCursor=function(){this.isVisible=!0,r.removeCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.restartTimer=function(){var e=this.$updateCursors;clearInterval(this.intervalId),clearTimeout(this.timeoutId),this.$stopCssAnimation(),this.smoothBlinking&&r.removeCssClass(this.element,"ace_smooth-blinking"),e(!0);if(!this.isBlinking||!this.blinkInterval||!this.isVisible){this.$stopCssAnimation();return}this.smoothBlinking&&setTimeout(function(){r.addCssClass(this.element,"ace_smooth-blinking")}.bind(this));if(r.HAS_CSS_ANIMATION)this.$startCssAnimation();else{var t=function(){this.timeoutId=setTimeout(function(){e(!1)},.6*this.blinkInterval)}.bind(this);this.intervalId=setInterval(function(){e(!0),t()},this.blinkInterval),t()}},this.getPixelPosition=function(e,t){if(!this.config||!this.session)return{left:0,top:0};e||(e=this.session.selection.getCursor());var n=this.session.documentToScreenPosition(e),r=this.$padding+(this.session.$bidiHandler.isBidiRow(n.row,e.row)?this.session.$bidiHandler.getPosLeft(n.column):n.column*this.config.characterWidth),i=(n.row-(t?this.config.firstRowScreen:0))*this.config.lineHeight;return{left:r,top:i}},this.isCursorInView=function(e,t){return e.top>=0&&e.tope.height+e.offset||o.top<0)&&n>1)continue;var u=this.cursors[i++]||this.addCursor(),a=u.style;this.drawCursor?this.drawCursor(u,o,e,t[n],this.session):this.isCursorInView(o,e)?(r.setStyle(a,"display","block"),r.translate(u,o.left,o.top),r.setStyle(a,"width",Math.round(e.characterWidth)+"px"),r.setStyle(a,"height",e.lineHeight+"px")):r.setStyle(a,"display","none")}while(this.cursors.length>i)this.removeCursor();var f=this.session.getOverwrite();this.$setOverwrite(f),this.$pixelPos=o,this.restartTimer()},this.drawCursor=null,this.$setOverwrite=function(e){e!=this.overwrite&&(this.overwrite=e,e?r.addCssClass(this.element,"ace_overwrite-cursors"):r.removeCssClass(this.element,"ace_overwrite-cursors"))},this.destroy=function(){clearInterval(this.intervalId),clearTimeout(this.timeoutId)}}).call(i.prototype),t.Cursor=i}),ace.define("ace/scrollbar",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/event"),o=e("./lib/event_emitter").EventEmitter,u=32768,a=function(e){this.element=i.createElement("div"),this.element.className="ace_scrollbar ace_scrollbar"+this.classSuffix,this.inner=i.createElement("div"),this.inner.className="ace_scrollbar-inner",this.element.appendChild(this.inner),e.appendChild(this.element),this.setVisible(!1),this.skipEvent=!1,s.addListener(this.element,"scroll",this.onScroll.bind(this)),s.addListener(this.element,"mousedown",s.preventDefault)};(function(){r.implement(this,o),this.setVisible=function(e){this.element.style.display=e?"":"none",this.isVisible=e,this.coeff=1}}).call(a.prototype);var f=function(e,t){a.call(this,e),this.scrollTop=0,this.scrollHeight=0,t.$scrollbarWidth=this.width=i.scrollbarWidth(e.ownerDocument),this.inner.style.width=this.element.style.width=(this.width||15)+5+"px",this.$minWidth=0};r.inherits(f,a),function(){this.classSuffix="-v",this.onScroll=function(){if(!this.skipEvent){this.scrollTop=this.element.scrollTop;if(this.coeff!=1){var e=this.element.clientHeight/this.scrollHeight;this.scrollTop=this.scrollTop*(1-e)/(this.coeff-e)}this._emit("scroll",{data:this.scrollTop})}this.skipEvent=!1},this.getWidth=function(){return Math.max(this.isVisible?this.width:0,this.$minWidth||0)},this.setHeight=function(e){this.element.style.height=e+"px"},this.setInnerHeight=this.setScrollHeight=function(e){this.scrollHeight=e,e>u?(this.coeff=u/e,e=u):this.coeff!=1&&(this.coeff=1),this.inner.style.height=e+"px"},this.setScrollTop=function(e){this.scrollTop!=e&&(this.skipEvent=!0,this.scrollTop=e,this.element.scrollTop=e*this.coeff)}}.call(f.prototype);var l=function(e,t){a.call(this,e),this.scrollLeft=0,this.height=t.$scrollbarWidth,this.inner.style.height=this.element.style.height=(this.height||15)+5+"px"};r.inherits(l,a),function(){this.classSuffix="-h",this.onScroll=function(){this.skipEvent||(this.scrollLeft=this.element.scrollLeft,this._emit("scroll",{data:this.scrollLeft})),this.skipEvent=!1},this.getHeight=function(){return this.isVisible?this.height:0},this.setWidth=function(e){this.element.style.width=e+"px"},this.setInnerWidth=function(e){this.inner.style.width=e+"px"},this.setScrollWidth=function(e){this.inner.style.width=e+"px"},this.setScrollLeft=function(e){this.scrollLeft!=e&&(this.skipEvent=!0,this.scrollLeft=this.element.scrollLeft=e)}}.call(l.prototype),t.ScrollBar=f,t.ScrollBarV=f,t.ScrollBarH=l,t.VScrollBar=f,t.HScrollBar=l}),ace.define("ace/renderloop",["require","exports","module","ace/lib/event"],function(e,t,n){"use strict";var r=e("./lib/event"),i=function(e,t){this.onRender=e,this.pending=!1,this.changes=0,this.$recursionLimit=2,this.window=t||window;var n=this;this._flush=function(e){n.pending=!1;var t=n.changes;t&&(r.blockIdle(100),n.changes=0,n.onRender(t));if(n.changes){if(n.$recursionLimit--<0)return;n.schedule()}else n.$recursionLimit=2}};(function(){this.schedule=function(e){this.changes=this.changes|e,this.changes&&!this.pending&&(r.nextFrame(this._flush),this.pending=!0)},this.clear=function(e){var t=this.changes;return this.changes=0,t}}).call(i.prototype),t.RenderLoop=i}),ace.define("ace/layer/font_metrics",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/event","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/event"),u=e("../lib/useragent"),a=e("../lib/event_emitter").EventEmitter,f=256,l=typeof ResizeObserver=="function",c=200,h=t.FontMetrics=function(e){this.el=i.createElement("div"),this.$setMeasureNodeStyles(this.el.style,!0),this.$main=i.createElement("div"),this.$setMeasureNodeStyles(this.$main.style),this.$measureNode=i.createElement("div"),this.$setMeasureNodeStyles(this.$measureNode.style),this.el.appendChild(this.$main),this.el.appendChild(this.$measureNode),e.appendChild(this.el),this.$measureNode.innerHTML=s.stringRepeat("X",f),this.$characterSize={width:0,height:0},l?this.$addObserver():this.checkForSizeChanges()};(function(){r.implement(this,a),this.$characterSize={width:0,height:0},this.$setMeasureNodeStyles=function(e,t){e.width=e.height="auto",e.left=e.top="0px",e.visibility="hidden",e.position="absolute",e.whiteSpace="pre",u.isIE<8?e["font-family"]="inherit":e.font="inherit",e.overflow=t?"hidden":"visible"},this.checkForSizeChanges=function(e){e===undefined&&(e=this.$measureSizes());if(e&&(this.$characterSize.width!==e.width||this.$characterSize.height!==e.height)){this.$measureNode.style.fontWeight="bold";var t=this.$measureSizes();this.$measureNode.style.fontWeight="",this.$characterSize=e,this.charSizes=Object.create(null),this.allowBoldFonts=t&&t.width===e.width&&t.height===e.height,this._emit("changeCharacterSize",{data:e})}},this.$addObserver=function(){var e=this;this.$observer=new window.ResizeObserver(function(t){var n=t[0].contentRect;e.checkForSizeChanges({height:n.height,width:n.width/f})}),this.$observer.observe(this.$measureNode)},this.$pollSizeChanges=function(){if(this.$pollSizeChangesTimer||this.$observer)return this.$pollSizeChangesTimer;var e=this;return this.$pollSizeChangesTimer=o.onIdle(function t(){e.checkForSizeChanges(),o.onIdle(t,500)},500)},this.setPolling=function(e){e?this.$pollSizeChanges():this.$pollSizeChangesTimer&&(clearInterval(this.$pollSizeChangesTimer),this.$pollSizeChangesTimer=0)},this.$measureSizes=function(e){var t={height:(e||this.$measureNode).clientHeight,width:(e||this.$measureNode).clientWidth/f};return t.width===0||t.height===0?null:t},this.$measureCharWidth=function(e){this.$main.innerHTML=s.stringRepeat(e,f);var t=this.$main.getBoundingClientRect();return t.width/f},this.getCharacterWidth=function(e){var t=this.charSizes[e];return t===undefined&&(t=this.charSizes[e]=this.$measureCharWidth(e)/this.$characterSize.width),t},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer),this.$observer&&this.$observer.disconnect(),this.el&&this.el.parentNode&&this.el.parentNode.removeChild(this.el)},this.$getZoom=function e(t){return t?(window.getComputedStyle(t).zoom||1)*e(t.parentElement):1},this.$initTransformMeasureNodes=function(){var e=function(e,t){return["div",{style:"position: absolute;top:"+e+"px;left:"+t+"px;"}]};this.els=i.buildDom([e(0,0),e(c,0),e(0,c),e(c,c)],this.el)},this.transformCoordinates=function(e,t){function r(e,t,n){var r=e[1]*t[0]-e[0]*t[1];return[(-t[1]*n[0]+t[0]*n[1])/r,(+e[1]*n[0]-e[0]*n[1])/r]}function i(e,t){return[e[0]-t[0],e[1]-t[1]]}function s(e,t){return[e[0]+t[0],e[1]+t[1]]}function o(e,t){return[e*t[0],e*t[1]]}function u(e){var t=e.getBoundingClientRect();return[t.left,t.top]}if(e){var n=this.$getZoom(this.el);e=o(1/n,e)}this.els||this.$initTransformMeasureNodes();var a=u(this.els[0]),f=u(this.els[1]),l=u(this.els[2]),h=u(this.els[3]),p=r(i(h,f),i(h,l),i(s(f,l),s(h,a))),d=o(1+p[0],i(f,a)),v=o(1+p[1],i(l,a));if(t){var m=t,g=p[0]*m[0]/c+p[1]*m[1]/c+1,y=s(o(m[0],d),o(m[1],v));return s(o(1/g/c,y),a)}var b=i(e,a),w=r(i(d,o(p[0],b)),i(v,o(p[1],b)),b);return o(c,w)}}).call(h.prototype)}),ace.define("ace/virtual_renderer",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/config","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/scrollbar","ace/renderloop","ace/layer/font_metrics","ace/lib/event_emitter","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./config"),o=e("./layer/gutter").Gutter,u=e("./layer/marker").Marker,a=e("./layer/text").Text,f=e("./layer/cursor").Cursor,l=e("./scrollbar").HScrollBar,c=e("./scrollbar").VScrollBar,h=e("./renderloop").RenderLoop,p=e("./layer/font_metrics").FontMetrics,d=e("./lib/event_emitter").EventEmitter,v='.ace_br1 {border-top-left-radius : 3px;}.ace_br2 {border-top-right-radius : 3px;}.ace_br3 {border-top-left-radius : 3px; border-top-right-radius: 3px;}.ace_br4 {border-bottom-right-radius: 3px;}.ace_br5 {border-top-left-radius : 3px; border-bottom-right-radius: 3px;}.ace_br6 {border-top-right-radius : 3px; border-bottom-right-radius: 3px;}.ace_br7 {border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px;}.ace_br8 {border-bottom-left-radius : 3px;}.ace_br9 {border-top-left-radius : 3px; border-bottom-left-radius: 3px;}.ace_br10{border-top-right-radius : 3px; border-bottom-left-radius: 3px;}.ace_br11{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br12{border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br13{border-top-left-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br14{border-top-right-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br15{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_editor {position: relative;overflow: hidden;font: 12px/normal \'Monaco\', \'Menlo\', \'Ubuntu Mono\', \'Consolas\', \'source-code-pro\', monospace;direction: ltr;text-align: left;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}.ace_scroller {position: absolute;overflow: hidden;top: 0;bottom: 0;background-color: inherit;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;cursor: text;}.ace_content {position: absolute;box-sizing: border-box;min-width: 100%;contain: style size layout;}.ace_dragging .ace_scroller:before{position: absolute;top: 0;left: 0;right: 0;bottom: 0;content: \'\';background: rgba(250, 250, 250, 0.01);z-index: 1000;}.ace_dragging.ace_dark .ace_scroller:before{background: rgba(0, 0, 0, 0.01);}.ace_selecting, .ace_selecting * {cursor: text !important;}.ace_gutter {position: absolute;overflow : hidden;width: auto;top: 0;bottom: 0;left: 0;cursor: default;z-index: 4;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;contain: style size layout;}.ace_gutter-active-line {position: absolute;left: 0;right: 0;}.ace_scroller.ace_scroll-left {box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;}.ace_gutter-cell {position: absolute;top: 0;left: 0;right: 0;padding-left: 19px;padding-right: 6px;background-repeat: no-repeat;}.ace_gutter-cell.ace_error {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: 2px center;}.ace_gutter-cell.ace_warning {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg==");background-position: 2px center;}.ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII=");background-position: 2px center;}.ace_dark .ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC");}.ace_scrollbar {contain: strict;position: absolute;right: 0;bottom: 0;z-index: 6;}.ace_scrollbar-inner {position: absolute;cursor: text;left: 0;top: 0;}.ace_scrollbar-v{overflow-x: hidden;overflow-y: scroll;top: 0;}.ace_scrollbar-h {overflow-x: scroll;overflow-y: hidden;left: 0;}.ace_print-margin {position: absolute;height: 100%;}.ace_text-input {position: absolute;z-index: 0;width: 0.5em;height: 1em;opacity: 0;background: transparent;-moz-appearance: none;appearance: none;border: none;resize: none;outline: none;overflow: hidden;font: inherit;padding: 0 1px;margin: 0 -1px;contain: strict;-ms-user-select: text;-moz-user-select: text;-webkit-user-select: text;user-select: text;white-space: pre!important;}.ace_text-input.ace_composition {background: transparent;color: inherit;z-index: 1000;opacity: 1;}.ace_composition_placeholder { color: transparent }.ace_composition_marker { border-bottom: 1px solid;position: absolute;border-radius: 0;margin-top: 1px;}[ace_nocontext=true] {transform: none!important;filter: none!important;perspective: none!important;clip-path: none!important;mask : none!important;contain: none!important;perspective: none!important;mix-blend-mode: initial!important;z-index: auto;}.ace_layer {z-index: 1;position: absolute;overflow: hidden;word-wrap: normal;white-space: pre;height: 100%;width: 100%;box-sizing: border-box;pointer-events: none;}.ace_gutter-layer {position: relative;width: auto;text-align: right;pointer-events: auto;height: 1000000px;contain: style size layout;}.ace_text-layer {font: inherit !important;position: absolute;height: 1000000px;width: 1000000px;contain: style size layout;}.ace_text-layer > .ace_line, .ace_text-layer > .ace_line_group {contain: style size layout;position: absolute;top: 0;left: 0;right: 0;}.ace_hidpi .ace_text-layer,.ace_hidpi .ace_gutter-layer,.ace_hidpi .ace_content,.ace_hidpi .ace_gutter {contain: strict;will-change: transform;}.ace_hidpi .ace_text-layer > .ace_line, .ace_hidpi .ace_text-layer > .ace_line_group {contain: strict;}.ace_cjk {display: inline-block;text-align: center;}.ace_cursor-layer {z-index: 4;}.ace_cursor {z-index: 4;position: absolute;box-sizing: border-box;border-left: 2px solid;transform: translatez(0);}.ace_multiselect .ace_cursor {border-left-width: 1px;}.ace_slim-cursors .ace_cursor {border-left-width: 1px;}.ace_overwrite-cursors .ace_cursor {border-left-width: 0;border-bottom: 1px solid;}.ace_hidden-cursors .ace_cursor {opacity: 0.2;}.ace_smooth-blinking .ace_cursor {transition: opacity 0.18s;}.ace_animate-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: step-end;animation-name: blink-ace-animate;animation-iteration-count: infinite;}.ace_animate-blinking.ace_smooth-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: ease-in-out;animation-name: blink-ace-animate-smooth;}@keyframes blink-ace-animate {from, to { opacity: 1; }60% { opacity: 0; }}@keyframes blink-ace-animate-smooth {from, to { opacity: 1; }45% { opacity: 1; }60% { opacity: 0; }85% { opacity: 0; }}.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {position: absolute;z-index: 3;}.ace_marker-layer .ace_selection {position: absolute;z-index: 5;}.ace_marker-layer .ace_bracket {position: absolute;z-index: 6;}.ace_marker-layer .ace_active-line {position: absolute;z-index: 2;}.ace_marker-layer .ace_selected-word {position: absolute;z-index: 4;box-sizing: border-box;}.ace_line .ace_fold {box-sizing: border-box;display: inline-block;height: 11px;margin-top: -2px;vertical-align: middle;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=");background-repeat: no-repeat, repeat-x;background-position: center center, top left;color: transparent;border: 1px solid black;border-radius: 2px;cursor: pointer;pointer-events: auto;}.ace_dark .ace_fold {}.ace_fold:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC");}.ace_tooltip {background-color: #FFF;background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));border: 1px solid gray;border-radius: 1px;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);color: black;max-width: 100%;padding: 3px 4px;position: fixed;z-index: 999999;box-sizing: border-box;cursor: default;white-space: pre;word-wrap: break-word;line-height: normal;font-style: normal;font-weight: normal;letter-spacing: normal;pointer-events: none;}.ace_folding-enabled > .ace_gutter-cell {padding-right: 13px;}.ace_fold-widget {box-sizing: border-box;margin: 0 -12px 0 1px;display: none;width: 11px;vertical-align: top;background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: center;border-radius: 3px;border: 1px solid transparent;cursor: pointer;}.ace_folding-enabled .ace_fold-widget {display: inline-block; }.ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==");}.ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");}.ace_fold-widget:hover {border: 1px solid rgba(0, 0, 0, 0.3);background-color: rgba(255, 255, 255, 0.2);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);}.ace_fold-widget:active {border: 1px solid rgba(0, 0, 0, 0.4);background-color: rgba(0, 0, 0, 0.05);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);}.ace_dark .ace_fold-widget {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC");}.ace_dark .ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget:hover {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);background-color: rgba(255, 255, 255, 0.1);}.ace_dark .ace_fold-widget:active {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);}.ace_inline_button {border: 1px solid lightgray;display: inline-block;margin: -1px 8px;padding: 0 5px;pointer-events: auto;cursor: pointer;}.ace_inline_button:hover {border-color: gray;background: rgba(200,200,200,0.2);display: inline-block;pointer-events: auto;}.ace_fold-widget.ace_invalid {background-color: #FFB4B4;border-color: #DE5555;}.ace_fade-fold-widgets .ace_fold-widget {transition: opacity 0.4s ease 0.05s;opacity: 0;}.ace_fade-fold-widgets:hover .ace_fold-widget {transition: opacity 0.05s ease 0.05s;opacity:1;}.ace_underline {text-decoration: underline;}.ace_bold {font-weight: bold;}.ace_nobold .ace_bold {font-weight: normal;}.ace_italic {font-style: italic;}.ace_error-marker {background-color: rgba(255, 0, 0,0.2);position: absolute;z-index: 9;}.ace_highlight-marker {background-color: rgba(255, 255, 0,0.2);position: absolute;z-index: 8;}',m=e("./lib/useragent"),g=m.isIE;i.importCssString(v,"ace_editor.css");var y=function(e,t){var n=this;this.container=e||i.createElement("div"),i.addCssClass(this.container,"ace_editor"),i.HI_DPI&&i.addCssClass(this.container,"ace_hidpi"),this.setTheme(t),this.$gutter=i.createElement("div"),this.$gutter.className="ace_gutter",this.container.appendChild(this.$gutter),this.$gutter.setAttribute("aria-hidden",!0),this.scroller=i.createElement("div"),this.scroller.className="ace_scroller",this.container.appendChild(this.scroller),this.content=i.createElement("div"),this.content.className="ace_content",this.scroller.appendChild(this.content),this.$gutterLayer=new o(this.$gutter),this.$gutterLayer.on("changeGutterWidth",this.onGutterResize.bind(this)),this.$markerBack=new u(this.content);var r=this.$textLayer=new a(this.content);this.canvas=r.element,this.$markerFront=new u(this.content),this.$cursorLayer=new f(this.content),this.$horizScroll=!1,this.$vScroll=!1,this.scrollBar=this.scrollBarV=new c(this.container,this),this.scrollBarH=new l(this.container,this),this.scrollBarV.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollTop(e.data-n.scrollMargin.top)}),this.scrollBarH.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollLeft(e.data-n.scrollMargin.left)}),this.scrollTop=0,this.scrollLeft=0,this.cursorPos={row:0,column:0},this.$fontMetrics=new p(this.container),this.$textLayer.$setFontMetrics(this.$fontMetrics),this.$textLayer.addEventListener("changeCharacterSize",function(e){n.updateCharacterSize(),n.onResize(!0,n.gutterWidth,n.$size.width,n.$size.height),n._signal("changeCharacterSize",e)}),this.$size={width:0,height:0,scrollerHeight:0,scrollerWidth:0,$dirty:!0},this.layerConfig={width:1,padding:0,firstRow:0,firstRowScreen:0,lastRow:0,lineHeight:0,characterWidth:0,minHeight:1,maxHeight:1,offset:0,height:1,gutterOffset:1},this.scrollMargin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.margin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.$keepTextAreaAtCursor=!m.isIOS,this.$loop=new h(this.$renderChanges.bind(this),this.container.ownerDocument.defaultView),this.$loop.schedule(this.CHANGE_FULL),this.updateCharacterSize(),this.setPadding(4),s.resetOptions(this),s._emit("renderer",this)};(function(){this.CHANGE_CURSOR=1,this.CHANGE_MARKER=2,this.CHANGE_GUTTER=4,this.CHANGE_SCROLL=8,this.CHANGE_LINES=16,this.CHANGE_TEXT=32,this.CHANGE_SIZE=64,this.CHANGE_MARKER_BACK=128,this.CHANGE_MARKER_FRONT=256,this.CHANGE_FULL=512,this.CHANGE_H_SCROLL=1024,r.implement(this,d),this.updateCharacterSize=function(){this.$textLayer.allowBoldFonts!=this.$allowBoldFonts&&(this.$allowBoldFonts=this.$textLayer.allowBoldFonts,this.setStyle("ace_nobold",!this.$allowBoldFonts)),this.layerConfig.characterWidth=this.characterWidth=this.$textLayer.getCharacterWidth(),this.layerConfig.lineHeight=this.lineHeight=this.$textLayer.getLineHeight(),this.$updatePrintMargin()},this.setSession=function(e){this.session&&this.session.doc.off("changeNewLineMode",this.onChangeNewLineMode),this.session=e,e&&this.scrollMargin.top&&e.getScrollTop()<=0&&e.setScrollTop(-this.scrollMargin.top),this.$cursorLayer.setSession(e),this.$markerBack.setSession(e),this.$markerFront.setSession(e),this.$gutterLayer.setSession(e),this.$textLayer.setSession(e);if(!e)return;this.$loop.schedule(this.CHANGE_FULL),this.session.$setFontMetrics(this.$fontMetrics),this.scrollBarH.scrollLeft=this.scrollBarV.scrollTop=null,this.onChangeNewLineMode=this.onChangeNewLineMode.bind(this),this.onChangeNewLineMode(),this.session.doc.on("changeNewLineMode",this.onChangeNewLineMode)},this.updateLines=function(e,t,n){t===undefined&&(t=Infinity),this.$changedLines?(this.$changedLines.firstRow>e&&(this.$changedLines.firstRow=e),this.$changedLines.lastRowthis.layerConfig.lastRow)return;this.$loop.schedule(this.CHANGE_LINES)},this.onChangeNewLineMode=function(){this.$loop.schedule(this.CHANGE_TEXT),this.$textLayer.$updateEolChar(),this.session.$bidiHandler.setEolChar(this.$textLayer.EOL_CHAR)},this.onChangeTabSize=function(){this.$loop.schedule(this.CHANGE_TEXT|this.CHANGE_MARKER),this.$textLayer.onChangeTabSize()},this.updateText=function(){this.$loop.schedule(this.CHANGE_TEXT)},this.updateFull=function(e){e?this.$renderChanges(this.CHANGE_FULL,!0):this.$loop.schedule(this.CHANGE_FULL)},this.updateFontSize=function(){this.$textLayer.checkForSizeChanges()},this.$changes=0,this.$updateSizeAsync=function(){this.$loop.pending?this.$size.$dirty=!0:this.onResize()},this.onResize=function(e,t,n,r){if(this.resizing>2)return;this.resizing>0?this.resizing++:this.resizing=e?1:0;var i=this.container;r||(r=i.clientHeight||i.scrollHeight),n||(n=i.clientWidth||i.scrollWidth);var s=this.$updateCachedSize(e,t,n,r);if(!this.$size.scrollerHeight||!n&&!r)return this.resizing=0;e&&(this.$gutterLayer.$padding=null),e?this.$renderChanges(s|this.$changes,!0):this.$loop.schedule(s|this.$changes),this.resizing&&(this.resizing=0),this.scrollBarV.scrollLeft=this.scrollBarV.scrollTop=null},this.$updateCachedSize=function(e,t,n,r){r-=this.$extraHeight||0;var s=0,o=this.$size,u={width:o.width,height:o.height,scrollerHeight:o.scrollerHeight,scrollerWidth:o.scrollerWidth};r&&(e||o.height!=r)&&(o.height=r,s|=this.CHANGE_SIZE,o.scrollerHeight=o.height,this.$horizScroll&&(o.scrollerHeight-=this.scrollBarH.getHeight()),this.scrollBarV.element.style.bottom=this.scrollBarH.getHeight()+"px",s|=this.CHANGE_SCROLL);if(n&&(e||o.width!=n)){s|=this.CHANGE_SIZE,o.width=n,t==null&&(t=this.$showGutter?this.$gutter.offsetWidth:0),this.gutterWidth=t,i.setStyle(this.scrollBarH.element.style,"left",t+"px"),i.setStyle(this.scroller.style,"left",t+this.margin.left+"px"),o.scrollerWidth=Math.max(0,n-t-this.scrollBarV.getWidth()-this.margin.h),i.setStyle(this.$gutter.style,"left",this.margin.left+"px");var a=this.scrollBarV.getWidth()+"px";i.setStyle(this.scrollBarH.element.style,"right",a),i.setStyle(this.scroller.style,"right",a),i.setStyle(this.scroller.style,"bottom",this.scrollBarH.getHeight());if(this.session&&this.session.getUseWrapMode()&&this.adjustWrapLimit()||e)s|=this.CHANGE_FULL}return o.$dirty=!n||!r,s&&this._signal("resize",u),s},this.onGutterResize=function(e){var t=this.$showGutter?e:0;t!=this.gutterWidth&&(this.$changes|=this.$updateCachedSize(!0,t,this.$size.width,this.$size.height)),this.session.getUseWrapMode()&&this.adjustWrapLimit()?this.$loop.schedule(this.CHANGE_FULL):this.$size.$dirty?this.$loop.schedule(this.CHANGE_FULL):this.$computeLayerConfig()},this.adjustWrapLimit=function(){var e=this.$size.scrollerWidth-this.$padding*2,t=Math.floor(e/this.characterWidth);return this.session.adjustWrapLimit(t,this.$showPrintMargin&&this.$printMarginColumn)},this.setAnimatedScroll=function(e){this.setOption("animatedScroll",e)},this.getAnimatedScroll=function(){return this.$animatedScroll},this.setShowInvisibles=function(e){this.setOption("showInvisibles",e),this.session.$bidiHandler.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.getOption("showInvisibles")},this.getDisplayIndentGuides=function(){return this.getOption("displayIndentGuides")},this.setDisplayIndentGuides=function(e){this.setOption("displayIndentGuides",e)},this.setShowPrintMargin=function(e){this.setOption("showPrintMargin",e)},this.getShowPrintMargin=function(){return this.getOption("showPrintMargin")},this.setPrintMarginColumn=function(e){this.setOption("printMarginColumn",e)},this.getPrintMarginColumn=function(){return this.getOption("printMarginColumn")},this.getShowGutter=function(){return this.getOption("showGutter")},this.setShowGutter=function(e){return this.setOption("showGutter",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.$updatePrintMargin=function(){if(!this.$showPrintMargin&&!this.$printMarginEl)return;if(!this.$printMarginEl){var e=i.createElement("div");e.className="ace_layer ace_print-margin-layer",this.$printMarginEl=i.createElement("div"),this.$printMarginEl.className="ace_print-margin",e.appendChild(this.$printMarginEl),this.content.insertBefore(e,this.content.firstChild)}var t=this.$printMarginEl.style;t.left=Math.round(this.characterWidth*this.$printMarginColumn+this.$padding)+"px",t.visibility=this.$showPrintMargin?"visible":"hidden",this.session&&this.session.$wrap==-1&&this.adjustWrapLimit()},this.getContainerElement=function(){return this.container},this.getMouseEventTarget=function(){return this.scroller},this.getTextAreaContainer=function(){return this.container},this.$moveTextAreaToCursor=function(){var e=this.textarea.style;if(!this.$keepTextAreaAtCursor){i.translate(this.textarea,-100,0);return}var t=this.$cursorLayer.$pixelPos;if(!t)return;var n=this.$composition;n&&n.markerRange&&(t=this.$cursorLayer.getPixelPosition(n.markerRange.start,!0));var r=this.layerConfig,s=t.top,o=t.left;s-=r.offset;var u=n&&n.useTextareaForIME?this.lineHeight:g?0:1;if(s<0||s>r.height-u){i.translate(this.textarea,0,0);return}var a=1;if(!n)s+=this.lineHeight;else if(n.useTextareaForIME){var f=this.textarea.value;a=this.characterWidth*this.session.$getStringScreenWidth(f)[0],u+=2}else s+=this.lineHeight+2;o-=this.scrollLeft,o>this.$size.scrollerWidth-a&&(o=this.$size.scrollerWidth-a),o+=this.gutterWidth+this.margin.left,i.setStyle(e,"height",u+"px"),i.setStyle(e,"width",a+"px"),i.translate(this.textarea,Math.min(o,this.$size.scrollerWidth-a),Math.min(s,this.$size.height-u))},this.getFirstVisibleRow=function(){return this.layerConfig.firstRow},this.getFirstFullyVisibleRow=function(){return this.layerConfig.firstRow+(this.layerConfig.offset===0?0:1)},this.getLastFullyVisibleRow=function(){var e=this.layerConfig,t=e.lastRow,n=this.session.documentToScreenRow(t,0)*e.lineHeight;return n-this.session.getScrollTop()>e.height-e.lineHeight?t-1:t},this.getLastVisibleRow=function(){return this.layerConfig.lastRow},this.$padding=null,this.setPadding=function(e){this.$padding=e,this.$textLayer.setPadding(e),this.$cursorLayer.setPadding(e),this.$markerFront.setPadding(e),this.$markerBack.setPadding(e),this.$loop.schedule(this.CHANGE_FULL),this.$updatePrintMargin()},this.setScrollMargin=function(e,t,n,r){var i=this.scrollMargin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,i.top&&this.scrollTop<=0&&this.session&&this.session.setScrollTop(-i.top),this.updateFull()},this.setMargin=function(e,t,n,r){var i=this.margin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,this.$updateCachedSize(!0,this.gutterWidth,this.$size.width,this.$size.height),this.updateFull()},this.getHScrollBarAlwaysVisible=function(){return this.$hScrollBarAlwaysVisible},this.setHScrollBarAlwaysVisible=function(e){this.setOption("hScrollBarAlwaysVisible",e)},this.getVScrollBarAlwaysVisible=function(){return this.$vScrollBarAlwaysVisible},this.setVScrollBarAlwaysVisible=function(e){this.setOption("vScrollBarAlwaysVisible",e)},this.$updateScrollBarV=function(){var e=this.layerConfig.maxHeight,t=this.$size.scrollerHeight;!this.$maxLines&&this.$scrollPastEnd&&(e-=(t-this.lineHeight)*this.$scrollPastEnd,this.scrollTop>e-t&&(e=this.scrollTop+t,this.scrollBarV.scrollTop=null)),this.scrollBarV.setScrollHeight(e+this.scrollMargin.v),this.scrollBarV.setScrollTop(this.scrollTop+this.scrollMargin.top)},this.$updateScrollBarH=function(){this.scrollBarH.setScrollWidth(this.layerConfig.width+2*this.$padding+this.scrollMargin.h),this.scrollBarH.setScrollLeft(this.scrollLeft+this.scrollMargin.left)},this.$frozen=!1,this.freeze=function(){this.$frozen=!0},this.unfreeze=function(){this.$frozen=!1},this.$renderChanges=function(e,t){this.$changes&&(e|=this.$changes,this.$changes=0);if(!this.session||!this.container.offsetWidth||this.$frozen||!e&&!t){this.$changes|=e;return}if(this.$size.$dirty)return this.$changes|=e,this.onResize(!0);this.lineHeight||this.$textLayer.checkForSizeChanges(),this._signal("beforeRender"),this.session&&this.session.$bidiHandler&&this.session.$bidiHandler.updateCharacterWidths(this.$fontMetrics);var n=this.layerConfig;if(e&this.CHANGE_FULL||e&this.CHANGE_SIZE||e&this.CHANGE_TEXT||e&this.CHANGE_LINES||e&this.CHANGE_SCROLL||e&this.CHANGE_H_SCROLL){e|=this.$computeLayerConfig()|this.$loop.clear();if(n.firstRow!=this.layerConfig.firstRow&&n.firstRowScreen==this.layerConfig.firstRowScreen){var r=this.scrollTop+(n.firstRow-this.layerConfig.firstRow)*this.lineHeight;r>0&&(this.scrollTop=r,e|=this.CHANGE_SCROLL,e|=this.$computeLayerConfig()|this.$loop.clear())}n=this.layerConfig,this.$updateScrollBarV(),e&this.CHANGE_H_SCROLL&&this.$updateScrollBarH(),i.translate(this.content,-this.scrollLeft,-n.offset);var s=n.width+2*this.$padding+"px",o=n.minHeight+"px";i.setStyle(this.content.style,"width",s),i.setStyle(this.content.style,"height",o)}e&this.CHANGE_H_SCROLL&&(i.translate(this.content,-this.scrollLeft,-n.offset),this.scroller.className=this.scrollLeft<=0?"ace_scroller":"ace_scroller ace_scroll-left");if(e&this.CHANGE_FULL){this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender");return}if(e&this.CHANGE_SCROLL){e&this.CHANGE_TEXT||e&this.CHANGE_LINES?this.$textLayer.update(n):this.$textLayer.scrollLines(n),this.$showGutter&&(e&this.CHANGE_GUTTER||e&this.CHANGE_LINES?this.$gutterLayer.update(n):this.$gutterLayer.scrollLines(n)),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender");return}e&this.CHANGE_TEXT?(this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n)):e&this.CHANGE_LINES?(this.$updateLines()||e&this.CHANGE_GUTTER&&this.$showGutter)&&this.$gutterLayer.update(n):e&this.CHANGE_TEXT||e&this.CHANGE_GUTTER?this.$showGutter&&this.$gutterLayer.update(n):e&this.CHANGE_CURSOR&&this.$highlightGutterLine&&this.$gutterLayer.updateLineHighlight(n),e&this.CHANGE_CURSOR&&(this.$cursorLayer.update(n),this.$moveTextAreaToCursor()),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_FRONT)&&this.$markerFront.update(n),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_BACK)&&this.$markerBack.update(n),this._signal("afterRender")},this.$autosize=function(){var e=this.session.getScreenLength()*this.lineHeight,t=this.$maxLines*this.lineHeight,n=Math.min(t,Math.max((this.$minLines||1)*this.lineHeight,e))+this.scrollMargin.v+(this.$extraHeight||0);this.$horizScroll&&(n+=this.scrollBarH.getHeight()),this.$maxPixelHeight&&n>this.$maxPixelHeight&&(n=this.$maxPixelHeight);var r=n<=2*this.lineHeight,i=!r&&e>t;if(n!=this.desiredHeight||this.$size.height!=this.desiredHeight||i!=this.$vScroll){i!=this.$vScroll&&(this.$vScroll=i,this.scrollBarV.setVisible(i));var s=this.container.clientWidth;this.container.style.height=n+"px",this.$updateCachedSize(!0,this.$gutterWidth,s,n),this.desiredHeight=n,this._signal("autosize")}},this.$computeLayerConfig=function(){var e=this.session,t=this.$size,n=t.height<=2*this.lineHeight,r=this.session.getScreenLength(),i=r*this.lineHeight,s=this.$getLongestLine(),o=!n&&(this.$hScrollBarAlwaysVisible||t.scrollerWidth-s-2*this.$padding<0),u=this.$horizScroll!==o;u&&(this.$horizScroll=o,this.scrollBarH.setVisible(o));var a=this.$vScroll;this.$maxLines&&this.lineHeight>1&&this.$autosize();var f=t.scrollerHeight+this.lineHeight,l=!this.$maxLines&&this.$scrollPastEnd?(t.scrollerHeight-this.lineHeight)*this.$scrollPastEnd:0;i+=l;var c=this.scrollMargin;this.session.setScrollTop(Math.max(-c.top,Math.min(this.scrollTop,i-t.scrollerHeight+c.bottom))),this.session.setScrollLeft(Math.max(-c.left,Math.min(this.scrollLeft,s+2*this.$padding-t.scrollerWidth+c.right)));var h=!n&&(this.$vScrollBarAlwaysVisible||t.scrollerHeight-i+l<0||this.scrollTop>c.top),p=a!==h;p&&(this.$vScroll=h,this.scrollBarV.setVisible(h));var d=this.scrollTop%this.lineHeight,v=Math.ceil(f/this.lineHeight)-1,m=Math.max(0,Math.round((this.scrollTop-d)/this.lineHeight)),g=m+v,y,b,w=this.lineHeight;m=e.screenToDocumentRow(m,0);var E=e.getFoldLine(m);E&&(m=E.start.row),y=e.documentToScreenRow(m,0),b=e.getRowLength(m)*w,g=Math.min(e.screenToDocumentRow(g,0),e.getLength()-1),f=t.scrollerHeight+e.getRowLength(g)*w+b,d=this.scrollTop-y*w;var S=0;if(this.layerConfig.width!=s||u)S=this.CHANGE_H_SCROLL;if(u||p)S=this.$updateCachedSize(!0,this.gutterWidth,t.width,t.height),this._signal("scrollbarVisibilityChanged"),p&&(s=this.$getLongestLine());return this.layerConfig={width:s,padding:this.$padding,firstRow:m,firstRowScreen:y,lastRow:g,lineHeight:w,characterWidth:this.characterWidth,minHeight:f,maxHeight:i,offset:d,gutterOffset:w?Math.max(0,Math.ceil((d+t.height-t.scrollerHeight)/w)):0,height:this.$size.scrollerHeight},this.session.$bidiHandler&&this.session.$bidiHandler.setContentWidth(s-this.$padding),S},this.$updateLines=function(){if(!this.$changedLines)return;var e=this.$changedLines.firstRow,t=this.$changedLines.lastRow;this.$changedLines=null;var n=this.layerConfig;if(e>n.lastRow+1)return;if(tthis.$textLayer.MAX_LINE_LENGTH&&(e=this.$textLayer.MAX_LINE_LENGTH+30),Math.max(this.$size.scrollerWidth-2*this.$padding,Math.round(e*this.characterWidth))},this.updateFrontMarkers=function(){this.$markerFront.setMarkers(this.session.getMarkers(!0)),this.$loop.schedule(this.CHANGE_MARKER_FRONT)},this.updateBackMarkers=function(){this.$markerBack.setMarkers(this.session.getMarkers()),this.$loop.schedule(this.CHANGE_MARKER_BACK)},this.addGutterDecoration=function(e,t){this.$gutterLayer.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){this.$gutterLayer.removeGutterDecoration(e,t)},this.updateBreakpoints=function(e){this.$loop.schedule(this.CHANGE_GUTTER)},this.setAnnotations=function(e){this.$gutterLayer.setAnnotations(e),this.$loop.schedule(this.CHANGE_GUTTER)},this.updateCursor=function(){this.$loop.schedule(this.CHANGE_CURSOR)},this.hideCursor=function(){this.$cursorLayer.hideCursor()},this.showCursor=function(){this.$cursorLayer.showCursor()},this.scrollSelectionIntoView=function(e,t,n){this.scrollCursorIntoView(e,n),this.scrollCursorIntoView(t,n)},this.scrollCursorIntoView=function(e,t,n){if(this.$size.scrollerHeight===0)return;var r=this.$cursorLayer.getPixelPosition(e),i=r.left,s=r.top,o=n&&n.top||0,u=n&&n.bottom||0,a=this.$scrollAnimation?this.session.getScrollTop():this.scrollTop;a+o>s?(t&&a+o>s+this.lineHeight&&(s-=t*this.$size.scrollerHeight),s===0&&(s=-this.scrollMargin.top),this.session.setScrollTop(s)):a+this.$size.scrollerHeight-ui?(i=1-this.scrollMargin.top)return!0;if(t>0&&this.session.getScrollTop()+this.$size.scrollerHeight-this.layerConfig.maxHeight<-1+this.scrollMargin.bottom)return!0;if(e<0&&this.session.getScrollLeft()>=1-this.scrollMargin.left)return!0;if(e>0&&this.session.getScrollLeft()+this.$size.scrollerWidth-this.layerConfig.width<-1+this.scrollMargin.right)return!0},this.pixelToScreenCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=Math.floor((t+this.scrollTop-n.top)/this.lineHeight),u=this.$blockCursor?Math.floor(s):Math.round(s);return{row:o,column:u,side:s-u>0?1:-1,offsetX:i}},this.screenToTextCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=this.$blockCursor?Math.floor(s):Math.round(s),u=Math.floor((t+this.scrollTop-n.top)/this.lineHeight);return this.session.screenToDocumentPosition(u,Math.max(o,0),i)},this.textToScreenCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=this.session.documentToScreenPosition(e,t),i=this.$padding+(this.session.$bidiHandler.isBidiRow(r.row,e)?this.session.$bidiHandler.getPosLeft(r.column):Math.round(r.column*this.characterWidth)),s=r.row*this.lineHeight;return{pageX:n.left+i-this.scrollLeft,pageY:n.top+s-this.scrollTop}},this.visualizeFocus=function(){i.addCssClass(this.container,"ace_focus")},this.visualizeBlur=function(){i.removeCssClass(this.container,"ace_focus")},this.showComposition=function(e){this.$composition=e,e.cssText||(e.cssText=this.textarea.style.cssText,e.keepTextAreaAtCursor=this.$keepTextAreaAtCursor),e.useTextareaForIME=this.$useTextareaForIME,this.$useTextareaForIME?(this.$keepTextAreaAtCursor=!0,i.addCssClass(this.textarea,"ace_composition"),this.textarea.style.cssText="",this.$moveTextAreaToCursor(),this.$cursorLayer.element.style.display="none"):e.markerId=this.session.addMarker(e.markerRange,"ace_composition_marker","text")},this.setCompositionText=function(e){var t=this.session.selection.cursor;this.addToken(e,"composition_placeholder",t.row,t.column),this.$moveTextAreaToCursor()},this.hideComposition=function(){if(!this.$composition)return;this.$composition.markerId&&this.session.removeMarker(this.$composition.markerId),i.removeCssClass(this.textarea,"ace_composition"),this.$keepTextAreaAtCursor=this.$composition.keepTextAreaAtCursor,this.textarea.style.cssText=this.$composition.cssText,this.$composition=null,this.$cursorLayer.element.style.display=""},this.addToken=function(e,t,n,r){var i=this.session;i.bgTokenizer.lines[n]=null;var s={type:t,value:e},o=i.getTokens(n);if(r==null)o.push(s);else{var u=0;for(var a=0;a50&&e.length>this.$doc.getLength()>>1?this.call("setValue",[this.$doc.getValue()]):this.emit("change",{data:e})}}).call(f.prototype);var l=function(e,t,n){this.$sendDeltaQueue=this.$sendDeltaQueue.bind(this),this.changeListener=this.changeListener.bind(this),this.callbackId=1,this.callbacks={},this.messageBuffer=[];var r=null,i=!1,u=Object.create(s),a=this;this.$worker={},this.$worker.terminate=function(){},this.$worker.postMessage=function(e){a.messageBuffer.push(e),r&&(i?setTimeout(f):f())},this.setEmitSync=function(e){i=e};var f=function(){var e=a.messageBuffer.shift();e.command?r[e.command].apply(r,e.args):e.event&&u._signal(e.event,e.data)};u.postMessage=function(e){a.onMessage({data:e})},u.callback=function(e,t){this.postMessage({type:"call",id:t,data:e})},u.emit=function(e,t){this.postMessage({type:"event",name:e,data:t})},o.loadModule(["worker",t],function(e){r=new e[n](u);while(a.messageBuffer.length)f()})};l.prototype=f.prototype,t.UIWorkerClient=l,t.WorkerClient=f,t.createWorker=a}),ace.define("ace/placeholder",["require","exports","module","ace/range","ace/lib/event_emitter","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./range").Range,i=e("./lib/event_emitter").EventEmitter,s=e("./lib/oop"),o=function(e,t,n,r,i,s){var o=this;this.length=t,this.session=e,this.doc=e.getDocument(),this.mainClass=i,this.othersClass=s,this.$onUpdate=this.onUpdate.bind(this),this.doc.on("change",this.$onUpdate),this.$others=r,this.$onCursorChange=function(){setTimeout(function(){o.onCursorChange()})},this.$pos=n;var u=e.getUndoManager().$undoStack||e.getUndoManager().$undostack||{length:-1};this.$undoStackDepth=u.length,this.setup(),e.selection.on("changeCursor",this.$onCursorChange)};(function(){s.implement(this,i),this.setup=function(){var e=this,t=this.doc,n=this.session;this.selectionBefore=n.selection.toJSON(),n.selection.inMultiSelectMode&&n.selection.toSingleRange(),this.pos=t.createAnchor(this.$pos.row,this.$pos.column);var i=this.pos;i.$insertRight=!0,i.detach(),i.markerId=n.addMarker(new r(i.row,i.column,i.row,i.column+this.length),this.mainClass,null,!1),this.others=[],this.$others.forEach(function(n){var r=t.createAnchor(n.row,n.column);r.$insertRight=!0,r.detach(),e.others.push(r)}),n.setUndoSelect(!1)},this.showOtherMarkers=function(){if(this.othersActive)return;var e=this.session,t=this;this.othersActive=!0,this.others.forEach(function(n){n.markerId=e.addMarker(new r(n.row,n.column,n.row,n.column+t.length),t.othersClass,null,!1)})},this.hideOtherMarkers=function(){if(!this.othersActive)return;this.othersActive=!1;for(var e=0;e=this.pos.column&&t.start.column<=this.pos.column+this.length+1,s=t.start.column-this.pos.column;this.updateAnchors(e),i&&(this.length+=n);if(i&&!this.session.$fromUndo)if(e.action==="insert")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.insertMergedLines(a,e.lines)}else if(e.action==="remove")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.remove(new r(a.row,a.column,a.row,a.column-n))}this.$updating=!1,this.updateMarkers()},this.updateAnchors=function(e){this.pos.onChange(e);for(var t=this.others.length;t--;)this.others[t].onChange(e);this.updateMarkers()},this.updateMarkers=function(){if(this.$updating)return;var e=this,t=this.session,n=function(n,i){t.removeMarker(n.markerId),n.markerId=t.addMarker(new r(n.row,n.column,n.row,n.column+e.length),i,null,!1)};n(this.pos,this.mainClass);for(var i=this.others.length;i--;)n(this.others[i],this.othersClass)},this.onCursorChange=function(e){if(this.$updating||!this.session)return;var t=this.session.selection.getCursor();t.row===this.pos.row&&t.column>=this.pos.column&&t.column<=this.pos.column+this.length?(this.showOtherMarkers(),this._emit("cursorEnter",e)):(this.hideOtherMarkers(),this._emit("cursorLeave",e))},this.detach=function(){this.session.removeMarker(this.pos&&this.pos.markerId),this.hideOtherMarkers(),this.doc.removeEventListener("change",this.$onUpdate),this.session.selection.removeEventListener("changeCursor",this.$onCursorChange),this.session.setUndoSelect(!0),this.session=null},this.cancel=function(){if(this.$undoStackDepth===-1)return;var e=this.session.getUndoManager(),t=(e.$undoStack||e.$undostack).length-this.$undoStackDepth;for(var n=0;n1&&!this.inMultiSelectMode&&(this._signal("multiSelect"),this.inMultiSelectMode=!0,this.session.$undoSelect=!1,this.rangeList.attach(this.session)),t||this.fromOrientedRange(e)},this.toSingleRange=function(e){e=e||this.ranges[0];var t=this.rangeList.removeAll();t.length&&this.$onRemoveRange(t),e&&this.fromOrientedRange(e)},this.substractPoint=function(e){var t=this.rangeList.substractPoint(e);if(t)return this.$onRemoveRange(t),t[0]},this.mergeOverlappingRanges=function(){var e=this.rangeList.merge();e.length&&this.$onRemoveRange(e)},this.$onAddRange=function(e){this.rangeCount=this.rangeList.ranges.length,this.ranges.unshift(e),this._signal("addRange",{range:e})},this.$onRemoveRange=function(e){this.rangeCount=this.rangeList.ranges.length;if(this.rangeCount==1&&this.inMultiSelectMode){var t=this.rangeList.ranges.pop();e.push(t),this.rangeCount=0}for(var n=e.length;n--;){var r=this.ranges.indexOf(e[n]);this.ranges.splice(r,1)}this._signal("removeRange",{ranges:e}),this.rangeCount===0&&this.inMultiSelectMode&&(this.inMultiSelectMode=!1,this._signal("singleSelect"),this.session.$undoSelect=!0,this.rangeList.detach(this.session)),t=t||this.ranges[0],t&&!t.isEqual(this.getRange())&&this.fromOrientedRange(t)},this.$initRangeList=function(){if(this.rangeList)return;this.rangeList=new r,this.ranges=[],this.rangeCount=0},this.getAllRanges=function(){return this.rangeCount?this.rangeList.ranges.concat():[this.getRange()]},this.splitIntoLines=function(){if(this.rangeCount>1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var n=this.getRange(),r=this.isBackwards(),s=n.start.row,o=n.end.row;if(s==o){if(r)var u=n.end,a=n.start;else var u=n.start,a=n.end;this.addRange(i.fromPoints(a,a)),this.addRange(i.fromPoints(u,u));return}var f=[],l=this.getLineRange(s,!0);l.start.column=n.start.column,f.push(l);for(var c=s+1;c1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var r=this.session.documentToScreenPosition(this.cursor),s=this.session.documentToScreenPosition(this.anchor),o=this.rectangularRangeBlock(r,s);o.forEach(this.addRange,this)}},this.rectangularRangeBlock=function(e,t,n){var r=[],s=e.column0)g--;if(g>0){var y=0;while(r[y].isEmpty())y++}for(var b=g;b>=y;b--)r[b].isEmpty()&&r.splice(b,1)}return r}}.call(s.prototype);var d=e("./editor").Editor;(function(){this.updateSelectionMarkers=function(){this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.addSelectionMarker=function(e){e.cursor||(e.cursor=e.end);var t=this.getSelectionStyle();return e.marker=this.session.addMarker(e,"ace_selection",t),this.session.$selectionMarkers.push(e),this.session.selectionMarkerCount=this.session.$selectionMarkers.length,e},this.removeSelectionMarker=function(e){if(!e.marker)return;this.session.removeMarker(e.marker);var t=this.session.$selectionMarkers.indexOf(e);t!=-1&&this.session.$selectionMarkers.splice(t,1),this.session.selectionMarkerCount=this.session.$selectionMarkers.length},this.removeSelectionMarkers=function(e){var t=this.session.$selectionMarkers;for(var n=e.length;n--;){var r=e[n];if(!r.marker)continue;this.session.removeMarker(r.marker);var i=t.indexOf(r);i!=-1&&t.splice(i,1)}this.session.selectionMarkerCount=t.length},this.$onAddRange=function(e){this.addSelectionMarker(e.range),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onRemoveRange=function(e){this.removeSelectionMarkers(e.ranges),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onMultiSelect=function(e){if(this.inMultiSelectMode)return;this.inMultiSelectMode=!0,this.setStyle("ace_multiselect"),this.keyBinding.addKeyboardHandler(f.keyboardHandler),this.commands.setDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onSingleSelect=function(e){if(this.session.multiSelect.inVirtualMode)return;this.inMultiSelectMode=!1,this.unsetStyle("ace_multiselect"),this.keyBinding.removeKeyboardHandler(f.keyboardHandler),this.commands.removeDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers(),this._emit("changeSelection")},this.$onMultiSelectExec=function(e){var t=e.command,n=e.editor;if(!n.multiSelect)return;if(!t.multiSelectAction){var r=t.exec(n,e.args||{});n.multiSelect.addRange(n.multiSelect.toOrientedRange()),n.multiSelect.mergeOverlappingRanges()}else t.multiSelectAction=="forEach"?r=n.forEachSelection(t,e.args):t.multiSelectAction=="forEachLine"?r=n.forEachSelection(t,e.args,!0):t.multiSelectAction=="single"?(n.exitMultiSelectMode(),r=t.exec(n,e.args||{})):r=t.multiSelectAction(n,e.args||{});return r},this.forEachSelection=function(e,t,n){if(this.inVirtualSelectionMode)return;var r=n&&n.keepOrder,i=n==1||n&&n.$byLines,o=this.session,u=this.selection,a=u.rangeList,f=(r?u:a).ranges,l;if(!f.length)return e.exec?e.exec(this,t||{}):e(this,t||{});var c=u._eventRegistry;u._eventRegistry={};var h=new s(o);this.inVirtualSelectionMode=!0;for(var p=f.length;p--;){if(i)while(p>0&&f[p].start.row==f[p-1].end.row)p--;h.fromOrientedRange(f[p]),h.index=p,this.selection=o.selection=h;var d=e.exec?e.exec(this,t||{}):e(this,t||{});!l&&d!==undefined&&(l=d),h.toOrientedRange(f[p])}h.detach(),this.selection=o.selection=u,this.inVirtualSelectionMode=!1,u._eventRegistry=c,u.mergeOverlappingRanges(),u.ranges[0]&&u.fromOrientedRange(u.ranges[0]);var v=this.renderer.$scrollAnimation;return this.onCursorChange(),this.onSelectionChange(),v&&v.from==v.to&&this.renderer.animateScrolling(v.from),l},this.exitMultiSelectMode=function(){if(!this.inMultiSelectMode||this.inVirtualSelectionMode)return;this.multiSelect.toSingleRange()},this.getSelectedText=function(){var e="";if(this.inMultiSelectMode&&!this.inVirtualSelectionMode){var t=this.multiSelect.rangeList.ranges,n=[];for(var r=0;r0);u<0&&(u=0),f>=c&&(f=c-1)}var p=this.session.removeFullLines(u,f);p=this.$reAlignText(p,l),this.session.insert({row:u,column:0},p.join("\n")+"\n"),l||(o.start.column=0,o.end.column=p[p.length-1].length),this.selection.setRange(o)}else{s.forEach(function(e){t.substractPoint(e.cursor)});var d=0,v=Infinity,m=n.map(function(t){var n=t.cursor,r=e.getLine(n.row),i=r.substr(n.column).search(/\S/g);return i==-1&&(i=0),n.column>d&&(d=n.column),io?e.insert(r,a.stringRepeat(" ",s-o)):e.remove(new i(r.row,r.column,r.row,r.column-s+o)),t.start.column=t.end.column=d,t.start.row=t.end.row=r.row,t.cursor=t.end}),t.fromOrientedRange(n[0]),this.renderer.updateCursor(),this.renderer.updateBackMarkers()}},this.$reAlignText=function(e,t){function u(e){return a.stringRepeat(" ",e)}function f(e){return e[2]?u(i)+e[2]+u(s-e[2].length+o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function l(e){return e[2]?u(i+s-e[2].length)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function c(e){return e[2]?u(i)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}var n=!0,r=!0,i,s,o;return e.map(function(e){var t=e.match(/(\s*)(.*?)(\s*)([=:].*)/);return t?i==null?(i=t[1].length,s=t[2].length,o=t[3].length,t):(i+s+o!=t[1].length+t[2].length+t[3].length&&(r=!1),i!=t[1].length&&(n=!1),i>t[1].length&&(i=t[1].length),st[3].length&&(o=t[3].length),t):[e]}).map(t?f:n?r?l:f:c)}}).call(d.prototype),t.onSessionChange=function(e){var t=e.session;t&&!t.multiSelect&&(t.$selectionMarkers=[],t.selection.$initRangeList(),t.multiSelect=t.selection),this.multiSelect=t&&t.multiSelect;var n=e.oldSession;n&&(n.multiSelect.off("addRange",this.$onAddRange),n.multiSelect.off("removeRange",this.$onRemoveRange),n.multiSelect.off("multiSelect",this.$onMultiSelect),n.multiSelect.off("singleSelect",this.$onSingleSelect),n.multiSelect.lead.off("change",this.$checkMultiselectChange),n.multiSelect.anchor.off("change",this.$checkMultiselectChange)),t&&(t.multiSelect.on("addRange",this.$onAddRange),t.multiSelect.on("removeRange",this.$onRemoveRange),t.multiSelect.on("multiSelect",this.$onMultiSelect),t.multiSelect.on("singleSelect",this.$onSingleSelect),t.multiSelect.lead.on("change",this.$checkMultiselectChange),t.multiSelect.anchor.on("change",this.$checkMultiselectChange)),t&&this.inMultiSelectMode!=t.selection.inMultiSelectMode&&(t.selection.inMultiSelectMode?this.$onMultiSelect():this.$onSingleSelect())},t.MultiSelect=m,e("./config").defineOptions(d.prototype,"editor",{enableMultiselect:{set:function(e){m(this),e?(this.on("changeSession",this.$multiselectOnSessionChange),this.on("mousedown",o)):(this.off("changeSession",this.$multiselectOnSessionChange),this.off("mousedown",o))},value:!0},enableBlockSelect:{set:function(e){this.$blockSelectEnabled=e},value:!0}})}),ace.define("ace/mode/folding/fold_mode",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../../range").Range,i=t.FoldMode=function(){};(function(){this.foldingStartMarker=null,this.foldingStopMarker=null,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);return this.foldingStartMarker.test(r)?"start":t=="markbeginend"&&this.foldingStopMarker&&this.foldingStopMarker.test(r)?"end":""},this.getFoldWidgetRange=function(e,t,n){return null},this.indentationBlock=function(e,t,n){var i=/\S/,s=e.getLine(t),o=s.search(i);if(o==-1)return;var u=n||s.length,a=e.getLength(),f=t,l=t;while(++tf){var h=e.getLine(l).length;return new r(f,u,l,h)}},this.openingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i+1},u=e.$findClosingBracket(t,o,s);if(!u)return;var a=e.foldWidgets[u.row];return a==null&&(a=e.getFoldWidget(u.row)),a=="start"&&u.row>o.row&&(u.row--,u.column=e.getLine(u.row).length),r.fromPoints(o,u)},this.closingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i},u=e.$findOpeningBracket(t,o);if(!u)return;return u.column++,o.column--,r.fromPoints(u,o)}}).call(i.prototype)}),ace.define("ace/theme/textmate",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";t.isDark=!1,t.cssClass="ace-tm",t.cssText='.ace-tm .ace_gutter {background: #f0f0f0;color: #333;}.ace-tm .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-tm .ace_fold {background-color: #6B72E6;}.ace-tm {background-color: #FFFFFF;color: black;}.ace-tm .ace_cursor {color: black;}.ace-tm .ace_invisible {color: rgb(191, 191, 191);}.ace-tm .ace_storage,.ace-tm .ace_keyword {color: blue;}.ace-tm .ace_constant {color: rgb(197, 6, 11);}.ace-tm .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-tm .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-tm .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-tm .ace_invalid {background-color: rgba(255, 0, 0, 0.1);color: red;}.ace-tm .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-tm .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-tm .ace_support.ace_type,.ace-tm .ace_support.ace_class {color: rgb(109, 121, 222);}.ace-tm .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-tm .ace_string {color: rgb(3, 106, 7);}.ace-tm .ace_comment {color: rgb(76, 136, 107);}.ace-tm .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-tm .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-tm .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-tm .ace_variable {color: rgb(49, 132, 149);}.ace-tm .ace_xml-pe {color: rgb(104, 104, 91);}.ace-tm .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-tm .ace_heading {color: rgb(12, 7, 255);}.ace-tm .ace_list {color:rgb(185, 6, 144);}.ace-tm .ace_meta.ace_tag {color:rgb(0, 22, 142);}.ace-tm .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-tm .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-tm.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;}.ace-tm .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-tm .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-tm .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-tm .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-tm .ace_gutter-active-line {background-color : #dcdcdc;}.ace-tm .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-tm .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}',t.$id="ace/theme/textmate";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}),ace.define("ace/line_widgets",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e){this.session=e,this.session.widgetManager=this,this.session.getRowLength=this.getRowLength,this.session.$getWidgetScreenLength=this.$getWidgetScreenLength,this.updateOnChange=this.updateOnChange.bind(this),this.renderWidgets=this.renderWidgets.bind(this),this.measureWidgets=this.measureWidgets.bind(this),this.session._changedWidgets=[],this.$onChangeEditor=this.$onChangeEditor.bind(this),this.session.on("change",this.updateOnChange),this.session.on("changeFold",this.updateOnFold),this.session.on("changeEditor",this.$onChangeEditor)}var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./range").Range;(function(){this.getRowLength=function(e){var t;return this.lineWidgets?t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0:t=0,!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.$getWidgetScreenLength=function(){var e=0;return this.lineWidgets.forEach(function(t){t&&t.rowCount&&!t.hidden&&(e+=t.rowCount)}),e},this.$onChangeEditor=function(e){this.attach(e.editor)},this.attach=function(e){e&&e.widgetManager&&e.widgetManager!=this&&e.widgetManager.detach();if(this.editor==e)return;this.detach(),this.editor=e,e&&(e.widgetManager=this,e.renderer.on("beforeRender",this.measureWidgets),e.renderer.on("afterRender",this.renderWidgets))},this.detach=function(e){var t=this.editor;if(!t)return;this.editor=null,t.widgetManager=null,t.renderer.off("beforeRender",this.measureWidgets),t.renderer.off("afterRender",this.renderWidgets);var n=this.session.lineWidgets;n&&n.forEach(function(e){e&&e.el&&e.el.parentNode&&(e._inDocument=!1,e.el.parentNode.removeChild(e.el))})},this.updateOnFold=function(e,t){var n=t.lineWidgets;if(!n||!e.action)return;var r=e.data,i=r.start.row,s=r.end.row,o=e.action=="add";for(var u=i+1;u0&&!r[i])i--;this.firstRow=n.firstRow,this.lastRow=n.lastRow,t.$cursorLayer.config=n;for(var o=i;o<=s;o++){var u=r[o];if(!u||!u.el)continue;if(u.hidden){u.el.style.top=-100-(u.pixelHeight||0)+"px";continue}u._inDocument||(u._inDocument=!0,t.container.appendChild(u.el));var a=t.$cursorLayer.getPixelPosition({row:o,column:0},!0).top;u.coverLine||(a+=n.lineHeight*this.session.getRowLineCount(u.row)),u.el.style.top=a-n.offset+"px";var f=u.coverGutter?0:t.gutterWidth;u.fixedWidth||(f-=t.scrollLeft),u.el.style.left=f+"px",u.fullWidth&&u.screenWidth&&(u.el.style.minWidth=n.width+2*n.padding+"px"),u.fixedWidth?u.el.style.right=t.scrollBar.getWidth()+"px":u.el.style.right=""}}}).call(o.prototype),t.LineWidgets=o}),ace.define("ace/ext/error_marker",["require","exports","module","ace/line_widgets","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e,t,n){var r=0,i=e.length-1;while(r<=i){var s=r+i>>1,o=n(t,e[s]);if(o>0)r=s+1;else{if(!(o<0))return s;i=s-1}}return-(r+1)}function u(e,t,n){var r=e.getAnnotations().sort(s.comparePoints);if(!r.length)return;var i=o(r,{row:t,column:-1},s.comparePoints);i<0&&(i=-i-1),i>=r.length?i=n>0?0:r.length-1:i===0&&n<0&&(i=r.length-1);var u=r[i];if(!u||!n)return;if(u.row===t){do u=r[i+=n];while(u&&u.row===t);if(!u)return r.slice()}var a=[];t=u.row;do a[n<0?"unshift":"push"](u),u=r[i+=n];while(u&&u.row==t);return a.length&&a}var r=e("../line_widgets").LineWidgets,i=e("../lib/dom"),s=e("../range").Range;t.showErrorMarker=function(e,t){var n=e.session;n.widgetManager||(n.widgetManager=new r(n),n.widgetManager.attach(e));var s=e.getCursorPosition(),o=s.row,a=n.widgetManager.getWidgetsAtRow(o).filter(function(e){return e.type=="errorMarker"})[0];a?a.destroy():o-=t;var f=u(n,o,t),l;if(f){var c=f[0];s.column=(c.pos&&typeof c.column!="number"?c.pos.sc:c.column)||0,s.row=c.row,l=e.renderer.$gutterLayer.$annotations[s.row]}else{if(a)return;l={text:["Looks good!"],className:"ace_ok"}}e.session.unfold(s.row),e.selection.moveToPosition(s);var h={row:s.row,fixedWidth:!0,coverGutter:!0,el:i.createElement("div"),type:"errorMarker"},p=h.el.appendChild(i.createElement("div")),d=h.el.appendChild(i.createElement("div"));d.className="error_widget_arrow "+l.className;var v=e.renderer.$cursorLayer.getPixelPosition(s).left;d.style.left=v+e.renderer.gutterWidth-5+"px",h.el.className="error_widget_wrapper",p.className="error_widget "+l.className,p.innerHTML=l.text.join("
"),p.appendChild(i.createElement("div"));var m=function(e,t,n){if(t===0&&(n==="esc"||n==="return"))return h.destroy(),{command:"null"}};h.destroy=function(){if(e.$mouseHandler.isMousePressed)return;e.keyBinding.removeKeyboardHandler(m),n.widgetManager.removeLineWidget(h),e.off("changeSelection",h.destroy),e.off("changeSession",h.destroy),e.off("mouseup",h.destroy),e.off("change",h.destroy)},e.keyBinding.addKeyboardHandler(m),e.on("changeSelection",h.destroy),e.on("changeSession",h.destroy),e.on("mouseup",h.destroy),e.on("change",h.destroy),e.session.widgetManager.addLineWidget(h),h.el.onmousedown=e.focus.bind(e),e.renderer.scrollCursorIntoView(null,.5,{bottom:h.el.offsetHeight})},i.importCssString(" .error_widget_wrapper { background: inherit; color: inherit; border:none } .error_widget { border-top: solid 2px; border-bottom: solid 2px; margin: 5px 0; padding: 10px 40px; white-space: pre-wrap; } .error_widget.ace_error, .error_widget_arrow.ace_error{ border-color: #ff5a5a } .error_widget.ace_warning, .error_widget_arrow.ace_warning{ border-color: #F1D817 } .error_widget.ace_info, .error_widget_arrow.ace_info{ border-color: #5a5a5a } .error_widget.ace_ok, .error_widget_arrow.ace_ok{ border-color: #5aaa5a } .error_widget_arrow { position: absolute; border: solid 5px; border-top-color: transparent!important; border-right-color: transparent!important; border-left-color: transparent!important; top: -5px; }","")}),ace.define("ace/ace",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/dom","ace/lib/event","ace/range","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/worker/worker_client","ace/keyboard/hash_handler","ace/placeholder","ace/multi_select","ace/mode/folding/fold_mode","ace/theme/textmate","ace/ext/error_marker","ace/config"],function(e,t,n){"use strict";e("./lib/fixoldbrowsers");var r=e("./lib/dom"),i=e("./lib/event"),s=e("./range").Range,o=e("./editor").Editor,u=e("./edit_session").EditSession,a=e("./undomanager").UndoManager,f=e("./virtual_renderer").VirtualRenderer;e("./worker/worker_client"),e("./keyboard/hash_handler"),e("./placeholder"),e("./multi_select"),e("./mode/folding/fold_mode"),e("./theme/textmate"),e("./ext/error_marker"),t.config=e("./config"),t.require=e,typeof define=="function"&&(t.define=define),t.edit=function(e,n){if(typeof e=="string"){var s=e;e=document.getElementById(s);if(!e)throw new Error("ace.edit can't find div #"+s)}if(e&&e.env&&e.env.editor instanceof o)return e.env.editor;var u="";if(e&&/input|textarea/i.test(e.tagName)){var a=e;u=a.value,e=r.createElement("pre"),a.parentNode.replaceChild(e,a)}else e&&(u=e.textContent,e.innerHTML="");var l=t.createEditSession(u),c=new o(new f(e),l,n),h={document:l,editor:c,onResize:c.resize.bind(c,null)};return a&&(h.textarea=a),i.addListener(window,"resize",h.onResize),c.on("destroy",function(){i.removeListener(window,"resize",h.onResize),h.editor.container.env=null}),c.container.env=c.env=h,c},t.createEditSession=function(e,t){var n=new u(e,t);return n.setUndoManager(new a),n},t.Range=s,t.Editor=o,t.EditSession=u,t.UndoManager=a,t.VirtualRenderer=f,t.version="1.4.2"}); (function() { - ace.require(["ace/ace"], function(a) { - if (a) { - a.config.init(true); - a.define = ace.define; - } - if (!window.ace) - window.ace = a; - for (var key in a) if (a.hasOwnProperty(key)) - window.ace[key] = a[key]; - window.ace["default"] = window.ace; - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = window.ace; - } - }); - })(); diff --git a/esphome/dashboard/static/css/esphome.css b/esphome/dashboard/static/css/esphome.css new file mode 100644 index 0000000000..db0ac55985 --- /dev/null +++ b/esphome/dashboard/static/css/esphome.css @@ -0,0 +1,393 @@ +/* Base */ + +:root { + /* Colors */ + --primary-bg-color: #fafafa; + + --alert-standard-color: #666666; + --alert-standard-color-bg: #e6e6e6; + --alert-info-color: #00539f; + --alert-info-color-bg: #E6EEF5; + --alert-success-color: #4CAF50; + --alert-success-color-bg: #EDF7EE; + --alert-warning-color: #FF9800; + --alert-warning-color-bg: #FFF5E6; + --alert-error-color: #D93025; + --alert-error-color-bg: #FAEFEB; +} + +body { + display: flex; + min-height: 100vh; + flex-direction: column; + background-color: var(--primary-bg-color); +} + +/* Layout */ +.valign-wrapper { + position: absolute; + width:100vw; + height:100vh; +} + +.valign { + width: 100%; +} + +main { + flex: 1 0 auto; +} + +/* Alerts & Errors */ +.alert { + width: 100%; + margin: 10px auto; + padding: 10px; + border-radius: 2px; + border-left-width: 4px; + border-left-style: solid; +} + +.alert .title { + font-weight: bold; +} + +.alert .title::after { + content: "\A"; + white-space: pre; +} + +.alert.alert-error { + color: var(--alert-error-color); + border-left-color: var(--alert-error-color); + background-color: var(--alert-error-color-bg); +} + +.card.card-error, .card.status-offline { + border-top: 4px solid var(--alert-error-color); +} + +.card.status-online { + border-top: 4px solid var(--alert-success-color); +} + +.card.status-not-responding { + border-top: 4px solid var(--alert-warning-color); +} + +.card.status-unknown { + border-top: 4px solid var(--alert-standard-color); +} + +/* Login Page */ +#login-page .row.no-bottom-margin { + margin-bottom: 0 !important; +} + +#login-page .logo { + display: block; + width: auto; + max-width: 300px; + margin-left: auto; + margin-right: auto; +} + +#login-page .input-field input:focus + label { + color: #000; +} + +#login-page .input-field input:focus { + border-bottom: 1px solid #000; + box-shadow: 0 1px 0 0 #000; +} + +#login-page .input-field .prefix.active { + color: #000; +} + +#login-page .version-number { + display: block; + text-align: center; + margin-bottom: 20px;; + color:#808080; + font-size: 12px; +} + +#login-page footer { + color: #757575; + font-size: 12px; +} + +#login-page footer a { + color: #424242; +} + +#login-page footer p { + -webkit-margin-before: 0px; + margin-block-start: 0px; + -webkit-margin-after: 5px; + margin-block-end: 5px; +} + +#login-page footer p:last-child { + -webkit-margin-after: 0px; + margin-block-end: 0px; +} + +/* Dashboard */ +.logo-wrapper { + height: 64px; + height: 100%; + width: 0; + margin-left: 24px; +} + +.logo { + width: auto; + height: 48px; + margin: 8px 0; +} + +@media only screen and (max-width: 601px) { + .logo { + height: 38px; + margin: 9px 0; + } +} + +.nav-icons { + margin-right: 24px; +} + +.nav-icons i { + color: black; +} + +.select-port-container { + margin-top: 8px; + margin-right: 10px; + width: 350px; +} + +.serial-port-select { + margin-top: 8px; + margin-right: 10px; + width: 350px; +} + +.serial-port-select .select-dropdown { + color: black; +} + +.serial-port-select .select-dropdown:focus { + border-bottom: 1px solid #607d8b !important; +} + +.serial-port-select .caret { + fill: black; +} + +.serial-port-select .dropdown-content li>span { + color: black; +} + +#nav-dropdown li a, .node-dropdown li a { + color: black; +} + +main .container { + margin-top: 20px; + margin-bottom: 20px; + width: 90%; + max-width: 1920px; +} + +#nodes .card-content { + height: calc(100% - 47px); +} + +#nodes .card-content, #nodes .card-action { + padding: 12px; +} + +#nodes .grid-1-col { + display: grid; + grid-template-columns: 1fr; +} + +#nodes .grid-2-col { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 1.5rem; +} + +#nodes .grid-3-col { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-column-gap: 1.5rem; +} + +@media only screen and (max-width: 1100px) { + #nodes .grid-3-col { + grid-template-columns: 1fr 1fr; + grid-column-gap: 1.5rem; + } +} + +@media only screen and (max-width: 750px) { + #nodes .grid-2-col { + grid-template-columns: 1fr; + grid-column-gap: 0; + } + + #nodes .grid-3-col { + grid-template-columns: 1fr; + grid-column-gap: 0; + } +} + +i.node-update-avaliable { + color:#3f51b5; +} + +i.node-webserver { + color:#039be5; +} + +.node-config-path { + margin-top: -8px; + margin-bottom: 8px; + font-size: 14px; +} + +.node-card-comment { + color: #444; + font-style: italic; +} + +.card-action a, .card-dropdown-action a { + cursor: pointer; +} + +.tooltipped { + cursor: help; +} + +#js-loading-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.editor { + margin-top: 0; + margin-bottom: 0; + border-radius: 3px; + height: calc(100% - 56px); +} + +.inlinecode { + box-sizing: border-box; + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; + font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; +} + +.log { + height: 100%; + max-height: calc(100% - 56px); + background-color: #1c1c1c; + margin-top: 0; + margin-bottom: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; + padding: 16px; + overflow: auto; + line-height: 1.45; + border-radius: 3px; + white-space: pre-wrap; + overflow-wrap: break-word; + color: #DDD; +} + +.log-bold { font-weight: bold; } +.log-italic { font-style: italic; } +.log-underline { text-decoration: underline; } +.log-strikethrough { text-decoration: line-through; } +.log-underline.log-strikethrough { text-decoration: underline line-through; } +.log-secret { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.log-secret-redacted { + opacity: 0; + width: 1px; + font-size: 1px; +} +.log-fg-black { color: rgb(128,128,128); } +.log-fg-red { color: rgb(255,0,0); } +.log-fg-green { color: rgb(0,255,0); } +.log-fg-yellow { color: rgb(255,255,0); } +.log-fg-blue { color: rgb(0,0,255); } +.log-fg-magenta { color: rgb(255,0,255); } +.log-fg-cyan { color: rgb(0,255,255); } +.log-fg-white { color: rgb(187,187,187); } +.log-bg-black { background-color: rgb(0,0,0); } +.log-bg-red { background-color: rgb(255,0,0); } +.log-bg-green { background-color: rgb(0,255,0); } +.log-bg-yellow { background-color: rgb(255,255,0); } +.log-bg-blue { background-color: rgb(0,0,255); } +.log-bg-magenta { background-color: rgb(255,0,255); } +.log-bg-cyan { background-color: rgb(0,255,255); } +.log-bg-white { background-color: rgb(255,255,255); } + +ul.browser-default { + padding-left: 30px; + margin-top: 10px; + margin-bottom: 15px; +} + +ul.browser-default li { + list-style-type: initial; +} + +ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before { + background-color: #3f51b5 !important; +} + +.select-action { + width: auto !important; + height: auto !important; + white-space: nowrap; +} + +.modal { + width: 95%; + max-height: 90%; + height: 85% !important; +} + +.page-footer { + display: flex; + align-items: center; + min-height: 50px; + padding-top: 0; + color: grey; +} + +.page-footer a { + color: #afafaf; +} + +@media only screen and (max-width: 992px) { + .page-footer .left, .page-footer .right { + width: 100%; + text-align: center; + } +} diff --git a/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css b/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css new file mode 100644 index 0000000000..390f16a011 --- /dev/null +++ b/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css @@ -0,0 +1,10 @@ +/** + * Materialize Stepper - A little plugin that implements a stepper to Materializecss framework. + * @version v3.1.0 + * @author Igor Marcossi (Kinark) . + * @link https://github.com/Kinark/Materialize-stepper + * + * Licensed under the MIT License (https://github.com/Kinark/Materialize-stepper/blob/master/LICENSE). + */ + + .card-content ul.stepper{margin:1em -24px;padding:0 24px}@media only screen and (min-width:993px){.card-content ul.stepper.horizontal{margin-left:-24px;margin-right:-24px;padding-left:24px;padding-right:24px}.card-content ul.stepper.horizontal:first-child{margin-top:-24px}.card-content ul.stepper.horizontal .step.step-content{padding-left:40px;padding-right:40px}.card-content ul.stepper.horizontal .step.step-content .step-actions{padding-left:40px;padding-right:40px}}ul.stepper{counter-reset:section;overflow-y:auto;overflow-x:hidden}ul.stepper .wait-feedback{left:0;right:0;top:0;z-index:2;position:absolute;width:100%;height:100%;text-align:center;display:flex;justify-content:center;align-items:center}ul.stepper .step{position:relative;transition:height .4s cubic-bezier(.4,0,.2,1),padding-bottom .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step .step-title{margin:0 -24px;cursor:pointer;padding:15.5px 44px 24px 64px;display:block}ul.stepper .step .step-title:hover{background-color:rgba(0,0,0,.06)}ul.stepper .step .step-title::after{content:attr(data-step-label);display:block;position:absolute;font-size:12.8px;font-size:.8rem;color:#424242;font-weight:400}ul.stepper .step .step-content{position:relative;display:none;height:0;transition:height .4s cubic-bezier(.4,0,.2,1);width:inherit;overflow:visible;margin-left:41px;margin-right:24px}ul.stepper .step .step-content .step-actions{padding-top:16px;padding-bottom:4px;display:flex;justify-content:flex-start}ul.stepper .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper .step .step-content .step-actions .btn:not(:last-child){margin-right:5px}ul.stepper .step .step-content .row{margin-bottom:7px}ul.stepper .step::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:13px}ul.stepper .step.active .step-title{font-weight:500}ul.stepper .step.active .step-content{height:auto;display:block}ul.stepper .step.active::before,ul.stepper .step.done::before{background-color:#2196f3}ul.stepper .step.done::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper .step.wrong::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper .step.feedbacking .step-content>:not(.wait-feedback){opacity:.1}ul.stepper .step:not(:last-of-type)::after{content:'';position:absolute;top:52px;left:13.5px;width:1px;height:40%;height:calc(100% - 52px);background-color:rgba(0,0,0,.1);transition:height .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step:not(:last-of-type).active{padding-bottom:36px}ul.stepper>li:not(:last-of-type){padding-bottom:10px}@media only screen and (min-width:993px){ul.stepper.horizontal{position:relative;display:flex;justify-content:space-between;min-height:458px;overflow:hidden}ul.stepper.horizontal::before{content:'';background-color:transparent;width:100%;min-height:84px;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:absolute;left:0}ul.stepper.horizontal .step{position:static;padding:0!important;width:100%;display:flex;align-items:center;height:84px}ul.stepper.horizontal .step::before{content:none}ul.stepper.horizontal .step:last-of-type{width:auto!important}ul.stepper.horizontal .step.active:not(:last-of-type)::after,ul.stepper.horizontal .step:not(:last-of-type)::after{content:'';position:static;display:inline-block;width:100%;height:1px}ul.stepper.horizontal .step .step-title{line-height:84px;height:84px;margin:0;padding:0 25px 0 65px;display:inline-block;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}ul.stepper.horizontal .step .step-title::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:28.5px;left:19px}ul.stepper.horizontal .step .step-title::after{top:15px}ul.stepper.horizontal .step.active~.step .step-content{left:100%}ul.stepper.horizontal .step.active .step-content{left:0!important}ul.stepper.horizontal .step.active .step-title::before,ul.stepper.horizontal .step.done .step-title::before{background-color:#2196f3}ul.stepper.horizontal .step.done .step-title::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper.horizontal .step.wrong .step-title::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper.horizontal .step .step-content{position:absolute;height:calc(100% - 84px);top:84px;display:block;left:-100%;width:100%;overflow-y:auto;overflow-x:hidden;margin:0;padding:20px 20px 76px 20px;transition:left .4s cubic-bezier(.4,0,.2,1)}ul.stepper.horizontal .step .step-content .step-actions{position:absolute;bottom:0;left:0;width:100%;padding:20px;background-color:transparent;flex-direction:row-reverse}ul.stepper.horizontal .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn:not(:last-child){margin-left:5px;margin-right:0}} diff --git a/esphome/dashboard/static/materialize.min.css b/esphome/dashboard/static/css/vendor/materialize/materialize.min.css similarity index 100% rename from esphome/dashboard/static/materialize.min.css rename to esphome/dashboard/static/css/vendor/materialize/materialize.min.css diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css deleted file mode 100644 index fddfb5cf86..0000000000 --- a/esphome/dashboard/static/esphome.css +++ /dev/null @@ -1,256 +0,0 @@ -nav .brand-logo { - margin-left: 48px; - font-size: 20px; -} - -main .container { - margin-top: -12vh; - flex-shrink: 0; -} - -.ribbon { - width: 100%; - height: 17vh; - background-color: #3F51B5; - flex-shrink: 0; -} - -.ribbon-fab:not(.tap-target-origin) { - position: absolute; - right: 24px; - top: calc(17vh + 34px); -} - -i.very-large { - font-size: 8rem; - padding-top: 2px; - color: #424242; -} - -.card .card-content { - padding-left: 18px; - padding-bottom: 10px; -} - -.card-action a, .card-dropdown-action a { - cursor: pointer; -} - -.inlinecode { - box-sizing: border-box; - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: rgba(27,31,35,0.05); - border-radius: 3px; - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; -} - -.log { - height: 100%; - max-height: calc(100% - 56px); - background-color: #1c1c1c; - margin-top: 0; - margin-bottom: 0; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; - padding: 16px; - overflow: auto; - line-height: 1.45; - border-radius: 3px; - white-space: pre-wrap; - overflow-wrap: break-word; - color: #DDD; -} - -.log-bold { font-weight: bold; } -.log-italic { font-style: italic; } -.log-underline { text-decoration: underline; } -.log-strikethrough { text-decoration: line-through; } -.log-underline.log-strikethrough { text-decoration: underline line-through; } -.log-secret { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.log-secret-redacted { - opacity: 0; - width: 1px; - font-size: 1px; -} -.log-fg-black { color: rgb(128,128,128); } -.log-fg-red { color: rgb(255,0,0); } -.log-fg-green { color: rgb(0,255,0); } -.log-fg-yellow { color: rgb(255,255,0); } -.log-fg-blue { color: rgb(0,0,255); } -.log-fg-magenta { color: rgb(255,0,255); } -.log-fg-cyan { color: rgb(0,255,255); } -.log-fg-white { color: rgb(187,187,187); } -.log-bg-black { background-color: rgb(0,0,0); } -.log-bg-red { background-color: rgb(255,0,0); } -.log-bg-green { background-color: rgb(0,255,0); } -.log-bg-yellow { background-color: rgb(255,255,0); } -.log-bg-blue { background-color: rgb(0,0,255); } -.log-bg-magenta { background-color: rgb(255,0,255); } -.log-bg-cyan { background-color: rgb(0,255,255); } -.log-bg-white { background-color: rgb(255,255,255); } - -.modal { - width: 95%; - max-height: 90%; - height: 85% !important; -} - -.page-footer { - padding-top: 0; -} - -body { - display: flex; - min-height: 100vh; - flex-direction: column; -} - -main { - flex: 1 0 auto; -} - -ul.browser-default { - padding-left: 30px; - margin-top: 10px; - margin-bottom: 15px; -} - -ul.browser-default li { - list-style-type: initial; -} - -ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before { - background-color: #3f51b5 !important; -} - -.select-port-container { - margin-top: 8px; - margin-right: 10px; - width: 350px; -} - -#dropdown-nav-trigger { - margin-right: 24px; -} - -.select-port-container .select-dropdown { - color: #fff; -} - -.select-port-container .caret { - fill: #fff; -} - -.dropdown-trigger { - cursor: pointer; -} - -.select-action { - width: auto !important; - height: auto !important; - white-space: nowrap; -} - -.tap-target-wrapper { - position: fixed !important; -} - -/* https://github.com/tnhu/status-indicator/blob/master/styles.css */ -.status-indicator .status-indicator-icon { - display: inline-block; - border-radius: 50%; - width: 10px; - height: 10px; -} - -.status-indicator.unknown .status-indicator-icon { - background-color: rgb(216, 226, 233); -} - -.status-indicator.unknown .status-indicator-text::after { - content: "Unknown status"; -} - -.status-indicator.offline .status-indicator-icon { - background-color: rgb(255, 77, 77); -} - -.status-indicator.offline .status-indicator-text::after { - content: "Offline"; -} - -.status-indicator.not-responding .status-indicator-icon { - background-color: rgb(255, 170, 0); -} - -.status-indicator.not-responding .status-indicator-text::after { - content: "Not Responding"; -} - -@keyframes status-indicator-pulse-online { - 0% { - box-shadow: 0 0 0 0 rgba(75, 210, 143, .5); - } - 25% { - box-shadow: 0 0 0 10px rgba(75, 210, 143, 0); - } - 30% { - box-shadow: 0 0 0 0 rgba(75, 210, 143, 0); - } -} - -.status-indicator.online .status-indicator-icon { - background-color: rgb(75, 210, 143); - animation-duration: 5s; - animation-timing-function: ease-in-out; - animation-iteration-count: infinite; - animation-direction: normal; - animation-delay: 0s; - animation-fill-mode: none; - animation-name: status-indicator-pulse-online; -} - -.status-indicator.online .status-indicator-text::after { - content: "Online"; -} - -#editor { - margin-top: 0; - margin-bottom: 0; - border-radius: 3px; - height: calc(100% - 56px); -} - -.update-available i { - vertical-align: bottom; - font-size: 20px !important; - color: #3F51B5 !important; - margin-right: -4.5px; - margin-left: -5.5px; -} - -.flash-using-esphomeflasher { - vertical-align: middle; - color: #666 !important; -} - -.error { - background: #e53935; - color: #fff; - padding: 10px 15px; - margin-top: 15px; -} - -.card-comment { - margin-bottom: 8px; - font-size: 14px; - color: #444; - font-style: italic; -} diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js deleted file mode 100644 index e284690ac6..0000000000 --- a/esphome/dashboard/static/esphome.js +++ /dev/null @@ -1,782 +0,0 @@ -// Disclaimer: This file was written in a hurry and by someone -// who does not know JS at all. This file desperately needs cleanup. - -// ============================= Global Vars ============================= -document.addEventListener('DOMContentLoaded', () => { - M.AutoInit(document.body); -}); -const loc = window.location; -const wsLoc = new URL("./",`${loc.protocol}//${loc.host}${loc.pathname}`); -wsLoc.protocol = 'ws:'; -if (loc.protocol === "https:") { - wsLoc.protocol = 'wss:'; -} -const wsUrl = wsLoc.href; - -// ============================= Color Log Parsing ============================= -const initializeColorState = () => { - return { - bold: false, - italic: false, - underline: false, - strikethrough: false, - foregroundColor: false, - backgroundColor: false, - carriageReturn: false, - secret: false, - }; -}; - -const colorReplace = (pre, state, text) => { - const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; - let i = 0; - - if (state.carriageReturn) { - if (text !== "\n") { - // don't remove if \r\n - pre.removeChild(pre.lastChild); - } - state.carriageReturn = false; - } - - if (text.includes("\r")) { - state.carriageReturn = true; - } - - const lineSpan = document.createElement("span"); - lineSpan.classList.add("line"); - pre.appendChild(lineSpan); - - const addSpan = (content) => { - if (content === "") - return; - - const span = document.createElement("span"); - if (state.bold) span.classList.add("log-bold"); - if (state.italic) span.classList.add("log-italic"); - if (state.underline) span.classList.add("log-underline"); - if (state.strikethrough) span.classList.add("log-strikethrough"); - if (state.secret) span.classList.add("log-secret"); - if (state.foregroundColor !== null) span.classList.add(`log-fg-${state.foregroundColor}`); - if (state.backgroundColor !== null) span.classList.add(`log-bg-${state.backgroundColor}`); - span.appendChild(document.createTextNode(content)); - lineSpan.appendChild(span); - - if (state.secret) { - const redacted = document.createElement("span"); - redacted.classList.add("log-secret-redacted"); - redacted.appendChild(document.createTextNode("[redacted]")); - lineSpan.appendChild(redacted); - } - }; - - - while (true) { - const match = re.exec(text); - if (match === null) - break; - - const j = match.index; - addSpan(text.substring(i, j)); - i = j + match[0].length; - - if (match[1] === undefined) continue; - - for (const colorCode of match[1].split(";")) { - switch (parseInt(colorCode)) { - case 0: - // reset - state.bold = false; - state.italic = false; - state.underline = false; - state.strikethrough = false; - state.foregroundColor = null; - state.backgroundColor = null; - state.secret = false; - break; - case 1: - state.bold = true; - break; - case 3: - state.italic = true; - break; - case 4: - state.underline = true; - break; - case 5: - state.secret = true; - break; - case 6: - state.secret = false; - break; - case 9: - state.strikethrough = true; - break; - case 22: - state.bold = false; - break; - case 23: - state.italic = false; - break; - case 24: - state.underline = false; - break; - case 29: - state.strikethrough = false; - break; - case 30: - state.foregroundColor = "black"; - break; - case 31: - state.foregroundColor = "red"; - break; - case 32: - state.foregroundColor = "green"; - break; - case 33: - state.foregroundColor = "yellow"; - break; - case 34: - state.foregroundColor = "blue"; - break; - case 35: - state.foregroundColor = "magenta"; - break; - case 36: - state.foregroundColor = "cyan"; - break; - case 37: - state.foregroundColor = "white"; - break; - case 39: - state.foregroundColor = null; - break; - case 41: - state.backgroundColor = "red"; - break; - case 42: - state.backgroundColor = "green"; - break; - case 43: - state.backgroundColor = "yellow"; - break; - case 44: - state.backgroundColor = "blue"; - break; - case 45: - state.backgroundColor = "magenta"; - break; - case 46: - state.backgroundColor = "cyan"; - break; - case 47: - state.backgroundColor = "white"; - break; - case 40: - case 49: - state.backgroundColor = null; - break; - } - } - } - addSpan(text.substring(i)); - if (pre.scrollTop + 56 >= (pre.scrollHeight - pre.offsetHeight)) { - // at bottom - pre.scrollTop = pre.scrollHeight; - } -}; - -// ============================= Online/Offline Status Indicators ============================= -let isFetchingPing = false; -const fetchPing = () => { - if (isFetchingPing) - return; - isFetchingPing = true; - - fetch(`./ping`, {credentials: "same-origin"}).then(res => res.json()) - .then(response => { - for (let filename in response) { - let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); - if (node === null) - continue; - - let status = response[filename]; - let klass; - if (status === null) { - klass = 'unknown'; - } else if (status === true) { - klass = 'online'; - node.setAttribute('data-last-connected', Date.now().toString()); - } else if (node.hasAttribute('data-last-connected')) { - const attr = parseInt(node.getAttribute('data-last-connected')); - if (Date.now() - attr <= 5000) { - klass = 'not-responding'; - } else { - klass = 'offline'; - } - } else { - klass = 'offline'; - } - - if (node.classList.contains(klass)) - continue; - - node.classList.remove('unknown', 'online', 'offline', 'not-responding'); - node.classList.add(klass); - } - - isFetchingPing = false; - }); -}; -setInterval(fetchPing, 2000); -fetchPing(); - -// ============================= Serial Port Selector ============================= -const portSelect = document.querySelector('.nav-wrapper select'); -let ports = []; - -const fetchSerialPorts = (begin=false) => { - fetch(`./serial-ports`, {credentials: "same-origin"}).then(res => res.json()) - .then(response => { - if (ports.length === response.length) { - let allEqual = true; - for (let i = 0; i < response.length; i++) { - if (ports[i].port !== response[i].port) { - allEqual = false; - break; - } - } - if (allEqual) - return; - } - const hasNewPort = response.length >= ports.length; - - ports = response; - - const inst = M.FormSelect.getInstance(portSelect); - if (inst !== undefined) { - inst.destroy(); - } - - portSelect.innerHTML = ""; - const prevSelected = getUploadPort(); - for (let i = 0; i < response.length; i++) { - const val = response[i]; - if (val.port === prevSelected) { - portSelect.innerHTML += ``; - } else { - portSelect.innerHTML += ``; - } - } - - M.FormSelect.init(portSelect, {}); - if (!begin && hasNewPort) - M.toast({html: "Discovered new serial port."}); - }); -}; - -const getUploadPort = () => { - const inst = M.FormSelect.getInstance(portSelect); - if (inst === undefined) { - return "OTA"; - } - - inst._setSelectedStates(); - return inst.getSelectedValues()[0]; -}; -setInterval(fetchSerialPorts, 5000); -fetchSerialPorts(true); - - -// ============================= Logs Button ============================= - -class LogModalElem { - constructor({ - name, - onPrepare = (modalElem, config) => {}, - onProcessExit = (modalElem, code) => {}, - onSocketClose = (modalElem) => {}, - dismissible = true, - }) { - this.modalId = `modal-${name}`; - this.actionClass = `action-${name}`; - this.wsUrl = `${wsUrl}${name}`; - this.dismissible = dismissible; - this.activeConfig = null; - - this.modalElem = document.getElementById(this.modalId); - this.logElem = this.modalElem.querySelector('.log'); - this.onPrepare = onPrepare; - this.onProcessExit = onProcessExit; - this.onSocketClose = onSocketClose; - } - - setup() { - const boundOnPress = this._onPress.bind(this); - document.querySelectorAll(`.${this.actionClass}`).forEach((btn) => { - btn.addEventListener('click', boundOnPress); - }); - } - - _setupModalInstance() { - this.modalInstance = M.Modal.getInstance(this.modalElem); - this.modalInstance.options.dismissible = this.dismissible; - this._boundKeydown = this._onKeydown.bind(this); - this.modalInstance.options.onOpenStart = () => { - document.addEventListener('keydown', this._boundKeydown); - }; - this.modalInstance.options.onCloseStart = this._onCloseStart.bind(this); - } - - _onCloseStart() { - document.removeEventListener('keydown', this._boundKeydown); - this.activeSocket.close(); - } - - open(event) { - this._onPress(event); - } - - _onPress(event) { - this.activeConfig = event.target.getAttribute('data-node'); - this._setupModalInstance(); - // clear log - this.logElem.innerHTML = ""; - const colorlogState = initializeColorState(); - // prepare modal - this.modalElem.querySelectorAll('.filename').forEach((field) => { - field.innerHTML = this.activeConfig; - }); - this.onPrepare(this.modalElem, this.activeConfig); - document.addEventListener('keydown', this._onKeydown); - - let stopped = false; - - // open modal - this.modalInstance.open(); - - const socket = new WebSocket(this.wsUrl); - this.activeSocket = socket; - socket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(this.logElem, colorlogState, data.data); - } else if (data.event === "exit") { - this.onProcessExit(this.modalElem, data.code); - stopped = true; - } - }); - socket.addEventListener('open', () => { - const msg = JSON.stringify(this.encodeSpawnMessage(this.activeConfig)); - socket.send(msg); - }); - socket.addEventListener('close', () => { - if (!stopped) { - this.onSocketClose(this.modalElem); - } - }); - } - - _onKeydown(event) { - if (event.keyCode === 27) { - this.modalInstance.close(); - } - } - - encodeSpawnMessage(config) { - return { - type: 'spawn', - configuration: config, - port: getUploadPort(), - }; - } -} - -const logsModal = new LogModalElem({ - name: "logs", - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - } else { - M.toast({html: `Program failed with code ${code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -logsModal.setup(); - -const retryUploadButton = document.querySelector('.retry-upload'); -const editAfterUploadButton = document.querySelector('.edit-after-upload'); -const downloadAfterUploadButton = document.querySelector('.download-after-upload'); -const uploadModal = new LogModalElem({ - name: 'upload', - onPrepare: (modalElem, config) => { - downloadAfterUploadButton.classList.add('disabled'); - retryUploadButton.setAttribute('data-node', uploadModal.activeConfig); - retryUploadButton.classList.add('disabled'); - editAfterUploadButton.setAttribute('data-node', uploadModal.activeConfig); - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - // if compilation succeeds but OTA fails, you can still download the binary and upload manually - downloadAfterUploadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${code}`}); - downloadAfterUploadButton.classList.add('disabled'); - retryUploadButton.classList.remove('disabled'); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -uploadModal.setup(); -downloadAfterUploadButton.addEventListener('click', () => { - const link = document.createElement("a"); - link.download = name; - link.href = `./download.bin?configuration=${encodeURIComponent(uploadModal.activeConfig)}`; - document.body.appendChild(link); - link.click(); - link.remove(); -}); - -const validateModal = new LogModalElem({ - name: 'validate', - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - modalElem.querySelector(".action-edit").setAttribute('data-node', validateModal.activeConfig); - modalElem.querySelector(".action-upload").setAttribute('data-node', validateModal.activeConfig); - modalElem.querySelector(".action-upload").classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({ - html: `${validateModal.activeConfig} is valid 👍`, - displayLength: 5000, - }); - modalElem.querySelector(".action-upload").classList.remove('disabled'); - } else { - M.toast({ - html: `${validateModal.activeConfig} is invalid 😕`, - displayLength: 5000, - }); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -validateModal.setup(); - -const downloadButton = document.querySelector('.download-binary'); -const compileModal = new LogModalElem({ - name: 'compile', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - downloadButton.classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -compileModal.setup(); -downloadButton.addEventListener('click', () => { - const link = document.createElement("a"); - link.download = name; - link.href = `./download.bin?configuration=${encodeURIComponent(compileModal.activeConfig)}`; - document.body.appendChild(link); - link.click(); - link.remove(); -}); - -const cleanMqttModal = new LogModalElem({ - name: 'clean-mqtt', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -cleanMqttModal.setup(); - -const cleanModal = new LogModalElem({ - name: 'clean', - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - } else { - M.toast({html: `Program failed with code ${code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -cleanModal.setup(); - -document.querySelectorAll(".action-delete").forEach((btn) => { - btn.addEventListener('click', (e) => { - let configuration = e.target.getAttribute('data-node'); - - fetch(`./delete?configuration=${configuration}`, { - credentials: "same-origin", - method: "POST", - }).then(res => res.text()).then(() => { - const toastHtml = `Deleted ${configuration} - `; - const toast = M.toast({html: toastHtml}); - const undoButton = toast.el.querySelector('.toast-action'); - - document.querySelector(`.entry-row[data-node="${configuration}"]`).remove(); - - undoButton.addEventListener('click', () => { - fetch(`./undo-delete?configuration=${configuration}`, { - credentials: "same-origin", - method: "POST", - }).then(res => res.text()).then(() => { - window.location.reload(false); - }); - }); - }); - }); -}); - -const editModalElem = document.getElementById("modal-editor"); -const editorElem = editModalElem.querySelector("#editor"); -const editor = ace.edit(editorElem); -let activeEditorConfig = null; -let activeEditorSecrets = false; -let aceWs = null; -let aceValidationScheduled = false; -let aceValidationRunning = false; -const startAceWebsocket = () => { - aceWs = new WebSocket(`${wsUrl}ace`); - aceWs.addEventListener('message', (event) => { - const raw = JSON.parse(event.data); - if (raw.event === "line") { - const msg = JSON.parse(raw.data); - if (msg.type === "result") { - const arr = []; - - for (const v of msg.validation_errors) { - let o = { - text: v.message, - type: 'error', - row: 0, - column: 0 - }; - if (v.range != null) { - o.row = v.range.start_line; - o.column = v.range.start_col; - } - arr.push(o); - } - for (const v of msg.yaml_errors) { - arr.push({ - text: v.message, - type: 'error', - row: 0, - column: 0 - }); - } - - editor.session.setAnnotations(arr); - - if(arr.length) { - editorUploadButton.classList.add('disabled'); - } else { - editorUploadButton.classList.remove('disabled'); - } - - aceValidationRunning = false; - } else if (msg.type === "read_file") { - sendAceStdin({ - type: 'file_response', - content: editor.getValue() - }); - } - } - }); - aceWs.addEventListener('open', () => { - const msg = JSON.stringify({type: 'spawn'}); - aceWs.send(msg); - }); - aceWs.addEventListener('close', () => { - aceWs = null; - setTimeout(startAceWebsocket, 5000) - }); -}; -const sendAceStdin = (data) => { - let send = JSON.stringify({ - type: 'stdin', - data: JSON.stringify(data)+'\n', - }); - aceWs.send(send); -}; -startAceWebsocket(); - -editor.setTheme("ace/theme/dreamweaver"); -editor.session.setMode("ace/mode/yaml"); -editor.session.setOption('useSoftTabs', true); -editor.session.setOption('tabSize', 2); -editor.session.setOption('useWorker', false); - -const saveButton = editModalElem.querySelector(".save-button"); -const editorUploadButton = editModalElem.querySelector(".editor-upload-button"); -const saveEditor = () => { - fetch(`./edit?configuration=${activeEditorConfig}`, { - credentials: "same-origin", - method: "POST", - body: editor.getValue() - }).then(res => res.text()).then(() => { - M.toast({ - html: `Saved ${activeEditorConfig}` - }); - }); -}; - -const debounce = (func, wait) => { - let timeout; - return function() { - let context = this, args = arguments; - let later = function() { - timeout = null; - func.apply(context, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -}; - -editor.commands.addCommand({ - name: 'saveCommand', - bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, - exec: saveEditor, - readOnly: false -}); - -editor.session.on('change', debounce(() => { - aceValidationScheduled = !activeEditorSecrets; -}, 250)); - -setInterval(() => { - if (!aceValidationScheduled || aceValidationRunning) - return; - if (aceWs == null) - return; - - sendAceStdin({ - type: 'validate', - file: activeEditorConfig - }); - aceValidationRunning = true; - aceValidationScheduled = false; -}, 100); - -saveButton.addEventListener('click', saveEditor); -editorUploadButton.addEventListener('click', saveEditor); - -document.querySelectorAll(".action-edit").forEach((btn) => { - btn.addEventListener('click', (e) => { - activeEditorConfig = e.target.getAttribute('data-node'); - activeEditorSecrets = activeEditorConfig === 'secrets.yaml'; - const modalInstance = M.Modal.getInstance(editModalElem); - const filenameField = editModalElem.querySelector('.filename'); - editorUploadButton.setAttribute('data-node', activeEditorConfig); - if (activeEditorSecrets) { - editorUploadButton.classList.add('disabled'); - } - filenameField.innerHTML = activeEditorConfig; - - editor.setValue("Loading configuration yaml..."); - editor.setOption('readOnly', true); - fetch(`./edit?configuration=${activeEditorConfig}`, {credentials: "same-origin"}) - .then(res => res.text()).then(response => { - editor.setValue(response, -1); - editor.setOption('readOnly', false); - }); - - modalInstance.open(); - }); -}); - -const modalSetupElem = document.getElementById("modal-wizard"); -const setupWizardStart = document.getElementById('setup-wizard-start'); -const startWizard = () => { - const modalInstance = M.Modal.getInstance(modalSetupElem); - modalInstance.open(); - - $('.stepper').activateStepper({ - linearStepsNavigation: false, - autoFocusInput: true, - autoFormCreation: true, - showFeedbackLoader: true, - parallel: false - }); -}; - -setupWizardStart.addEventListener('click', startWizard); - -jQuery.validator.addMethod("nospaces", (value, element) => { - return value.indexOf(' ') < 0; -}, "Name must not contain spaces."); - -jQuery.validator.addMethod("lowercase", (value, element) => { - return value === value.toLowerCase(); -}, "Name must be lowercase."); - -const updateAllModal = new LogModalElem({ - name: 'update-all', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - downloadButton.classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -updateAllModal.setup(); - -const updateAllButton = document.getElementById('update-all-button'); -updateAllButton.addEventListener('click', (e) => { - updateAllModal.open(e); -}); diff --git a/esphome/dashboard/static/ext-searchbox.js b/esphome/dashboard/static/ext-searchbox.js deleted file mode 100644 index c6379623a7..0000000000 --- a/esphome/dashboard/static/ext-searchbox.js +++ /dev/null @@ -1,7 +0,0 @@ -ace.define("ace/ext/searchbox",["require","exports","module","ace/lib/dom","ace/lib/lang","ace/lib/event","ace/keyboard/hash_handler","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/lang"),s=e("../lib/event"),o='.ace_search {background-color: #ddd;color: #666;border: 1px solid #cbcbcb;border-top: 0 none;overflow: hidden;margin: 0;padding: 4px 6px 0 4px;position: absolute;top: 0;z-index: 99;white-space: normal;}.ace_search.left {border-left: 0 none;border-radius: 0px 0px 5px 0px;left: 0;}.ace_search.right {border-radius: 0px 0px 0px 5px;border-right: 0 none;right: 0;}.ace_search_form, .ace_replace_form {margin: 0 20px 4px 0;overflow: hidden;line-height: 1.9;}.ace_replace_form {margin-right: 0;}.ace_search_form.ace_nomatch {outline: 1px solid red;}.ace_search_field {border-radius: 3px 0 0 3px;background-color: white;color: black;border: 1px solid #cbcbcb;border-right: 0 none;outline: 0;padding: 0;font-size: inherit;margin: 0;line-height: inherit;padding: 0 6px;min-width: 17em;vertical-align: top;min-height: 1.8em;box-sizing: content-box;}.ace_searchbtn {border: 1px solid #cbcbcb;line-height: inherit;display: inline-block;padding: 0 6px;background: #fff;border-right: 0 none;border-left: 1px solid #dcdcdc;cursor: pointer;margin: 0;position: relative;color: #666;}.ace_searchbtn:last-child {border-radius: 0 3px 3px 0;border-right: 1px solid #cbcbcb;}.ace_searchbtn:disabled {background: none;cursor: default;}.ace_searchbtn:hover {background-color: #eef1f6;}.ace_searchbtn.prev, .ace_searchbtn.next {padding: 0px 0.7em}.ace_searchbtn.prev:after, .ace_searchbtn.next:after {content: "";border: solid 2px #888;width: 0.5em;height: 0.5em;border-width: 2px 0 0 2px;display:inline-block;transform: rotate(-45deg);}.ace_searchbtn.next:after {border-width: 0 2px 2px 0 ;}.ace_searchbtn_close {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;border-radius: 50%;border: 0 none;color: #656565;cursor: pointer;font: 16px/16px Arial;padding: 0;height: 14px;width: 14px;top: 9px;right: 7px;position: absolute;}.ace_searchbtn_close:hover {background-color: #656565;background-position: 50% 100%;color: white;}.ace_button {margin-left: 2px;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;overflow: hidden;opacity: 0.7;border: 1px solid rgba(100,100,100,0.23);padding: 1px;box-sizing: border-box!important;color: black;}.ace_button:hover {background-color: #eee;opacity:1;}.ace_button:active {background-color: #ddd;}.ace_button.checked {border-color: #3399ff;opacity:1;}.ace_search_options{margin-bottom: 3px;text-align: right;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;clear: both;}.ace_search_counter {float: left;font-family: arial;padding: 0 8px;}',u=e("../keyboard/hash_handler").HashHandler,a=e("../lib/keys"),f=999;r.importCssString(o,"ace_searchbox");var l=''.replace(/> +/g,">"),c=function(e,t,n){var i=r.createElement("div");i.innerHTML=l,this.element=i.firstChild,this.setSession=this.setSession.bind(this),this.$init(),this.setEditor(e),r.importCssString(o,"ace_searchbox",e.container)};(function(){this.setEditor=function(e){e.searchBox=this,e.renderer.scroller.appendChild(this.element),this.editor=e},this.setSession=function(e){this.searchRange=null,this.$syncOptions(!0)},this.$initElements=function(e){this.searchBox=e.querySelector(".ace_search_form"),this.replaceBox=e.querySelector(".ace_replace_form"),this.searchOption=e.querySelector("[action=searchInSelection]"),this.replaceOption=e.querySelector("[action=toggleReplace]"),this.regExpOption=e.querySelector("[action=toggleRegexpMode]"),this.caseSensitiveOption=e.querySelector("[action=toggleCaseSensitive]"),this.wholeWordOption=e.querySelector("[action=toggleWholeWords]"),this.searchInput=this.searchBox.querySelector(".ace_search_field"),this.replaceInput=this.replaceBox.querySelector(".ace_search_field"),this.searchCounter=e.querySelector(".ace_search_counter")},this.$init=function(){var e=this.element;this.$initElements(e);var t=this;s.addListener(e,"mousedown",function(e){setTimeout(function(){t.activeInput.focus()},0),s.stopPropagation(e)}),s.addListener(e,"click",function(e){var n=e.target||e.srcElement,r=n.getAttribute("action");r&&t[r]?t[r]():t.$searchBarKb.commands[r]&&t.$searchBarKb.commands[r].exec(t),s.stopPropagation(e)}),s.addCommandKeyListener(e,function(e,n,r){var i=a.keyCodeToString(r),o=t.$searchBarKb.findKeyCommand(n,i);o&&o.exec&&(o.exec(t),s.stopEvent(e))}),this.$onChange=i.delayedCall(function(){t.find(!1,!1)}),s.addListener(this.searchInput,"input",function(){t.$onChange.schedule(20)}),s.addListener(this.searchInput,"focus",function(){t.activeInput=t.searchInput,t.searchInput.value&&t.highlight()}),s.addListener(this.replaceInput,"focus",function(){t.activeInput=t.replaceInput,t.searchInput.value&&t.highlight()})},this.$closeSearchBarKb=new u([{bindKey:"Esc",name:"closeSearchBar",exec:function(e){e.searchBox.hide()}}]),this.$searchBarKb=new u,this.$searchBarKb.bindKeys({"Ctrl-f|Command-f":function(e){var t=e.isReplace=!e.isReplace;e.replaceBox.style.display=t?"":"none",e.replaceOption.checked=!1,e.$syncOptions(),e.searchInput.focus()},"Ctrl-H|Command-Option-F":function(e){if(e.editor.getReadOnly())return;e.replaceOption.checked=!0,e.$syncOptions(),e.replaceInput.focus()},"Ctrl-G|Command-G":function(e){e.findNext()},"Ctrl-Shift-G|Command-Shift-G":function(e){e.findPrev()},esc:function(e){setTimeout(function(){e.hide()})},Return:function(e){e.activeInput==e.replaceInput&&e.replace(),e.findNext()},"Shift-Return":function(e){e.activeInput==e.replaceInput&&e.replace(),e.findPrev()},"Alt-Return":function(e){e.activeInput==e.replaceInput&&e.replaceAll(),e.findAll()},Tab:function(e){(e.activeInput==e.replaceInput?e.searchInput:e.replaceInput).focus()}}),this.$searchBarKb.addCommands([{name:"toggleRegexpMode",bindKey:{win:"Alt-R|Alt-/",mac:"Ctrl-Alt-R|Ctrl-Alt-/"},exec:function(e){e.regExpOption.checked=!e.regExpOption.checked,e.$syncOptions()}},{name:"toggleCaseSensitive",bindKey:{win:"Alt-C|Alt-I",mac:"Ctrl-Alt-R|Ctrl-Alt-I"},exec:function(e){e.caseSensitiveOption.checked=!e.caseSensitiveOption.checked,e.$syncOptions()}},{name:"toggleWholeWords",bindKey:{win:"Alt-B|Alt-W",mac:"Ctrl-Alt-B|Ctrl-Alt-W"},exec:function(e){e.wholeWordOption.checked=!e.wholeWordOption.checked,e.$syncOptions()}},{name:"toggleReplace",exec:function(e){e.replaceOption.checked=!e.replaceOption.checked,e.$syncOptions()}},{name:"searchInSelection",exec:function(e){e.searchOption.checked=!e.searchRange,e.setSearchRange(e.searchOption.checked&&e.editor.getSelectionRange()),e.$syncOptions()}}]),this.setSearchRange=function(e){this.searchRange=e,e?this.searchRangeMarker=this.editor.session.addMarker(e,"ace_active-line"):this.searchRangeMarker&&(this.editor.session.removeMarker(this.searchRangeMarker),this.searchRangeMarker=null)},this.$syncOptions=function(e){r.setCssClass(this.replaceOption,"checked",this.searchRange),r.setCssClass(this.searchOption,"checked",this.searchOption.checked),this.replaceOption.textContent=this.replaceOption.checked?"-":"+",r.setCssClass(this.regExpOption,"checked",this.regExpOption.checked),r.setCssClass(this.wholeWordOption,"checked",this.wholeWordOption.checked),r.setCssClass(this.caseSensitiveOption,"checked",this.caseSensitiveOption.checked);var t=this.editor.getReadOnly();this.replaceOption.style.display=t?"none":"",this.replaceBox.style.display=this.replaceOption.checked&&!t?"":"none",this.find(!1,!1,e)},this.highlight=function(e){this.editor.session.highlight(e||this.editor.$search.$options.re),this.editor.renderer.updateBackMarkers()},this.find=function(e,t,n){var i=this.editor.find(this.searchInput.value,{skipCurrent:e,backwards:t,wrap:!0,regExp:this.regExpOption.checked,caseSensitive:this.caseSensitiveOption.checked,wholeWord:this.wholeWordOption.checked,preventScroll:n,range:this.searchRange}),s=!i&&this.searchInput.value;r.setCssClass(this.searchBox,"ace_nomatch",s),this.editor._emit("findSearchBox",{match:!s}),this.highlight(),this.updateCounter()},this.updateCounter=function(){var e=this.editor,t=e.$search.$options.re,n=0,r=0;if(t){var i=this.searchRange?e.session.getTextRange(this.searchRange):e.getValue(),s=e.session.doc.positionToIndex(e.selection.anchor);this.searchRange&&(s-=e.session.doc.positionToIndex(this.searchRange.start));var o=t.lastIndex=0,u;while(u=t.exec(i)){n++,o=u.index,o<=s&&r++;if(n>f)break;if(!u[0]){t.lastIndex=o+=1;if(o>=i.length)break}}}this.searchCounter.textContent=r+" of "+(n>f?f+"+":n)},this.findNext=function(){this.find(!0,!1)},this.findPrev=function(){this.find(!0,!0)},this.findAll=function(){var e=this.editor.findAll(this.searchInput.value,{regExp:this.regExpOption.checked,caseSensitive:this.caseSensitiveOption.checked,wholeWord:this.wholeWordOption.checked}),t=!e&&this.searchInput.value;r.setCssClass(this.searchBox,"ace_nomatch",t),this.editor._emit("findSearchBox",{match:!t}),this.highlight(),this.hide()},this.replace=function(){this.editor.getReadOnly()||this.editor.replace(this.replaceInput.value)},this.replaceAndFindNext=function(){this.editor.getReadOnly()||(this.editor.replace(this.replaceInput.value),this.findNext())},this.replaceAll=function(){this.editor.getReadOnly()||this.editor.replaceAll(this.replaceInput.value)},this.hide=function(){this.active=!1,this.setSearchRange(null),this.editor.off("changeSession",this.setSession),this.element.style.display="none",this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb),this.editor.focus()},this.show=function(e,t){this.active=!0,this.editor.on("changeSession",this.setSession),this.element.style.display="",this.replaceOption.checked=t,e&&(this.searchInput.value=e),this.searchInput.focus(),this.searchInput.select(),this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb),this.$syncOptions(!0)},this.isFocused=function(){var e=document.activeElement;return e==this.searchInput||e==this.replaceInput}}).call(c.prototype),t.SearchBox=c,t.Search=function(e,t){var n=e.searchBox||new c(e);n.show(e.session.getTextRange(),t)}}); (function() { - ace.require(["ace/ext/searchbox"], function(m) { - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = m; - } - }); - })(); diff --git a/esphome/dashboard/static/fonts/LICENSE b/esphome/dashboard/static/fonts/material-icons/LICENSE similarity index 100% rename from esphome/dashboard/static/fonts/LICENSE rename to esphome/dashboard/static/fonts/material-icons/LICENSE diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff similarity index 100% rename from esphome/dashboard/static/fonts/MaterialIcons-Regular.woff rename to esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 similarity index 100% rename from esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 rename to esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 diff --git a/esphome/dashboard/static/fonts/README.md b/esphome/dashboard/static/fonts/material-icons/README.md similarity index 100% rename from esphome/dashboard/static/fonts/README.md rename to esphome/dashboard/static/fonts/material-icons/README.md diff --git a/esphome/dashboard/static/fonts/material-icons.css b/esphome/dashboard/static/fonts/material-icons/material-icons.css similarity index 100% rename from esphome/dashboard/static/fonts/material-icons.css rename to esphome/dashboard/static/fonts/material-icons/material-icons.css diff --git a/esphome/dashboard/static/favicon.ico b/esphome/dashboard/static/images/favicon.ico similarity index 100% rename from esphome/dashboard/static/favicon.ico rename to esphome/dashboard/static/images/favicon.ico diff --git a/esphome/dashboard/static/jquery-ui.min.js b/esphome/dashboard/static/jquery-ui.min.js deleted file mode 100644 index a4c69733c0..0000000000 --- a/esphome/dashboard/static/jquery-ui.min.js +++ /dev/null @@ -1,401 +0,0 @@ -/*! - * jQuery UI 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.5",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106, -NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this, -"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position"); -if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"));if(!isNaN(b)&&b!=0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f, -"border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c.style(this,h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c.style(this, -h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}}); -c(function(){var a=document.createElement("div"),b=document.body;c.extend(a.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.appendChild(a).offsetHeight===100;b.removeChild(a).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); -(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== -"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= -this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;return true},_mouseStart:function(a){var b=this.options;this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top- -this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions(); -d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);return true},_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis|| -this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if(!this.element[0]||!this.element[0].parentNode)return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element, -b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this== -a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone():this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]|| -0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0], -this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top- -(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment== -"parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&& -a.containment.constructor!=Array){var b=d(a.containment)[0];if(b){a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"), -10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0], -this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft(): -f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.leftthis.containment[2])e=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?e:!(e-this.offset.click.left').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")})},stop:function(){d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options; -if(a.css("opacity"))b._opacity=a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!= -"HTML"){if(!c.axis||c.axis!="x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY=0;h--){var i=c.snapElements[h].left,k=i+c.snapElements[h].width,j=c.snapElements[h].top,l=j+c.snapElements[h].height;if(i-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= -i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), -top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= -this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", -nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== -String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); -this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()}; -if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(), -d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset= -this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio: -this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize", -b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height; -f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop", -b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top= -a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidthb.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height, -k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+ -a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this, -arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable, -{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize, -function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n= -(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition= -false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left- -a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize", -b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top", -"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset, -f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left= -a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+ -a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&& -e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative", -height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width= -d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery); -(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), -selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, -c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", -c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= -this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){this.containerCache={};this.element.addClass("ui-sortable"); -this.refresh();this.floating=this.items.length?/left|right/.test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a==="disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this, -arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem= -c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset, -{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment(); -if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start", -a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute"); -if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a, -c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]== -document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp();this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate", -null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem): -d(this.domPosition.parent).prepend(this.currentItem);return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute|| -"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")}, -_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!= -this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a= -this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable"); -if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h= -0;b--){var c=this.items[b],e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width= -this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f}, -update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b= -null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this)); -this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])? -g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive", -g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over= -0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",a,this._uiHash());for(e=0;e").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); -c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| -typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c== -"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c, -a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/= -e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+ -b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/ -2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); -(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); -(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100* -f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); -(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& -this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); -(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); -b.dequeue()})})}})(jQuery); -(function(c){c.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); -a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); -if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var f=d.closest(".ui-accordion-header");a.active=f.length?f:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion",function(g){return a._keydown(g)}).next().attr("role", -"tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(g){a._clickHandler.call(a,g,this);g.preventDefault()})},_createIcons:function(){var a=this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers); -this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("tabIndex"); -this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); -b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,f=this.headers.index(a.target),g=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:g=this.headers[(f+1)%d];break;case b.LEFT:case b.UP:g=this.headers[(f-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); -a.preventDefault()}if(g){c(a.target).attr("tabIndex",-1);c(g).attr("tabIndex",0);g.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ -c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; -if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected); -a.next().addClass("ui-accordion-content-active")}h=a.next();f=this.active.next();g={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):h,oldContent:f};d=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(h,f,g,b,d)}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); -this.active.next().addClass("ui-accordion-content-active");var f=this.active.next(),g={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:f},h=this.active=c([]);this._toggle(h,f,g)}},_toggle:function(a,b,d,f,g){var h=this,e=h.options;h.toShow=a;h.toHide=b;h.data=d;var j=function(){if(h)return h._completed.apply(h,arguments)};h._trigger("changestart",null,h.data);h.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&f?{toShow:c([]),toHide:b,complete:j, -down:g,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:g,autoHeight:e.autoHeight||e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;f=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!f[k]&&!c.easing[k])k="slide";f[k]||(f[k]=function(l){this.slide(l,{easing:k,duration:i||700})}); -f[k](d)}else{if(e.collapsible&&f)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.5",animations:{slide:function(a, -b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),f=0,g={},h={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){h[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);g[i]={value:j[1], -unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(h,{step:function(j,i){if(i.prop=="height")f=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=f*g[i.prop].value+g[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",paddingTop:"hide", -paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); -(function(e){e.widget("ui.autocomplete",{options:{appendTo:"body",delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},_create:function(){var a=this,b=this.element[0].ownerDocument;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!a.options.disabled){var d=e.ui.keyCode;switch(c.keyCode){case d.PAGE_UP:a._move("previousPage", -c);break;case d.PAGE_DOWN:a._move("nextPage",c);break;case d.UP:a._move("previous",c);c.preventDefault();break;case d.DOWN:a._move("next",c);c.preventDefault();break;case d.ENTER:case d.NUMPAD_ENTER:a.menu.element.is(":visible")&&c.preventDefault();case d.TAB:if(!a.menu.active)return;a.menu.select(c);break;case d.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay); -break}}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};this.menu=e("
    ").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0]; -c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus(); -a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"); -this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source= -function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(e("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")}, -filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery); -(function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", --1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})}, -deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0); -a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active|| -this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))}, -hasScroll:function(){return this.element.height()
    ").addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary;if(d.primary||d.secondary){b.addClass("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary"));d.primary&&b.prepend("");d.secondary&&b.append("");if(!this.options.text){b.addClass(e?"ui-button-icons-only":"ui-button-icon-only").removeClass("ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary"); -this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()}, -destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery); -(function(d,G){function L(){this.debug=false;this._curInst=null;this._keyEvent=false;this._disabledInputs=[];this._inDialog=this._datepickerShowing=false;this._mainDivId="ui-datepicker-div";this._inlineClass="ui-datepicker-inline";this._appendClass="ui-datepicker-append";this._triggerClass="ui-datepicker-trigger";this._dialogClass="ui-datepicker-dialog";this._disableClass="ui-datepicker-disabled";this._unselectableClass="ui-datepicker-unselectable";this._currentClass="ui-datepicker-current-day";this._dayOverClass= -"ui-datepicker-days-cell-over";this.regional=[];this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su", -"Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:false,showMonthAfterYear:false,yearSuffix:""};this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:false,hideIfNoPrevNext:false,navigationAsDateFormat:false,gotoCurrent:false,changeMonth:false,changeYear:false,yearRange:"c-10:c+10",showOtherMonths:false,selectOtherMonths:false,showWeek:false,calculateWeek:this.iso8601Week,shortYearCutoff:"+10", -minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:true,showButtonPanel:false,autoSize:false};d.extend(this._defaults,this.regional[""]);this.dpDiv=d('
    ')}function E(a,b){d.extend(a, -b);for(var c in b)if(b[c]==null||b[c]==G)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.5"}});var y=(new Date).getTime();d.extend(L.prototype,{markerClassName:"hasDatepicker",log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){E(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]= -f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_])/g,"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:d('
    ')}}, -_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&& -b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c=="focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f== -""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker():d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a, -c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b), -true);this._updateDatepicker(b);this._updateAlternate(b)}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+=1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}E(a.settings,e||{});b=b&&b.constructor== -Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]); -d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}}, -_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().removeClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b= -d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().addClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false; -for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target|| -a;if(a.nodeName.toLowerCase()!="input")a=d("input",a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);d.datepicker._curInst&&d.datepicker._curInst!=b&&d.datepicker._curInst.dpDiv.stop(true,true);var c=d.datepicker._get(b,"beforeShow");E(b.settings,c?c.apply(a,[a,b]):{});b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value="";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a); -d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b);c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&& -d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){d.datepicker._datepickerShowing=true;var i=d.datepicker._getBorders(b.dpDiv);b.dpDiv.find("iframe.ui-datepicker-cover").css({left:-i[0],top:-i[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})};b.dpDiv.zIndex(d(a).zIndex()+1);d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f, -h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}},_updateDatepicker:function(a){var b=this,c=d.datepicker._getBorders(a.dpDiv);a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function(){d(this).removeClass("ui-state-hover"); -this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).removeClass("ui-datepicker-prev-hover");this.className.indexOf("ui-datepicker-next")!=-1&&d(this).removeClass("ui-datepicker-next-hover")}).bind("mouseover",function(){if(!b._isDisabledDatepicker(a.inline?a.dpDiv.parent()[0]:a.input[0])){d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");d(this).addClass("ui-state-hover");this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).addClass("ui-datepicker-prev-hover"); -this.className.indexOf("ui-datepicker-next")!=-1&&d(this).addClass("ui-datepicker-next-hover")}}).end().find("."+this._dayOverClass+" a").trigger("mouseover").end();c=this._getNumberOfMonths(a);var e=c[1];e>1?a.dpDiv.addClass("ui-datepicker-multi-"+e).css("width",17*e+"em"):a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");a.dpDiv[(c[0]!=1||c[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"); -a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input.focus()},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(),h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(), -k=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>k&&k>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b=this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1);)a=a[b?"previousSibling":"nextSibling"]; -a=d(a).offset();return[a.left,a.top]},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b);this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();if(a=this._get(b,"onClose"))a.apply(b.input?b.input[0]:null,[b.input?b.input.val(): -"",b]);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&& -!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth; -b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e._selectingMonthYear=false;e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_clickMonthYear:function(a){var b= -this._getInst(d(a)[0]);b.input&&b._selectingMonthYear&&setTimeout(function(){b.input.focus()},0);b._selectingMonthYear=!b._selectingMonthYear},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a= -d(a);this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a, -"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b== -"object"?b.toString():b+"";if(b=="")return null;for(var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff,f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,k=c=-1,l=-1,u=-1,j=false,o=function(p){(p=z+1 --1){k=1;l=u;do{e=this._getDaysInMonth(c,k-1);if(l<=e)break;k++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,k-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=k||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24* -60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames:null)||this._defaults.monthNames;var i=function(o){(o=j+112?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e? -"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),k= -this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay?new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),j=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=j&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a, -"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-k,1)),this._getFormatConfig(a));n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+ -n+"";var r=this._get(a,"nextText");r=!h?r:this.formatDate(r,this._daylightSavingAdjust(new Date(m,g+k,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+r+"":f?"":''+r+"";k=this._get(a,"currentText");r=this._get(a,"gotoCurrent")&&a.currentDay?u:b;k=!h?k:this.formatDate(k,r,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
    '+(c?h:"")+(this._isInRange(a,r)?'":"")+(c?"":h)+"
    ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;k=this._get(a,"showWeek");r=this._get(a,"dayNames");this._get(a,"dayNamesShort");var s=this._get(a,"dayNamesMin"),z=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),w=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var M=this._getDefaultDate(a),I="",C=0;C1)switch(D){case 0:x+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:x+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:x+=" ui-datepicker-group-middle";t="";break}x+='">'}x+='
    '+(/all|left/.test(t)&&C==0?c? -f:n:"")+(/all|right/.test(t)&&C==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,j,o,C>0||D>0,z,v)+'
    ';var A=k?'":"";for(t=0;t<7;t++){var q=(t+h)%7;A+="=5?' class="ui-datepicker-week-end"':"")+'>'+s[q]+""}x+=A+"";A=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, -A);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;A=l?6:Math.ceil((t+A)/7);q=this._daylightSavingAdjust(new Date(m,g,1-t));for(var O=0;O";var P=!k?"":'";for(t=0;t<7;t++){var F=p?p.apply(a.input?a.input[0]:null,[q]):[true,""],B=q.getMonth()!=g,K=B&&!H||!F[0]||j&&qo;P+='";q.setDate(q.getDate()+1);q=this._daylightSavingAdjust(q)}x+=P+""}g++;if(g>11){g=0;m++}x+="
    '+this._get(a,"weekHeader")+"
    '+this._get(a,"calculateWeek")(q)+""+(B&&!w?" ":K?''+q.getDate()+ -"":''+q.getDate()+"")+"
    "+(l?""+(i[0]>0&&D==i[1]-1?'
    ':""):"");N+=x}I+=N}I+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': -"");a._keyEvent=false;return I},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var k=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),j='
    ',o="";if(h||!k)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(j+=o+(h||!(k&&l)?" ":""));if(h||!l)j+=''+c+"";else{g=this._get(a,"yearRange").split(":");var r=(new Date).getFullYear();i=function(s){s=s.match(/c[+-].*/)?c+parseInt(s.substring(1),10):s.match(/[+-].*/)?r+parseInt(s,10):parseInt(s,10);return isNaN(s)?r:s};b=i(g[0]);g=Math.max(b, -i(g[1]||""));b=e?Math.max(b,e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(j+='"}j+=this._get(a,"yearSuffix");if(u)j+=(h||!(k&&l)?" ":"")+o;j+="
    ";return j},_adjustInstDate:function(a,b,c){var e= -a.drawYear+(c=="Y"?b:0),f=a.drawMonth+(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a, -"onChangeMonthYear");if(b)b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a); -c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a, -"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker= -function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); -return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.5";window["DP_jQuery_"+y]=d})(jQuery); -(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title"); -if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
    ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog", -"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("
    ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i); -return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&& -g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog"); -b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ= -f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show); -a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false, -f=c("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
    ").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position, -offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g); -b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(), -handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight, -a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a); -f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b? -d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&& -d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight- -b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","), -function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a, -b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0]; -b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d= -c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); -(function(b,c){b.widget("ui.progressbar",{options:{value:0},min:0,max:100,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.max,"aria-valuenow":this._value()});this.valueDiv=b("
    ").appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); -this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===c)return this._value();this._setOption("value",a);return this},_setOption:function(a,d){if(a==="value"){this.options.value=d;this._refreshValue();this._trigger("change")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.max,Math.max(this.min,a))},_refreshValue:function(){var a=this.value();this.valueDiv.toggleClass("ui-corner-right", -a===this.max).width(a+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.5"})})(jQuery); -(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled"); -this.range=d([]);if(b.range){if(b.range===true){this.range=d("
    ");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("
    ");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("").appendTo(this.element).addClass("ui-slider-handle"); -if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur(); -else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e= -false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h=== -a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); -this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a, -g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= -this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= -this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); -c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;fthis._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= -this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f- -g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"}, -b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.5"})})(jQuery); -(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
    ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(true)},_setOption:function(a,e){if(a=="selected")this.options.collapsible&& -e==this.options.selected||this.select(e);else{this.options[a]=e;this._tabify()}},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var a=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[a].concat(d.makeArray(arguments)))},_ui:function(a,e){return{tab:a,panel:e,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var a= -d(this);a.html(a.data("label.tabs")).removeData("label.tabs")})},_tabify:function(a){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var b=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| -(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))b.panels=b.panels.add(b._sanitizeSelector(i));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=b._tabId(f);f.href="#"+i;f=d("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(b.panels[g-1]||b.list);f.data("destroy.tabs",true)}b.panels=b.panels.add(f)}else c.disabled.push(g)});if(a){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); -this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(b._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= -this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return b.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); -if(c.selected>=0&&this.anchors.length){this.panels.eq(c.selected).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");b.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[c.selected],b.panels[c.selected]))});this.load(c.selected)}d(window).bind("unload",function(){b.lis.add(b.anchors).unbind(".tabs");b.lis=b.anchors=b.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));this.element[c.collapsible?"addClass": -"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);a=0;for(var j;j=this.lis[a];a++)d(j)[d.inArray(a,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+g)};this.lis.bind("mouseover.tabs", -function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",function(){e(f,o);b._trigger("show", -null,b._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");b._trigger("show",null,b._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);b.element.dequeue("tabs")})}:function(g,f){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");b.element.dequeue("tabs")};this.anchors.bind(c.event+".tabs", -function(){var g=this,f=d(g).closest("li"),i=b.panels.filter(":not(.ui-tabs-hide)"),l=d(b._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||b.panels.filter(":animated").length||b._trigger("select",null,b._ui(this,l[0]))===false){this.blur();return false}c.selected=b.anchors.index(this);b.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=-1;c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs", -function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this));this.blur();return false}c.cookie&&b._cookie(c.selected,c.cookie);if(l.length){i.length&&b.element.queue("tabs",function(){s(g,i)});b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";d.browser.msie&&this.blur()});this.anchors.bind("click.tabs", -function(){return false})},_getIndex:function(a){if(typeof a=="string")a=this.anchors.index(this.anchors.filter("[href$="+a+"]"));return a},destroy:function(){var a=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=d.data(this,"href.tabs");if(e)this.href= -e;var b=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){b.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});a.cookie&&this._cookie(null,a.cookie);return this},add:function(a,e,b){if(b===p)b=this.anchors.length; -var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,a).replace(/#\{label\}/g,e));a=!a.indexOf("#")?a.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=d("#"+a);j.length||(j=d(h.panelTemplate).attr("id",a).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(b>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[b]); -j.insertBefore(this.panels[b])}h.disabled=d.map(h.disabled,function(k){return k>=b?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[b],this.panels[b]));return this},remove:function(a){a=this._getIndex(a);var e=this.options,b=this.lis.eq(a).remove(),c=this.panels.eq(a).remove(); -if(b.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(a+(a+1=a?--h:h});this._tabify();this._trigger("remove",null,this._ui(b.find("a")[0],c[0]));return this},enable:function(a){a=this._getIndex(a);var e=this.options;if(d.inArray(a,e.disabled)!=-1){this.lis.eq(a).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(b){return b!=a});this._trigger("enable",null, -this._ui(this.anchors[a],this.panels[a]));return this}},disable:function(a){a=this._getIndex(a);var e=this.options;if(a!=e.selected){this.lis.eq(a).addClass("ui-state-disabled");e.disabled.push(a);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))}return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this}, -load:function(a){a=this._getIndex(a);var e=this,b=this.options,c=this.anchors.eq(a)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(a).addClass("ui-state-processing");if(b.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(b.spinner)}this.xhr=d.ajax(d.extend({},b.ajaxOptions,{url:h,success:function(k,n){d(e._sanitizeSelector(c.hash)).html(k);e._cleanup();b.cache&&d.data(c,"cache.tabs", -true);e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.error(k,n,a,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},url:function(a, -e){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.5"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(a,e){var b=this,c=this.options,h=b._rotate||(b._rotate=function(j){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var k=c.selected;b.select(++ka?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; -}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("