diff --git a/CHANGELOG.md b/CHANGELOG.md index fa13ff1..5cfef16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Made the codebase compatible with ESPHome v1.x.0 ([PR #1657: Introduce new async-def coroutine syntax](https://github.com/esphome/esphome/pull/1657)) +### Added +- Implemented support for visual feedback during the OTA flashing process in the + `example.yaml` file: the light becomes blue during the process, the brightness bar + represents the update progress, when updating fails the light flashes red and when it + completes successfuly, the light flashes green. + ## [1.0.0] **Note**: This release requires ESPHome v1.18.0 or newer. diff --git a/components/ota_triggers/__init__.py b/components/ota_triggers/__init__.py new file mode 100644 index 0000000..e2880ca --- /dev/null +++ b/components/ota_triggers/__init__.py @@ -0,0 +1,70 @@ +from esphome.cpp_generator import RawExpression +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.ota import ota_ns, OTAComponent +from esphome.const import ( + CONF_ID, +) + +CODEOWNERS = ["@mmakaay"] +DEPENDENCIES = ["ota"] + +CONF_ON_BEGIN = "on_begin" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_END = "on_end" +CONF_ON_ERROR = "on_error" +CONF_ON_BEGIN_TRIGGER_ID = "on_begin_trigger_id" +CONF_ON_PROGRESS_TRIGGER_ID = "on_progress_trigger_id" +CONF_ON_END_TRIGGER_ID = "on_end_trigger_id" +CONF_ON_ERROR_TRIGGER_ID = "on_error_trigger_id" + +OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template()) +OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template()) +OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) +OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(OTAComponent), + cv.Optional(CONF_ON_BEGIN): automation.validate_automation( + { + cv.GenerateID(CONF_ON_BEGIN_TRIGGER_ID): cv.declare_id(OTAStartTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_ON_ERROR_TRIGGER_ID): cv.declare_id(OTAErrorTrigger), + } + ), + cv.Optional(CONF_ON_PROGRESS): automation.validate_automation( + { + cv.GenerateID(CONF_ON_PROGRESS_TRIGGER_ID): cv.declare_id( + OTAProgressTrigger + ), + } + ), + cv.Optional(CONF_ON_END): automation.validate_automation( + { + cv.GenerateID(CONF_ON_END_TRIGGER_ID): cv.declare_id(OTAEndTrigger), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = await cg.get_variable(config[CONF_ID]) + + for conf in config.get(CONF_ON_BEGIN, []): + trigger = cg.new_Pvariable(conf[CONF_ON_BEGIN_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PROGRESS, []): + trigger = cg.new_Pvariable(conf[CONF_ON_PROGRESS_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_END, []): + trigger = cg.new_Pvariable(conf[CONF_ON_END_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_ON_ERROR_TRIGGER_ID], var) + await automation.build_automation(trigger, [(int, "x")], conf) diff --git a/components/ota_triggers/automation.h b/components/ota_triggers/automation.h new file mode 100644 index 0000000..3277658 --- /dev/null +++ b/components/ota_triggers/automation.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/ota/ota_component.h" + +namespace esphome { +namespace ota { + +class OTAStartTrigger : public Trigger<> { + public: + explicit OTAStartTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTAState::Started && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class OTAProgressTrigger : public Trigger { + public: + explicit OTAProgressTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTAState::InProgress && !parent->is_failed()) { + trigger(progress); + } + }); + } +}; + +class OTAEndTrigger : public Trigger<> { + public: + explicit OTAEndTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTAState::Completed && !parent->is_failed()) { + trigger(); + } + }); + } +}; + + +class OTAErrorTrigger : public Trigger { + public: + explicit OTAErrorTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTAState::Error && !parent->is_failed()) { + trigger(error); + } + }); + } +}; + +} // namespace ota +} // namespace esphome diff --git a/example.yaml b/example.yaml index 7f5cf14..872eaa2 100644 --- a/example.yaml +++ b/example.yaml @@ -200,3 +200,44 @@ sensor: id: ${id_light} brightness: !lambda return x; +# These OTA triggers are used to provide some visual feedback during the OTA +# flashing process. The light is turned blue when the upgrade starts, the +# brightness indicator will represent the update progress (fills up from 0% +# to 100%), the light will flash red when the upgrade fails or green when the +# upgrade succeeds. +# You can safely remove this section if you don't want the visual feedback. +ota_triggers: + on_begin: + then: + - light.disco_on: + id: ${id_light} + red: 0% + green: 0% + blue: 100% + brightness: 2% + transition_length: 0s + on_progress: + then: + - output.set_level: + id: ${id_front_panel_illumination} + level: !lambda return (x / 100.0f); + on_end: + then: + - light.disco_on: + id: ${id_light} + red: 0% + green: 100% + blue: 0% + brightness: 2% + transition_length: 0s + on_error: + then: + - light.disco_on: + id: ${id_light} + red: 100% + green: 0% + blue: 0% + brightness: 2% + - delay: 1s + - light.disco_off: + id: ${id_light}