diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp index 6b70c5d4fd..3b9a9a11cc 100644 --- a/esphome/components/hm3301/aqi_calculator.cpp +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -33,7 +33,7 @@ class AQICalculator : public AbstractAQICalculator { } int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - for (int i = 0; i < AMOUNT_OF_LEVELS - 1; i++) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { if (value >= array[i][0] && value <= array[i][1]) { return i; } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index b240f84e8f..d1fb2a0bcb 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -102,17 +102,18 @@ 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(); + // using the remote value because of transitions we need to trigger as early as possible + auto is_on = a_light->remote_values.is_on(); // only trigger when going from off to on - auto should_trigger = is_on && !last_on_; + auto should_trigger = is_on && !this->last_on_; // Set new state immediately so that trigger() doesn't devolve // into infinite loop - last_on_ = is_on; + this->last_on_ = is_on; if (should_trigger) { this->trigger(); } }); - last_on_ = a_light->current_values.is_on(); + this->last_on_ = a_light->current_values.is_on(); } protected: @@ -122,22 +123,14 @@ class LightTurnOnTrigger : public Trigger<> { class LightTurnOffTrigger : public Trigger<> { public: LightTurnOffTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this, a_light]() { + a_light->add_new_target_state_reached_callback([this, a_light]() { auto is_on = a_light->current_values.is_on(); // only trigger when going from on to off - auto should_trigger = !is_on && last_on_; - // Set new state immediately so that trigger() doesn't devolve - // into infinite loop - last_on_ = is_on; - if (should_trigger) { + if (!is_on) { this->trigger(); } }); - last_on_ = a_light->current_values.is_on(); } - - protected: - bool last_on_; }; template class AddressableSet : public Action { diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index d34bc88f53..dd78ced650 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -145,6 +145,7 @@ void LightState::loop() { if (this->transformer_ != nullptr) { if (this->transformer_->is_finished()) { this->remote_values = this->current_values = this->transformer_->get_end_values(); + this->target_state_reached_callback_.call(); if (this->transformer_->publish_at_end()) this->publish_state(); this->transformer_ = nullptr; @@ -336,6 +337,9 @@ void LightCall::perform() { this->parent_->set_immediately_(v, this->publish_); } + if (!this->has_transition_()) { + this->parent_->target_state_reached_callback_.call(); + } if (this->publish_) { this->parent_->publish_state(); } @@ -395,13 +399,13 @@ LightColorValues LightCall::validate_() { // 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); - } - // make white values binary aka 0.0f or 1.0f...this allows brightness to do its job if (traits.get_supports_color_interlock()) { + 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); + } + // make white values binary aka 0.0f or 1.0f...this allows brightness to do its job if (*this->white_ > 0.0f) { this->white_ = optional(1.0f); } else { @@ -411,11 +415,13 @@ LightColorValues LightCall::validate_() { } // White to 0% if (exclusively) setting any RGB value that isn't 255,255,255 else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f && traits.get_supports_rgb_white_value() && - traits.get_supports_color_interlock()) { - this->white_ = optional(1.0f); - } else if (!this->white_.has_value() || !traits.get_supports_rgb_white_value()) { - this->white_ = optional(0.0f); + if (traits.get_supports_color_interlock()) { + if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f && + traits.get_supports_rgb_white_value() && traits.get_supports_color_interlock()) { + this->white_ = optional(1.0f); + } else if (!this->white_.has_value() || !traits.get_supports_rgb_white_value()) { + this->white_ = optional(0.0f); + } } } // if changing Kelvin alone, change to white light @@ -752,6 +758,10 @@ void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bo void LightState::add_new_remote_values_callback(std::function &&send_callback) { this->remote_values_callback_.add(std::move(send_callback)); } +void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { + this->target_state_reached_callback_.add(std::move(send_callback)); +} + LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) return nullptr; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index e48cf9f864..e761e576e3 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -242,6 +242,13 @@ class LightState : public Nameable, public Component { */ void add_new_remote_values_callback(std::function &&send_callback); + /** + * The callback is called once the state of current_values and remote_values are equal + * + * @param send_callback + */ + void add_new_target_state_reached_callback(std::function &&send_callback); + /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -318,6 +325,12 @@ class LightState : public Nameable, public Component { * starting with the beginning of the transition. */ CallbackManager remote_values_callback_{}; + + /** Callback to call when the state of current_values and remote_values are equal + * This should be called once the state of current_values changed and equals the state of remote_values + */ + CallbackManager target_state_reached_callback_{}; + LightOutput *output_; ///< Store the output to allow effects to have more access. /// Whether the light value should be written in the next cycle. bool next_write_{true}; diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index ab9be4ad36..111c17223d 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -229,7 +229,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { b = pixels[col]; } else if (this->orientation_ == 2) { for (uint8_t i = 0; i < 8; i++) { - b |= ((pixels[i] >> (7 - col)) << (7 - i)); + b |= ((pixels[i] >> (7 - col)) & 1) << i; } } else { b = pixels[7 - col]; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 2ca27bf51a..202620c06d 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -12,6 +12,75 @@ namespace xiaomi_ble { static const char *TAG = "xiaomi_ble"; +bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { + // motion detection, 1 byte, 8-bit unsigned integer + if ((value_type == 0x03) && (value_length == 1)) { + result.has_motion = (data[0]) ? true : false; + } + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + else if ((value_type == 0x04) && (value_length == 2)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; + } + // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + else if ((value_type == 0x06) && (value_length == 2)) { + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; + } + // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx + else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + result.is_light = (illuminance == 100) ? true : false; + if (value_type == 0x0F) + result.has_motion = true; + } + // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x08) && (value_length == 1)) { + result.moisture = data[0]; + } + // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + else if ((value_type == 0x09) && (value_length == 2)) { + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + } + // battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x0A) && (value_length == 1)) { + result.battery_level = data[0]; + } + // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + else if ((value_type == 0x0D) && (value_length == 4)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + } + // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 + else if ((value_type == 0x10) && (value_length == 2)) { + const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.formaldehyde = formaldehyde / 100.0f; + } + // on/off state, 1 byte, 8-bit unsigned integer + else if ((value_type == 0x12) && (value_length == 1)) { + result.is_active = (data[0]) ? true : false; + } + // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x13) && (value_length == 1)) { + result.tablet = data[0]; + } + // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min + else if ((value_type == 0x17) && (value_length == 4)) { + const uint32_t idle_time = + uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); + result.idle_time = idle_time / 60.0f; + result.has_motion = (idle_time) ? false : true; + } else { + return false; + } + + return true; +} + bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status if (result.has_encryption) { @@ -25,81 +94,39 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult // Byte 2: length // Byte 3..3+len-1: data point value - const uint8_t *raw = message.data() + result.raw_offset; - const uint8_t *data = raw + 3; - const uint8_t data_length = raw[2]; + const uint8_t *payload = message.data() + result.raw_offset; + uint8_t payload_length = message.size() - result.raw_offset; + uint8_t payload_offset = 0; + bool success = false; - if ((data_length < 1) || (data_length > 4)) { - ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", data_length); + if (payload_length < 4) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", payload_length); return false; } - // motion detection, 1 byte, 8-bit unsigned integer - if ((raw[0] == 0x03) && (data_length == 1)) { - result.has_motion = (data[0]) ? true : false; - } - // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - else if ((raw[0] == 0x04) && (data_length == 2)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.temperature = temperature / 10.0f; - } - // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - else if ((raw[0] == 0x06) && (data_length == 2)) { - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.humidity = humidity / 10.0f; - } - // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx - else if (((raw[0] == 0x07) || (raw[0] == 0x0F)) && (data_length == 3)) { - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); - result.illuminance = illuminance; - result.is_light = (illuminance == 100) ? true : false; - if (raw[0] == 0x0F) - result.has_motion = true; - } - // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - else if ((raw[0] == 0x08) && (data_length == 1)) { - result.moisture = data[0]; - } - // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - else if ((raw[0] == 0x09) && (data_length == 2)) { - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.conductivity = conductivity; - } - // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((raw[0] == 0x0A) && (data_length == 1)) { - result.battery_level = data[0]; - } - // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - else if ((raw[0] == 0x0D) && (data_length == 4)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); - result.temperature = temperature / 10.0f; - result.humidity = humidity / 10.0f; - } - // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 - else if ((raw[0] == 0x10) && (data_length == 2)) { - const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.formaldehyde = formaldehyde / 100.0f; - } - // on/off state, 1 byte, 8-bit unsigned integer - else if ((raw[0] == 0x12) && (data_length == 1)) { - result.is_active = (data[0]) ? true : false; - } - // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % - else if ((raw[0] == 0x13) && (data_length == 1)) { - result.tablet = data[0]; - } - // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min - else if ((raw[0] == 0x17) && (data_length == 4)) { - const uint32_t idle_time = - uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); - result.idle_time = idle_time / 60.0f; - result.has_motion = (idle_time) ? false : true; - } else { - return false; + while (payload_length > 0) { + if (payload[payload_offset + 1] != 0x10) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); + break; + } + + const uint8_t value_length = payload[payload_offset + 2]; + if ((value_length < 1) || (value_length > 4) || (payload_length < (3 + value_length))) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): value has wrong size (%d)!", value_length); + break; + } + + const uint8_t value_type = payload[payload_offset + 0]; + const uint8_t *data = &payload[payload_offset + 3]; + + if (parse_xiaomi_value(value_type, data, value_length, result)) + success = true; + + payload_length -= 3 + value_length; + payload_offset += 3 + value_length; } - return true; + return success; } optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index daa71787a5..ad73226159 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -57,6 +57,7 @@ struct XiaomiAESVector { size_t ivsize; }; +bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1e54c04fe5..371ae0eea1 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -11,10 +11,11 @@ from string import ascii_letters, digits import voluptuous as vol from esphome import core -from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ - CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ - CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE +from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, \ + CONF_DISCOVERY, CONF_ID, CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, \ + CONF_PAYLOAD_NOT_AVAILABLE, CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ + CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, \ + CONF_TYPE, CONF_PACKAGES from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes from esphome.helpers import list_starts_with, add_class_to_obj @@ -1167,9 +1168,12 @@ class OnlyWith(Optional): @property def default(self): # pylint: disable=unsupported-membership-test - if self._component not in CORE.raw_config: - return vol.UNDEFINED - return self._default + if (self._component in CORE.raw_config or + (CONF_PACKAGES in CORE.raw_config and + self._component in + {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()})): + return self._default + return vol.UNDEFINED @default.setter def default(self, value): diff --git a/esphome/const.py b/esphome/const.py index b0ebaca0ef..abe559089e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -2,7 +2,7 @@ MAJOR_VERSION = 1 MINOR_VERSION = 15 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' __version__ = f'{__short_version__}.{PATCH_VERSION}' diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 78a62a5e86..38b80d85fb 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -178,8 +178,8 @@ void delay_microseconds_accurate(uint32_t usec) { if (usec <= 16383UL) { delayMicroseconds(usec); } else { - delay(usec / 16383UL); - delayMicroseconds(usec % 16383UL); + delay(usec / 1000UL); + delayMicroseconds(usec % 1000UL); } } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e61b2b13c6..9be39f4938 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -8,6 +8,7 @@ namespace esphome { static const char *TAG = "scheduler"; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER @@ -107,6 +108,26 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { } #endif // ESPHOME_DEBUG_SCHEDULER + auto to_remove_was = to_remove_; + auto items_was = items_.size(); + // If we have too many items to remove + if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + std::vector> valid_items; + while (!this->empty_()) { + auto item = std::move(this->items_[0]); + this->pop_raw_(); + valid_items.push_back(std::move(item)); + } + this->items_ = std::move(valid_items); + + // The following should not happen unless I'm missing something + if (to_remove_ != 0) { + ESP_LOGW(TAG, "to_remove_ was %u now: %u items where %zu now %zu. Please report this", to_remove_was, to_remove_, + items_was, items_.size()); + to_remove_ = 0; + } + } + while (!this->empty_()) { // use scoping to indicate visibility of `item` variable { @@ -147,6 +168,7 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { if (item->remove) { // We were removed/cancelled in the function call, stop + to_remove_--; continue; } @@ -182,6 +204,7 @@ void HOT Scheduler::cleanup_() { if (!item->remove) return; + to_remove_--; this->pop_raw_(); } } @@ -193,7 +216,8 @@ void HOT Scheduler::push_(std::unique_ptr item) { this bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { bool ret = false; for (auto &it : this->items_) - if (it->component == component && it->name == name && it->type == type) { + if (it->component == component && it->name == name && it->type == type && !it->remove) { + to_remove_++; it->remove = true; ret = true; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 5688058a1e..d1839cb4a7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -61,6 +61,7 @@ class Scheduler { std::vector> to_add_; uint32_t last_millis_{0}; uint8_t millis_major_{0}; + uint32_t to_remove_{0}; }; } // namespace esphome