diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 433e5d2792..b3fa6d4932 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,56 +1,63 @@ { "name": "ESPHome Dev", - "image": "esphome/esphome-lint:dev", + "image": "ghcr.io/esphome/esphome-lint:dev", "postCreateCommand": [ "script/devcontainer-post-create" ], + "containerEnv": { + "DEVCONTAINER": "1" + }, "runArgs": [ "--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1" ], "appPort": 6052, - "extensions": [ - // python - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - // yaml - "redhat.vscode-yaml", - // cpp - "ms-vscode.cpptools", - // editorconfig - "editorconfig.editorconfig", - ], - "settings": { - "python.languageServer": "Pylance", - "python.pythonPath": "/usr/bin/python3", - "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.defaultProfile.linux": "bash", - "yaml.customTags": [ - "!secret scalar", - "!lambda scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ], - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": { - "when": "$(basename).py" - }, - "**/__pycache__": true - }, - "files.associations": { - "**/.vscode/*.json": "jsonc" - }, - "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", + "customizations": { + "vscode": { + "extensions": [ + // python + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + // yaml + "redhat.vscode-yaml", + // cpp + "ms-vscode.cpptools", + // editorconfig + "editorconfig.editorconfig", + ], + "settings": { + "python.languageServer": "Pylance", + "python.pythonPath": "/usr/bin/python3", + "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.defaultProfile.linux": "bash", + "yaml.customTags": [ + "!secret scalar", + "!lambda scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true + }, + "files.associations": { + "**/.vscode/*.json": "jsonc" + }, + "C_Cpp.clang_format_path": "/usr/bin/clang-format-13" + } + } } } diff --git a/.editorconfig b/.editorconfig index 8ccf1eeebc..9e203f60e4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,10 +25,9 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 -quote_type = single +quote_type = double # JSON [*.json] indent_style = space indent_size = 2 - diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 864586fe6b..a8ca63d158 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ +--- # These are supported funding model platforms custom: https://www.nabucasa.com diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7f99701e39..804dad47c7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +--- blank_issues_enabled: false contact_links: - name: Issue Tracker @@ -5,7 +6,10 @@ contact_links: about: Please create bug reports in the dedicated issue tracker. - name: Feature Request Tracker url: https://github.com/esphome/feature-requests - about: Please create feature requests in the dedicated feature request tracker. + about: | + Please create feature requests in the dedicated feature request tracker. - name: Frequently Asked Question url: https://esphome.io/guides/faq.html - about: Please view the FAQ for common questions and what to include in a bug report. + about: | + Please view the FAQ for common questions and what + to include in a bug report. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f9c8cce0af..3221b8ac5c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ # What does this implement/fix? -Quick description and explanation of changes + ## Types of changes @@ -18,6 +18,7 @@ Quick description and explanation of changes - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 +- [ ] RP2040 ## Example entry for `config.yaml`: \r\n"); ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0, @@ -197,7 +197,7 @@ QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t * wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) - ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 + ret = (QMP6988_S16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 return ret; } @@ -332,13 +332,13 @@ void QMP6988Component::calculate_pressure_() { ESP_LOGE(TAG, "Error reading raw pressure/temp values"); return; } - p_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t)(a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2])); - p_raw = (QMP6988_S32_t)(p_read - SUBTRACTOR); + p_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) | + (((QMP6988_U16_t) (a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2])); + p_raw = (QMP6988_S32_t) (p_read - SUBTRACTOR); - t_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t)(a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5])); - t_raw = (QMP6988_S32_t)(t_read - SUBTRACTOR); + t_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) | + (((QMP6988_U16_t) (a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5])); + t_raw = (QMP6988_S32_t) (t_read - SUBTRACTOR); t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw); p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int); diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h index ef944ba4ff..f0c11adf43 100644 --- a/esphome/components/qmp6988/qmp6988.h +++ b/esphome/components/qmp6988/qmp6988.h @@ -91,8 +91,8 @@ class QMP6988Component : public PollingComponent, public i2c::I2CDevice { protected: qmp6988_data_t qmp6988_data_; - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; QMP6988Oversampling temperature_oversampling_{QMP6988_OVERSAMPLING_16X}; QMP6988Oversampling pressure_oversampling_{QMP6988_OVERSAMPLING_16X}; diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp index 6bb17f0508..3959178b94 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -50,7 +50,7 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->conn_id) + if (param->read.conn_id != this->parent()->get_conn_id()) break; if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); @@ -146,17 +146,17 @@ void RadonEyeRD200::update() { void RadonEyeRD200::write_query_message_() { ESP_LOGV(TAG, "writing 0x50 to write service"); int request = 0x50; - auto status = esp_ble_gattc_write_char_descr(this->parent()->gattc_if, this->parent()->conn_id, this->write_handle_, - sizeof(request), (uint8_t *) &request, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), + this->write_handle_, sizeof(request), (uint8_t *) &request, + ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status); } } void RadonEyeRD200::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->read_handle_, - ESP_GATT_AUTH_REQ_NONE); + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), + this->read_handle_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); } diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py index d64cf3c085..1a1e641623 100644 --- a/esphome/components/rc522/__init__.py +++ b/esphome/components/rc522/__init__.py @@ -2,7 +2,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation, pins from esphome.components import i2c -from esphome.const import CONF_ON_TAG, CONF_TRIGGER_ID, CONF_RESET_PIN +from esphome.const import ( + CONF_ON_TAG, + CONF_ON_TAG_REMOVED, + CONF_TRIGGER_ID, + CONF_RESET_PIN, +) CODEOWNERS = ["@glmnet"] AUTO_LOAD = ["binary_sensor"] @@ -24,6 +29,11 @@ RC522_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), } ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), + } + ), } ).extend(cv.polling_component_schema("1s")) @@ -37,5 +47,10 @@ async def setup_rc522(var, config): for conf in config.get(CONF_ON_TAG, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_trigger(trigger)) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 5bfeb40156..4e74020e4c 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -256,7 +256,7 @@ void RC522::loop() { this->current_uid_ = rfid_uid; - for (auto *trigger : this->triggers_) + for (auto *trigger : this->triggers_ontag_) trigger->process(rfid_uid); if (report) { @@ -265,6 +265,11 @@ void RC522::loop() { break; } case STATE_DONE: { + if (!this->current_uid_.empty()) { + ESP_LOGV(TAG, "Tag '%s' removed", format_uid(this->current_uid_).c_str()); + for (auto *trigger : this->triggers_ontagremoved_) + trigger->process(this->current_uid_); + } this->current_uid_ = {}; state_ = STATE_INIT; break; diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index d853d2f5ff..c6c5e119f0 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -5,6 +5,8 @@ #include "esphome/core/automation.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include + namespace esphome { namespace rc522 { @@ -22,7 +24,8 @@ class RC522 : public PollingComponent { void loop() override; void register_tag(RC522BinarySensor *tag) { this->binary_sensors_.push_back(tag); } - void register_trigger(RC522Trigger *trig) { this->triggers_.push_back(trig); } + void register_ontag_trigger(RC522Trigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(RC522Trigger *trig) { this->triggers_ontagremoved_.push_back(trig); } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } @@ -240,7 +243,8 @@ class RC522 : public PollingComponent { uint8_t reset_count_{0}; uint32_t reset_timeout_{0}; std::vector binary_sensors_; - std::vector triggers_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; std::vector current_uid_; enum RC522Error { diff --git a/esphome/components/rdm6300/__init__.py b/esphome/components/rdm6300/__init__.py index 37ebcb49a9..f57eaaad6a 100644 --- a/esphome/components/rdm6300/__init__.py +++ b/esphome/components/rdm6300/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor"] +MULTI_CONF = True rdm6300_ns = cg.esphome_ns.namespace("rdm6300") RDM6300Component = rdm6300_ns.class_("RDM6300Component", cg.Component, uart.UARTDevice) diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 13df400754..0aeabef2bc 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -5,6 +5,8 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include + namespace esphome { namespace rdm6300 { diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index e4d1e115e7..4d9196c9c5 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -79,7 +79,9 @@ def register_trigger(name, type, data_type): validator = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + cv.Optional(CONF_RECEIVER_ID): cv.invalid( + "This has been removed in ESPHome 2022.3.0 and the trigger attaches directly to the parent receiver." + ), } ) registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator) @@ -87,7 +89,6 @@ def register_trigger(name, type, data_type): def decorator(func): async def new_func(config): var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) - await register_listener(var, config) await coroutine(func)(var, config) await automation.build_automation(var, [(data_type, "x")], config) return var @@ -223,10 +224,12 @@ async def build_binary_sensor(full_config): async def build_triggers(full_config): + triggers = [] for key in TRIGGER_REGISTRY: for config in full_config.get(key, []): func = TRIGGER_REGISTRY[key][0] - await func(config) + triggers.append(await func(config)) + return triggers async def build_dumpers(config): @@ -237,6 +240,107 @@ async def build_dumpers(config): return dumpers +# CanalSat +( + CanalSatData, + CanalSatBinarySensor, + CanalSatTrigger, + CanalSatAction, + CanalSatDumper, +) = declare_protocol("CanalSat") +CANALSAT_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint8_t, + cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t, + cv.Required(CONF_COMMAND): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("canalsat", CanalSatBinarySensor, CANALSAT_SCHEMA) +def canalsat_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CanalSatData, + ("device", config[CONF_DEVICE]), + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("canalsat", CanalSatTrigger, CanalSatData) +def canalsat_trigger(var, config): + pass + + +@register_dumper("canalsat", CanalSatDumper) +def canalsat_dumper(var, config): + pass + + +@register_action("canalsat", CanalSatAction, CANALSAT_SCHEMA) +async def canalsat_action(var, config, args): + template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8) + cg.add(var.set_device(template_)) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + +( + CanalSatLDData, + CanalSatLDBinarySensor, + CanalSatLDTrigger, + CanalSatLDAction, + CanalSatLDDumper, +) = declare_protocol("CanalSatLD") +CANALSATLD_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint8_t, + cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t, + cv.Required(CONF_COMMAND): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSAT_SCHEMA) +def canalsatld_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CanalSatLDData, + ("device", config[CONF_DEVICE]), + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("canalsatld", CanalSatLDTrigger, CanalSatLDData) +def canalsatld_trigger(var, config): + pass + + +@register_dumper("canalsatld", CanalSatLDDumper) +def canalsatld_dumper(var, config): + pass + + +@register_action("canalsatld", CanalSatLDAction, CANALSATLD_SCHEMA) +async def canalsatld_action(var, config, args): + template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8) + cg.add(var.set_device(template_)) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # Coolix ( CoolixData, @@ -609,7 +713,7 @@ def sony_dumper(var, config): @register_action("sony", SonyAction, SONY_SCHEMA) async def sony_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16) + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) cg.add(var.set_nbits(template_)) @@ -1308,9 +1412,11 @@ MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_p MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) MIDEA_SCHEMA = cv.Schema( { - cv.Required(CONF_CODE): cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=5, max=5), + cv.Required(CONF_CODE): cv.templatable( + cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=5, max=5), + ) ), } ) @@ -1337,7 +1443,12 @@ def midea_dumper(var, config): MIDEA_SCHEMA, ) async def midea_action(var, config, args): - cg.add(var.set_code(config[CONF_CODE])) + code_ = config[CONF_CODE] + if cg.is_template(code_): + template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_code_template(template_)) + else: + cg.add(var.set_code_static(code_)) # AEHA diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index 6cb4706506..c41f3f8df1 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -2,6 +2,8 @@ #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp new file mode 100644 index 0000000000..1ea47750fd --- /dev/null +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -0,0 +1,108 @@ +#include "canalsat_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const CANALSAT_TAG = "remote.canalsat"; +static const char *const CANALSATLD_TAG = "remote.canalsatld"; + +static const uint16_t CANALSAT_FREQ = 55500; +static const uint16_t CANALSATLD_FREQ = 56000; +static const uint16_t CANALSAT_UNIT = 250; +static const uint16_t CANALSATLD_UNIT = 320; + +CanalSatProtocol::CanalSatProtocol() { + this->frequency_ = CANALSAT_FREQ; + this->unit_ = CANALSAT_UNIT; + this->tag_ = CANALSAT_TAG; +} + +CanalSatLDProtocol::CanalSatLDProtocol() { + this->frequency_ = CANALSATLD_FREQ; + this->unit_ = CANALSATLD_UNIT; + this->tag_ = CANALSATLD_TAG; +} + +void CanalSatBaseProtocol::encode(RemoteTransmitData *dst, const CanalSatData &data) { + dst->reserve(48); + dst->set_carrier_frequency(this->frequency_); + + uint32_t raw{ + static_cast((1 << 23) | (data.device << 16) | (data.address << 10) | (0 << 9) | (data.command << 1))}; + bool was_high{true}; + + for (uint32_t mask = 0x800000; mask; mask >>= 1) { + if (raw & mask) { + if (was_high) { + dst->mark(this->unit_); + } + was_high = true; + if (raw & mask >> 1) { + dst->space(this->unit_); + } else { + dst->space(this->unit_ * 2); + } + } else { + if (!was_high) { + dst->space(this->unit_); + } + was_high = false; + if (raw & mask >> 1) { + dst->mark(this->unit_ * 2); + } else { + dst->mark(this->unit_); + } + } + } +} + +optional CanalSatBaseProtocol::decode(RemoteReceiveData src) { + CanalSatData data{ + .device = 0, + .address = 0, + .repeat = 0, + .command = 0, + }; + + // Check if initial mark and spaces match + if (!src.peek_mark(this->unit_) || !(src.peek_space(this->unit_, 1) || src.peek_space(this->unit_ * 2, 1))) { + return {}; + } + + uint8_t bit{1}; + uint8_t offset{1}; + uint32_t buffer{0}; + + while (offset < 24) { + buffer = buffer | (bit << (24 - offset++)); + src.advance(); + if (src.peek_mark(this->unit_) || src.peek_space(this->unit_)) { + src.advance(); + } else if (src.peek_mark(this->unit_ * 2) || src.peek_space(this->unit_ * 2)) { + bit = !bit; + } else if (offset != 24 && bit != 1) { // If last bit is high, final space is indistinguishable + return {}; + } + } + + data.device = (0xFF0000 & buffer) >> 16; + data.address = (0x00FF00 & buffer) >> 10; + data.repeat = (0x00FF00 & buffer) >> 9; + data.command = (0x0000FF & buffer) >> 1; + + return data; +} + +void CanalSatBaseProtocol::dump(const CanalSatData &data) { + if (this->tag_ == CANALSATLD_TAG) { + ESP_LOGD(this->tag_, "Received CanalSatLD: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + data.address, data.command, data.repeat); + } else { + ESP_LOGD(this->tag_, "Received CanalSat: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + data.address, data.command, data.repeat); + } +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h new file mode 100644 index 0000000000..180989ef99 --- /dev/null +++ b/esphome/components/remote_base/canalsat_protocol.h @@ -0,0 +1,78 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct CanalSatData { + uint8_t device : 7; + uint8_t address : 6; + uint8_t repeat : 1; + uint8_t command : 7; + + bool operator==(const CanalSatData &rhs) const { + return device == rhs.device && address == rhs.address && command == rhs.command; + } +}; + +struct CanalSatLDData : public CanalSatData {}; + +class CanalSatBaseProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const CanalSatData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const CanalSatData &data) override; + + protected: + uint16_t frequency_; + uint16_t unit_; + const char *tag_; +}; + +class CanalSatProtocol : public CanalSatBaseProtocol { + public: + CanalSatProtocol(); +}; + +class CanalSatLDProtocol : public CanalSatBaseProtocol { + public: + CanalSatLDProtocol(); +}; + +DECLARE_REMOTE_PROTOCOL(CanalSat) + +template class CanalSatAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, device) + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) { + CanalSatData data{}; + data.device = this->device_.value(x...); + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + CanalSatProtocol().encode(dst, data); + } +}; + +DECLARE_REMOTE_PROTOCOL(CanalSatLD) + +template class CanalSatLDAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, device) + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) { + CanalSatData data{}; + data.device = this->device_.value(x...); + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + CanalSatLDProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 135a93b36d..a7f5636b06 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,9 +1,11 @@ #pragma once -#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" +#include +#include +#include namespace esphome { namespace remote_base { @@ -84,12 +86,23 @@ using MideaDumper = RemoteReceiverDumper; template class MideaAction : public RemoteTransmitterActionBase { TEMPLATABLE_VALUE(std::vector, code) - void set_code(const std::vector &code) { code_ = code; } + void set_code_static(std::vector code) { code_static_ = std::move(code); } + void set_code_template(std::function(Ts...)> func) { this->code_func_ = func; } + void encode(RemoteTransmitData *dst, Ts... x) override { - MideaData data = this->code_.value(x...); + MideaData data; + if (!this->code_static_.empty()) { + data = MideaData(this->code_static_); + } else { + data = MideaData(this->code_func_(x...)); + } data.finalize(); MideaProtocol().encode(dst, data); } + + protected: + std::function(Ts...)> code_func_{}; + std::vector code_static_{}; }; } // namespace remote_base diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index d434744e49..4951b12bb1 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -227,7 +227,19 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { return out; } -void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); } +void ProntoProtocol::dump(const ProntoData &data) { + std::string first, rest; + if (data.data.size() < 230) { + first = data.data; + } else { + first = data.data.substr(0, 229); + rest = data.data.substr(230); + } + ESP_LOGD(TAG, "Received Pronto: data=%s", first.c_str()); + if (!rest.empty()) { + ESP_LOGD(TAG, "%s", rest.c_str()); + } +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index 291bb8a99b..8c491257d3 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 054f02ff7c..dc22282d1c 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "remote_base.h" +#include + namespace esphome { namespace remote_base { diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 47a85cda57..cb6eed4c6c 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -78,7 +78,7 @@ optional RC5Protocol::decode(RemoteReceiveData src) { out_data |= 1; } - out.command = (uint8_t)(out_data & 0x3F) + (1 - field_bit) * 64u; + out.command = (uint8_t) (out_data & 0x3F) + (1 - field_bit) * 64u; out.address = (out_data >> 6) & 0x1F; return out; } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 3c76da84e3..fdb6d45e5f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -1,4 +1,5 @@ #include +#include #pragma once diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 253204bd1a..d59ad5c7f1 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -12,7 +12,7 @@ from esphome.const import ( CONF_TOLERANCE, CONF_MEMORY_BLOCKS, ) -from esphome.core import CORE +from esphome.core import CORE, TimePeriod AUTO_LOAD = ["remote_base"] remote_receiver_ns = cg.esphome_ns.namespace("remote_receiver") @@ -33,9 +33,10 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", esp8266="1000b" ): cv.validate_bytes, - cv.Optional( - CONF_FILTER, default="50us" - ): cv.positive_time_period_microseconds, + cv.Optional(CONF_FILTER, default="50us"): cv.All( + cv.positive_time_period_microseconds, + cv.Range(max=TimePeriod(microseconds=255)), + ), cv.Optional( CONF_IDLE, default="10ms" ): cv.positive_time_period_microseconds, @@ -56,7 +57,9 @@ async def to_code(config): for dumper in dumpers: cg.add(var.register_dumper(dumper)) - await remote_base.build_triggers(config) + triggers = await remote_base.build_triggers(config) + for trigger in triggers: + cg.add(var.register_listener(trigger)) await cg.register_component(var, config) cg.add(var.set_tolerance(config[CONF_TOLERANCE])) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index a4235e875f..560d83802e 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/remote_base/remote_base.h" +#include + namespace esphome { namespace remote_transmitter { diff --git a/esphome/components/restart/button/__init__.py b/esphome/components/restart/button/__init__.py index 1a0e9cdc3d..1b2c991261 100644 --- a/esphome/components/restart/button/__init__.py +++ b/esphome/components/restart/button/__init__.py @@ -10,13 +10,11 @@ from esphome.const import ( restart_ns = cg.esphome_ns.namespace("restart") RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component) -CONFIG_SCHEMA = ( - button.button_schema( - device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG - ) - .extend({cv.GenerateID(): cv.declare_id(RestartButton)}) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index d8c8047496..c34b3d2dc4 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -49,8 +49,9 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { data.high = (raw[6] << 8) | raw[7]; data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10]; - if (action == RF_CODE_LEARN_OK) + if (action == RF_CODE_LEARN_OK) { ESP_LOGD(TAG, "Learning success"); + } ESP_LOGI(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, data.code); diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 9156d995bc..fe6dd96b38 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py new file mode 100644 index 0000000000..3d0d6ec060 --- /dev/null +++ b/esphome/components/rp2040/__init__.py @@ -0,0 +1,161 @@ +import logging + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_BOARD, + CONF_FRAMEWORK, + CONF_SOURCE, + CONF_VERSION, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE, coroutine_with_priority + +from .const import KEY_BOARD, KEY_RP2040, rp2040_ns + +# force import gpio to register pin schema +from .gpio import rp2040_pin_to_code # noqa + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@jesserockz"] +AUTO_LOAD = [] + + +def set_core_data(config): + CORE.data[KEY_RP2040] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "rp2040" + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( + config[CONF_FRAMEWORK][CONF_VERSION] + ) + CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD] + + return config + + +def _format_framework_arduino_version(ver: cv.Version) -> str: + # The most recent releases have not been uploaded to platformio so grabbing them directly from + # the GitHub release is one path forward for now. + return f"https://github.com/earlephilhower/arduino-pico/releases/download/{ver}/rp2040-{ver}.zip" + + # format the given arduino (https://github.com/earlephilhower/arduino-pico/releases) version to + # a PIO earlephilhower/framework-arduinopico value + # List of package versions: https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico + # return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + +# NOTE: Keep this in mind when updating the recommended version: +# * The new version needs to be thoroughly validated before changing the +# recommended version as otherwise a bunch of devices could be bricked +# * For all constants below, update platformio.ini (in this repo) +# and platformio.ini/platformio-lint.ini in the esphome-docker-base repository + +# The default/recommended arduino framework version +# - https://github.com/earlephilhower/arduino-pico/releases +# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 6, 4) + +# The platformio/raspberrypi version to use for arduino frameworks +# - https://github.com/platformio/platform-raspberrypi/releases +# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi +ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0) + + +def _arduino_check_versions(value): + value = value.copy() + lookups = { + "dev": (cv.Version(2, 6, 4), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(2, 6, 4), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), + } + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] + else: + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) + + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) + + value[CONF_PLATFORM_VERSION] = value.get( + CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) + ) + + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + _LOGGER.warning( + "The selected Arduino framework version is not the recommended one." + ) + + return value + + +def _parse_platform_version(value): + try: + # if platform version is a valid version constraint, prefix the default package + cv.platformio_version_constraint(value) + return f"platformio/raspberrypi@{value}" + except cv.Invalid: + return value + + +CONF_PLATFORM_VERSION = "platform_version" + +ARDUINO_FRAMEWORK_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + } + ), + _arduino_check_versions, +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, + } + ), + set_core_data, +) + + +@coroutine_with_priority(1000) +async def to_code(config): + cg.add(rp2040_ns.setup_preferences()) + + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_RP2040") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "RP2040") + + conf = config[CONF_FRAMEWORK] + cg.add_platformio_option("framework", "arduino") + cg.add_build_flag("-DUSE_ARDUINO") + cg.add_build_flag("-DUSE_RP2040_FRAMEWORK_ARDUINO") + # cg.add_build_flag("-DPICO_BOARD=pico_w") + cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + cg.add_platformio_option( + "platform_packages", + [f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"], + ) + + cg.add_platformio_option("board_build.core", "earlephilhower") + cg.add_platformio_option("board_build.filesystem_size", "1m") + + ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + cg.add_define( + "USE_ARDUINO_VERSION_CODE", + cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), + ) diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py new file mode 100644 index 0000000000..c761efba58 --- /dev/null +++ b/esphome/components/rp2040/boards.py @@ -0,0 +1,28 @@ +RP2040_BASE_PINS = {} + +RP2040_BOARD_PINS = { + "pico": { + "SDA": 4, + "SCL": 5, + "LED": 25, + "SDA1": 26, + "SCL1": 27, + }, + "rpipico": "pico", + "rpipicow": { + "SDA": 4, + "SCL": 5, + "LED": 32, + "SDA1": 26, + "SCL1": 27, + }, +} + +BOARDS = { + "rpipico": { + "name": "Raspberry Pi Pico", + }, + "rpipicow": { + "name": "Raspberry Pi Pico W", + }, +} diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py new file mode 100644 index 0000000000..e09016ca31 --- /dev/null +++ b/esphome/components/rp2040/const.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +KEY_BOARD = "board" +KEY_RP2040 = "rp2040" + +rp2040_ns = cg.esphome_ns.namespace("rp2040") diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp new file mode 100644 index 0000000000..c20401c791 --- /dev/null +++ b/esphome/components/rp2040/core.cpp @@ -0,0 +1,33 @@ +#ifdef USE_RP2040 + +#include "core.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include "hardware/watchdog.h" + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::yield(); } +uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +uint32_t IRAM_ATTR HOT micros() { return ::micros(); } +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +void arch_restart() { + watchdog_reboot(0, 0, 10); + while (1) { + continue; + } +} +void arch_init() { watchdog_enable(0x7fffff, false); } +void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } + +uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } +uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/core.h b/esphome/components/rp2040/core.h new file mode 100644 index 0000000000..92fc4f824e --- /dev/null +++ b/esphome/components/rp2040/core.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_RP2040 + +#include +#include + +extern "C" unsigned long ulMainGetRunTimeCounterValue(); + +namespace esphome { +namespace rp2040 {} // namespace rp2040 +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/esp32/gpio_arduino.cpp b/esphome/components/rp2040/gpio.cpp similarity index 54% rename from esphome/components/esp32/gpio_arduino.cpp rename to esphome/components/rp2040/gpio.cpp index ba92894f97..e32b93b5c2 100644 --- a/esphome/components/esp32/gpio_arduino.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -1,16 +1,15 @@ -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_RP2040 -#include "gpio_arduino.h" +#include "gpio.h" #include "esphome/core/log.h" -#include namespace esphome { -namespace esp32 { +namespace rp2040 { -static const char *const TAG = "esp32"; +static const char *const TAG = "rp2040"; -static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { - if (flags == gpio::FLAG_INPUT) { +static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { + if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) return INPUT; } else if (flags == gpio::FLAG_OUTPUT) { return OUTPUT; @@ -18,8 +17,8 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags) { return INPUT_PULLUP; } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { return INPUT_PULLDOWN; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - return OUTPUT_OPEN_DRAIN; + // } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + // return OpenDrain; } else { return 0; } @@ -30,15 +29,15 @@ struct ISRPinArg { bool inverted; }; -ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const { +ISRInternalGPIOPin RP2040GPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) arg->pin = pin_; arg->inverted = inverted_; return ISRInternalGPIOPin((void *) arg); } -void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { - uint8_t arduino_mode = DISABLED; +void RP2040GPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + PinStatus arduino_mode = LOW; switch (type) { case gpio::INTERRUPT_RISING_EDGE: arduino_mode = inverted_ ? FALLING : RISING; @@ -50,39 +49,36 @@ void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, g arduino_mode = CHANGE; break; case gpio::INTERRUPT_LOW_LEVEL: - arduino_mode = inverted_ ? ONHIGH : ONLOW; + arduino_mode = inverted_ ? HIGH : LOW; break; case gpio::INTERRUPT_HIGH_LEVEL: - arduino_mode = inverted_ ? ONLOW : ONHIGH; + arduino_mode = inverted_ ? LOW : HIGH; break; } - attachInterruptArg(pin_, func, arg, arduino_mode); + attachInterrupt(pin_, func, arduino_mode, arg); +} +void RP2040GPIOPin::pin_mode(gpio::Flags flags) { + pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { - pinMode(pin_, flags_to_mode(flags)); // NOLINT -} - -std::string ArduinoInternalGPIOPin::dump_summary() const { +std::string RP2040GPIOPin::dump_summary() const { char buffer[32]; snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); return buffer; } -bool ArduinoInternalGPIOPin::digital_read() { +bool RP2040GPIOPin::digital_read() { return bool(digitalRead(pin_)) != inverted_; // NOLINT } -void ArduinoInternalGPIOPin::digital_write(bool value) { +void RP2040GPIOPin::digital_write(bool value) { digitalWrite(pin_, value != inverted_ ? 1 : 0); // NOLINT } -void ArduinoInternalGPIOPin::detach_interrupt() const { - detachInterrupt(pin_); // NOLINT -} +void RP2040GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } -} // namespace esp32 +} // namespace rp2040 -using namespace esp32; +using namespace rp2040; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(arg_); @@ -93,22 +89,15 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT } void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { - auto *arg = reinterpret_cast(arg_); -#ifdef CONFIG_IDF_TARGET_ESP32C3 - GPIO.status_w1tc.val = 1UL << arg->pin; -#else - if (arg->pin < 32) { - GPIO.status_w1tc = 1UL << arg->pin; - } else { - GPIO.status1_w1tc.intr_st = 1UL << (arg->pin - 32); - } -#endif + // TODO: implement + // auto *arg = reinterpret_cast(arg_); + // GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { auto *arg = reinterpret_cast(arg_); - pinMode(arg->pin, flags_to_mode(flags)); // NOLINT + pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT } } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_RP2040 diff --git a/esphome/components/esp32/gpio_arduino.h b/esphome/components/rp2040/gpio.h similarity index 83% rename from esphome/components/esp32/gpio_arduino.h rename to esphome/components/rp2040/gpio.h index e88d39b1a8..ef9500d5dd 100644 --- a/esphome/components/esp32/gpio_arduino.h +++ b/esphome/components/rp2040/gpio.h @@ -1,12 +1,14 @@ #pragma once -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_RP2040 + +#include #include "esphome/core/hal.h" namespace esphome { -namespace esp32 { +namespace rp2040 { -class ArduinoInternalGPIOPin : public InternalGPIOPin { +class RP2040GPIOPin : public InternalGPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -30,7 +32,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { gpio::Flags flags_; }; -} // namespace esp32 +} // namespace rp2040 } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py new file mode 100644 index 0000000000..2340bed892 --- /dev/null +++ b/esphome/components/rp2040/gpio.py @@ -0,0 +1,110 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +from esphome.core import CORE +from esphome import pins + +from . import boards +from .const import KEY_BOARD, KEY_RP2040, rp2040_ns + +RP2040GPIOPin = rp2040_ns.class_("RP2040GPIOPin", cg.InternalGPIOPin) + + +def _lookup_pin(value): + board = CORE.data[KEY_RP2040][KEY_BOARD] + board_pins = boards.RP2040_BOARD_PINS.get(board, {}) + + while isinstance(board_pins, str): + board_pins = boards.RP2040_BOARD_PINS[board_pins] + + if value in board_pins: + return board_pins[value] + if value in boards.RP2040_BASE_PINS: + return boards.RP2040_BASE_PINS[value] + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.") + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return _lookup_pin(value) + + +def validate_gpio_pin(value): + value = _translate_pin(value) + board = CORE.data[KEY_RP2040][KEY_BOARD] + if board == "rpipicow" and value == 32: + return value # Special case for Pico-w LED pin + if value < 0 or value > 29: + raise cv.Invalid(f"RP2040: Invalid pin number: {value}") + return value + + +def validate_supports(value): + board = CORE.data[KEY_RP2040][KEY_BOARD] + if board != "rpipicow" or value[CONF_NUMBER] != 32: + return value + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + is_output = mode[CONF_OUTPUT] + is_open_drain = mode[CONF_OPEN_DRAIN] + is_pullup = mode[CONF_PULLUP] + is_pulldown = mode[CONF_PULLDOWN] + if not is_output or is_input or is_open_drain or is_pullup or is_pulldown: + raise cv.Invalid("Only output mode is supported for Pico-w LED pin") + return value + + +CONF_ANALOG = "analog" + +RP2040_PIN_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RP2040GPIOPin), + cv.Required(CONF_NUMBER): validate_gpio_pin, + cv.Optional(CONF_MODE, default={}): cv.Schema( + { + cv.Optional(CONF_ANALOG, default=False): cv.boolean, + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } + ), + validate_supports, +) + + +@pins.PIN_SCHEMA_REGISTRY.register("rp2040", RP2040_PIN_SCHEMA) +async def rp2040_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp new file mode 100644 index 0000000000..e7aa9ab28d --- /dev/null +++ b/esphome/components/rp2040/preferences.cpp @@ -0,0 +1,160 @@ +#ifdef USE_RP2040 + +#include + +#include +#include + +#include "preferences.h" + +#include +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace rp2040 { + +static const char *const TAG = "rp2040.preferences"; + +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; + +extern "C" uint8_t _EEPROM_start; + +template uint8_t calculate_crc(It first, It last, uint32_t type) { + std::array type_array = decode_value(type); + uint8_t crc = type_array[0] ^ type_array[1] ^ type_array[2] ^ type_array[3]; + while (first != last) { + crc ^= (*first++); + } + return crc; +} + +class RP2040PreferenceBackend : public ESPPreferenceBackend { + public: + size_t offset = 0; + uint32_t type = 0; + + bool save(const uint8_t *data, size_t len) override { + std::vector buffer; + buffer.resize(len + 1); + memcpy(buffer.data(), data, len); + buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type); + + for (uint32_t i = 0; i < len + 1; i++) { + uint32_t j = offset + i; + if (j >= RP2040_FLASH_STORAGE_SIZE) + return false; + uint8_t v = buffer[i]; + uint8_t *ptr = &s_flash_storage[j]; + if (*ptr != v) + s_flash_dirty = true; + *ptr = v; + } + return true; + } + bool load(uint8_t *data, size_t len) override { + std::vector buffer; + buffer.resize(len + 1); + + for (size_t i = 0; i < len + 1; i++) { + uint32_t j = offset + i; + if (j >= RP2040_FLASH_STORAGE_SIZE) + return false; + buffer[i] = s_flash_storage[j]; + } + + uint8_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); + if (buffer[buffer.size() - 1] != crc) { + return false; + } + + memcpy(data, buffer.data(), len); + return true; + } +}; + +class RP2040Preferences : public ESPPreferences { + public: + uint32_t current_flash_offset = 0; + + RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} + void setup() { + s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT + ESP_LOGVV(TAG, "Loading preferences from flash..."); + memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + return make_preference(length, type); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { + uint32_t start = this->current_flash_offset; + uint32_t end = start + length + 1; + if (end > RP2040_FLASH_STORAGE_SIZE) { + return {}; + } + auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = start; + pref->type = type; + current_flash_offset = end; + return {pref}; + } + + bool sync() override { + if (!s_flash_dirty) + return true; + if (s_prevent_write) + return false; + + ESP_LOGD(TAG, "Saving preferences to flash..."); + + { + InterruptLock lock; + ::rp2040.idleOtherCore(); + flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096); + flash_range_program((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage, RP2040_FLASH_STORAGE_SIZE); + ::rp2040.resumeOtherCore(); + } + + s_flash_dirty = false; + return true; + } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + { + InterruptLock lock; + ::rp2040.idleOtherCore(); + flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096); + ::rp2040.resumeOtherCore(); + } + s_prevent_write = true; + return true; + } + + protected: + uint8_t *eeprom_sector_; +}; + +void setup_preferences() { + auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) + prefs->setup(); + global_preferences = prefs; +} +void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } + +} // namespace rp2040 + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/preferences.h b/esphome/components/rp2040/preferences.h new file mode 100644 index 0000000000..b815c6d58a --- /dev/null +++ b/esphome/components/rp2040/preferences.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_RP2040 + +namespace esphome { +namespace rp2040 { + +void setup_preferences(); +void preferences_prevent_write(bool prevent); + +} // namespace rp2040 +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040_pwm/__init__.py b/esphome/components/rp2040_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py new file mode 100644 index 0000000000..8f2972d4a0 --- /dev/null +++ b/esphome/components/rp2040_pwm/output.py @@ -0,0 +1,55 @@ +from esphome import pins, automation +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["rp2040"] + + +rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") +RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) +SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) +validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(RP2040PWM), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + + +@automation.register_action( + "output.rp2040_pwm.set_frequency", + SetFrequencyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(RP2040PWM), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), + } + ), +) +async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + return var diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp new file mode 100644 index 0000000000..3c5591885e --- /dev/null +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -0,0 +1,64 @@ +#ifdef USE_RP2040 + +#include "rp2040_pwm.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/macros.h" + +#include +#include +#include +#include + +namespace esphome { +namespace rp2040_pwm { + +static const char *const TAG = "rp2040_pwm"; + +void RP2040PWM::setup() { + ESP_LOGCONFIG(TAG, "Setting up RP2040 PWM Output..."); + + this->setup_pwm_(); +} + +void RP2040PWM::setup_pwm_() { + pwm_config config = pwm_get_default_config(); + + uint32_t clock = clock_get_hz(clk_sys); + float divider = ceil(clock / (4096 * this->frequency_)) / 16.0f; + uint16_t wrap = clock / divider / this->frequency_ - 1; + this->wrap_ = wrap; + + pwm_config_set_clkdiv(&config, divider); + pwm_config_set_wrap(&config, wrap); + pwm_init(pwm_gpio_to_slice_num(this->pin_->get_pin()), &config, true); +} + +void RP2040PWM::dump_config() { + ESP_LOGCONFIG(TAG, "RP2040 PWM:"); + LOG_PIN(" Pin: ", this->pin_); + ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); + LOG_FLOAT_OUTPUT(this); +} +void HOT RP2040PWM::write_state(float state) { + this->last_output_ = state; + + // Also check pin inversion + if (this->pin_->is_inverted()) { + state = 1.0f - state; + } + + if (this->frequency_changed_) { + this->setup_pwm_(); + this->frequency_changed_ = false; + } + + gpio_set_function(this->pin_->get_pin(), GPIO_FUNC_PWM); + pwm_set_gpio_level(this->pin_->get_pin(), state * this->wrap_); +} + +} // namespace rp2040_pwm +} // namespace esphome + +#endif diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h new file mode 100644 index 0000000000..e499e72b06 --- /dev/null +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -0,0 +1,59 @@ +#pragma once + +#ifdef USE_RP2040 + +#include "esphome/components/output/float_output.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace rp2040_pwm { + +class RP2040PWM : public output::FloatOutput, public Component { + public: + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } + void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically update frequency + void update_frequency(float frequency) override { + this->set_frequency(frequency); + this->frequency_changed_ = true; + this->write_state(this->last_output_); + } + + /// 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 setup_pwm_(); + + InternalGPIOPin *pin_; + float frequency_{1000.0}; + uint16_t wrap_{65535}; + /// Cache last output level for dynamic frequency updating + float last_output_{0.0}; + bool frequency_changed_{false}; +}; + +template class SetFrequencyAction : public Action { + public: + SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->update_frequency(freq); + } + + RP2040PWM *parent_; +}; + +} // namespace rp2040_pwm +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py index 2cd8892afb..307e4e372e 100644 --- a/esphome/components/safe_mode/button/__init__.py +++ b/esphome/components/safe_mode/button/__init__.py @@ -17,11 +17,11 @@ SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Compone CONFIG_SCHEMA = ( button.button_schema( + SafeModeButton, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(): cv.declare_id(SafeModeButton)}) .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h new file mode 100644 index 0000000000..37b3bc1674 --- /dev/null +++ b/esphome/components/scd30/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "scd30.h" + +namespace esphome { +namespace scd30 { + +template class ForceRecalibrationWithReference : public Action, public Parented { + public: + void play(Ts... x) override { + if (this->value_.has_value()) { + this->parent_->force_recalibration_with_reference(this->value_.value(x...)); + } + } + + protected: + TEMPLATABLE_VALUE(uint16_t, value) +}; + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 103b7a255d..01abca0a1f 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -43,7 +43,7 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); if (this->temperature_offset_ != 0) { - if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { + if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t) (temperature_offset_ * 100.0))) { ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -202,5 +202,27 @@ bool SCD30Component::is_data_ready_() { return is_data_ready == 1; } +bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) { + ESP_LOGD(TAG, "Performing CO2 force recalibration with reference %dppm.", co2_reference); + if (this->write_command(SCD30_CMD_FORCED_CALIBRATION, co2_reference)) { + ESP_LOGD(TAG, "Force recalibration complete."); + return true; + } else { + ESP_LOGE(TAG, "Failed to force recalibration with reference."); + this->error_code_ = FORCE_RECALIBRATION_FAILED; + this->status_set_warning(); + return false; + } +} + +uint16_t SCD30Component::get_forced_calibration_reference() { + uint16_t forced_calibration_reference; + // Get current CO2 calibration + if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { + ESP_LOGE(TAG, "Unable to read forced calibration reference."); + } + return forced_calibration_reference; +} + } // namespace scd30 } // namespace esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index c434bf0dea..40f075e673 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -16,10 +16,12 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } void set_ambient_pressure_compensation(float pressure) { - ambient_pressure_compensation_ = (uint16_t)(pressure * 1000); + ambient_pressure_compensation_ = (uint16_t) (pressure * 1000); } void set_temperature_offset(float offset) { temperature_offset_ = offset; } void set_update_interval(uint16_t interval) { update_interval_ = interval; } + bool force_recalibration_with_reference(uint16_t co2_reference); + uint16_t get_forced_calibration_reference(); void setup() override; void update(); @@ -33,6 +35,7 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe COMMUNICATION_FAILED, FIRMWARE_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, + FORCE_RECALIBRATION_FAILED, UNKNOWN } error_code_{UNKNOWN}; bool enable_asc_{true}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 3cfd861a63..1ddf0f1e85 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,4 +1,4 @@ -from esphome import core +from esphome import automation, core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor @@ -9,6 +9,8 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_CO2, CONF_UPDATE_INTERVAL, + CONF_VALUE, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -26,6 +28,11 @@ SCD30Component = scd30_ns.class_( "SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice ) +# Actions +ForceRecalibrationWithReference = scd30_ns.class_( + "ForceRecalibrationWithReference", automation.Action +) + CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" @@ -40,6 +47,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( @@ -106,3 +114,26 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + + +@automation.register_action( + "scd30.force_recalibration_with_reference", + ForceRecalibrationWithReference, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SCD30Component), + cv.Required(CONF_VALUE): cv.templatable( + cv.int_range(min=400, max=2000, max_included=True) + ), + }, + key=CONF_VALUE, + ), +) +async def scd30_force_recalibration_with_reference_to_code( + config, action_id, template_arg, args +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) + cg.add(var.set_value(template_)) + return var diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 681324fa18..a8a4129b48 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -50,7 +50,7 @@ void SCD4XComponent::setup() { uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, - (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { + (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { ESP_LOGE(TAG, "Error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -149,9 +149,9 @@ void SCD4XComponent::update() { } if (this->ambient_pressure_source_ != nullptr) { - float pressure = this->ambient_pressure_source_->state / 1000.0f; + float pressure = this->ambient_pressure_source_->state; if (!std::isnan(pressure)) { - set_ambient_pressure_compensation(this->ambient_pressure_source_->state / 1000.0f); + set_ambient_pressure_compensation(pressure); } } @@ -254,12 +254,15 @@ bool SCD4XComponent::factory_reset() { return true; } -// Note pressure in bar here. Convert to hPa -void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { +void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { ambient_pressure_compensation_ = true; - uint16_t new_ambient_pressure = (uint16_t)(pressure_in_bar * 1000); - // remove millibar from comparison to avoid frequent updates +/- 10 millibar doesn't matter - if (initialized_ && (new_ambient_pressure / 10 != ambient_pressure_ / 10)) { + uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; + if (!initialized_) { + ambient_pressure_ = new_ambient_pressure; + return; + } + // Only send pressure value if it has changed since last update + if (new_ambient_pressure != ambient_pressure_) { update_ambient_pressure_compensation_(new_ambient_pressure); ambient_pressure_ = new_ambient_pressure; } else { diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 23c3766e60..22055e78d0 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -26,7 +26,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } - void set_ambient_pressure_compensation(float pressure_in_bar); + void set_ambient_pressure_compensation(float pressure_in_hpa); void set_ambient_pressure_source(sensor::Sensor *pressure) { ambient_pressure_source_ = pressure; } void set_temperature_offset(float offset) { temperature_offset_ = offset; }; diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 9702878475..6337d89bcd 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_MODE +from esphome.const import CONF_ID, CONF_MODE, CONF_PARAMETERS +from esphome.core import CORE, EsphomeError CODEOWNERS = ["@esphome/core"] script_ns = cg.esphome_ns.namespace("script") @@ -16,6 +17,7 @@ RestartScript = script_ns.class_("RestartScript", Script) QueueingScript = script_ns.class_("QueueingScript", Script, cg.Component) ParallelScript = script_ns.class_("ParallelScript", Script) +CONF_SCRIPT = "script" CONF_SINGLE = "single" CONF_RESTART = "restart" CONF_QUEUED = "queued" @@ -29,6 +31,18 @@ SCRIPT_MODES = { CONF_PARALLEL: ParallelScript, } +PARAMETER_TYPE_TRANSLATIONS = { + "string": "std::string", +} + + +def get_script(script_id): + scripts = CORE.config.get(CONF_SCRIPT, {}) + for script in scripts: + if script.get(CONF_ID, None) == script_id: + return script + raise cv.Invalid(f"Script id '{script_id}' not found") + def check_max_runs(value): if CONF_MAX_RUNS not in value: @@ -47,6 +61,43 @@ def assign_declare_id(value): return value +def parameters_to_template(args): + template_args = [] + func_args = [] + script_arg_names = [] + for name, type_ in args.items(): + array = False + if type_.endswith("[]"): + array = True + type_ = type_[:-2] + type_ = PARAMETER_TYPE_TRANSLATIONS.get(type_, type_) + if array: + type_ = f"std::vector<{type_}>" + type_ = cg.esphome_ns.namespace(type_) + template_args.append(type_) + func_args.append((type_, name)) + script_arg_names.append(name) + template = cg.TemplateArguments(*template_args) + return template, func_args + + +def validate_parameter_name(value): + value = cv.string(value) + if value != CONF_ID: + return value + raise cv.Invalid(f"Script's parameter name cannot be {CONF_ID}") + + +ALLOWED_PARAM_TYPE_CHARSET = set("abcdefghijklmnopqrstuvwxyz0123456789_:*&[]") + + +def validate_parameter_type(value): + value = cv.string_strict(value) + if set(value.lower()) <= ALLOWED_PARAM_TYPE_CHARSET: + return value + raise cv.Invalid("Parameter type contains invalid characters") + + CONFIG_SCHEMA = automation.validate_automation( { # Don't declare id as cv.declare_id yet, because the ID type @@ -56,6 +107,11 @@ CONFIG_SCHEMA = automation.validate_automation( *SCRIPT_MODES, lower=True ), cv.Optional(CONF_MAX_RUNS): cv.positive_int, + cv.Optional(CONF_PARAMETERS, default={}): cv.Schema( + { + validate_parameter_name: validate_parameter_type, + } + ), }, extra_validators=cv.All(check_max_runs, assign_declare_id), ) @@ -65,7 +121,8 @@ async def to_code(config): # Register all variables first, so that scripts can use other scripts triggers = [] for conf in config: - trigger = cg.new_Pvariable(conf[CONF_ID]) + template, func_args = parameters_to_template(conf[CONF_PARAMETERS]) + trigger = cg.new_Pvariable(conf[CONF_ID], template) # Add a human-readable name to the script cg.add(trigger.set_name(conf[CONF_ID].id)) @@ -75,10 +132,10 @@ async def to_code(config): if conf[CONF_MODE] == CONF_QUEUED: await cg.register_component(trigger, conf) - triggers.append((trigger, conf)) + triggers.append((trigger, func_args, conf)) - for trigger, conf in triggers: - await automation.build_automation(trigger, [], conf) + for trigger, func_args, conf in triggers: + await automation.build_automation(trigger, func_args, conf) @automation.register_action( @@ -87,12 +144,39 @@ async def to_code(config): maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(Script), - } + cv.Optional(validate_parameter_name): cv.templatable(cv.valid), + }, ), ) async def script_execute_action_to_code(config, action_id, template_arg, args): + async def get_ordered_args(config, script_params): + config_args = config.copy() + config_args.pop(CONF_ID) + + # match script_args to the formal parameter order + script_args = [] + for type, name in script_params: + if name not in config_args: + raise EsphomeError( + f"Missing parameter: '{name}' in script.execute {config[CONF_ID]}" + ) + arg = await cg.templatable(config_args[name], args, type) + script_args.append(arg) + return script_args + + script = get_script(config[CONF_ID]) + params = script.get(CONF_PARAMETERS, []) + template, script_params = parameters_to_template(params) + script_args = await get_ordered_args(config, script_params) + + # We need to use the parent class 'Script' as the template argument + # to match the partial specialization of the ScriptExecuteAction template + template_arg = cg.TemplateArguments(Script.template(template), *template_arg) + paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_args(*script_args)) + return var @automation.register_action( @@ -101,7 +185,8 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), ) async def script_stop_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) + full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) + template_arg = cg.TemplateArguments(full_id.type, *template_arg) return cg.new_Pvariable(action_id, template_arg, paren) @@ -111,7 +196,8 @@ async def script_stop_action_to_code(config, action_id, template_arg, args): maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), ) async def script_wait_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) + full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) + template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) await cg.register_component(var, {}) return var @@ -123,5 +209,6 @@ async def script_wait_action_to_code(config, action_id, template_arg, args): automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), ) async def script_is_running_to_code(config, condition_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) + full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) + template_arg = cg.TemplateArguments(full_id.type, *template_arg) return cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp index 46bcef905b..331f7dcd65 100644 --- a/esphome/components/script/script.cpp +++ b/esphome/components/script/script.cpp @@ -6,61 +6,8 @@ namespace script { static const char *const TAG = "script"; -void SingleScript::execute() { - if (this->is_action_running()) { - ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); - return; - } - - this->trigger(); -} - -void RestartScript::execute() { - if (this->is_action_running()) { - ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); - this->stop_action(); - } - - this->trigger(); -} - -void QueueingScript::execute() { - if (this->is_action_running()) { - // num_runs_ is the number of *queued* instances, so total number of instances is - // num_runs_ + 1 - if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { - ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); - return; - } - - ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); - this->num_runs_++; - return; - } - - this->trigger(); - // Check if the trigger was immediate and we can continue right away. - this->loop(); -} - -void QueueingScript::stop() { - this->num_runs_ = 0; - Script::stop(); -} - -void QueueingScript::loop() { - if (this->num_runs_ != 0 && !this->is_action_running()) { - this->num_runs_--; - this->trigger(); - } -} - -void ParallelScript::execute() { - if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { - ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); - return; - } - this->trigger(); +void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) { + esp_log_printf_(level, TAG, line, format, param); } } // namespace script diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 5663d32ce8..165f90ed11 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -2,27 +2,49 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/log.h" +#include namespace esphome { namespace script { +class ScriptLogger { + protected: + void esp_logw_(int line, const char *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); + } + void esp_logd_(int line, const char *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); + } + void esp_log_(int level, int line, const char *format, const char *param); +}; + /// The abstract base class for all script types. -class Script : public Trigger<> { +template class Script : public ScriptLogger, public Trigger { public: /** Execute a new instance of this script. * * The behavior of this function when a script is already running is defined by the subtypes */ - virtual void execute() = 0; + virtual void execute(Ts...) = 0; /// Check if any instance of this script is currently running. virtual bool is_running() { return this->is_action_running(); } /// Stop all instances of this script. virtual void stop() { this->stop_action(); } + // execute this script using a tuple that contains the arguments + void execute_tuple(const std::tuple &tuple) { + this->execute_tuple_(tuple, typename gens::type()); + } + // Internal function to give scripts readable names. void set_name(const std::string &name) { name_ = name; } protected: + template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->execute(std::get(tuple)...); + } + std::string name_; }; @@ -31,9 +53,16 @@ class Script : public Trigger<> { * If a new instance is executed while the previous one hasn't finished yet, * a warning is printed and the new instance is discarded. */ -class SingleScript : public Script { +template class SingleScript : public Script { public: - void execute() override; + void execute(Ts... x) override { + if (this->is_action_running()) { + this->esp_logw_(__LINE__, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + return; + } + + this->trigger(x...); + } }; /** A script type that restarts scripts from the beginning when a new instance is started. @@ -41,25 +70,68 @@ class SingleScript : public Script { * If a new instance is started but another one is already running, the existing * script is stopped and the new instance starts from the beginning. */ -class RestartScript : public Script { +template class RestartScript : public Script { public: - void execute() override; + void execute(Ts... x) override { + if (this->is_action_running()) { + this->esp_logd_(__LINE__, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->stop_action(); + } + + this->trigger(x...); + } }; /** A script type that queues new instances that are created. * * Only one instance of the script can be active at a time. */ -class QueueingScript : public Script, public Component { +template class QueueingScript : public Script, public Component { public: - void execute() override; - void stop() override; - void loop() override; + void execute(Ts... x) override { + if (this->is_action_running() || this->num_runs_ > 0) { + // num_runs_ is the number of *queued* instances, so total number of instances is + // num_runs_ + 1 + if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { + this->esp_logw_(__LINE__, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + return; + } + + this->esp_logd_(__LINE__, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->num_runs_++; + this->var_queue_.push(std::make_tuple(x...)); + return; + } + + this->trigger(x...); + // Check if the trigger was immediate and we can continue right away. + this->loop(); + } + + void stop() override { + this->num_runs_ = 0; + Script::stop(); + } + + void loop() override { + if (this->num_runs_ != 0 && !this->is_action_running()) { + this->num_runs_--; + auto &vars = this->var_queue_.front(); + this->var_queue_.pop(); + this->trigger_tuple_(vars, typename gens::type()); + } + } + void set_max_runs(int max_runs) { max_runs_ = max_runs; } protected: + template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->trigger(std::get(tuple)...); + } + int num_runs_ = 0; int max_runs_ = 0; + std::queue> var_queue_; }; /** A script type that executes new instances in parallel. @@ -67,48 +139,84 @@ class QueueingScript : public Script, public Component { * If a new instance is started while previous ones haven't finished yet, * the new one is executed in parallel to the other instances. */ -class ParallelScript : public Script { +template class ParallelScript : public Script { public: - void execute() override; + void execute(Ts... x) override { + if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { + this->esp_logw_(__LINE__, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + return; + } + this->trigger(x...); + } void set_max_runs(int max_runs) { max_runs_ = max_runs; } protected: int max_runs_ = 0; }; -template class ScriptExecuteAction : public Action { - public: - ScriptExecuteAction(Script *script) : script_(script) {} +template class ScriptExecuteAction; - void play(Ts... x) override { this->script_->execute(); } +template class ScriptExecuteAction, Ts...> : public Action { + public: + ScriptExecuteAction(Script *script) : script_(script) {} + + using Args = std::tuple...>; + + template void set_args(F... x) { args_ = Args{x...}; } + + void play(Ts... x) override { this->script_->execute_tuple(this->eval_args_(x...)); } protected: - Script *script_; + // NOTE: + // `eval_args_impl` functions evaluates `I`th the functions in `args` member. + // and then recursively calls `eval_args_impl` for the `I+1`th arg. + // if `I` = `N` all args have been stored, and nothing is done. + + template + void eval_args_impl_(std::tuple & /*unused*/, std::integral_constant /*unused*/, + std::integral_constant /*unused*/, Ts... /*unused*/) {} + + template + void eval_args_impl_(std::tuple &evaled_args, std::integral_constant /*unused*/, + std::integral_constant n, Ts... x) { + std::get(evaled_args) = std::get(args_).value(x...); // NOTE: evaluate `i`th arg, and store in tuple. + eval_args_impl_(evaled_args, std::integral_constant{}, n, + x...); // NOTE: recurse to next index. + } + + std::tuple eval_args_(Ts... x) { + std::tuple evaled_args; + eval_args_impl_(evaled_args, std::integral_constant{}, std::tuple_size{}, x...); + return evaled_args; + } + + Script *script_; + Args args_; }; -template class ScriptStopAction : public Action { +template class ScriptStopAction : public Action { public: - ScriptStopAction(Script *script) : script_(script) {} + ScriptStopAction(C *script) : script_(script) {} void play(Ts... x) override { this->script_->stop(); } protected: - Script *script_; + C *script_; }; -template class IsRunningCondition : public Condition { +template class IsRunningCondition : public Condition { public: - explicit IsRunningCondition(Script *parent) : parent_(parent) {} + explicit IsRunningCondition(C *parent) : parent_(parent) {} bool check(Ts... x) override { return this->parent_->is_running(); } protected: - Script *parent_; + C *parent_; }; -template class ScriptWaitAction : public Action, public Component { +template class ScriptWaitAction : public Action, public Component { public: - ScriptWaitAction(Script *script) : script_(script) {} + ScriptWaitAction(C *script) : script_(script) {} void play_complex(Ts... x) override { this->num_running_++; @@ -137,7 +245,7 @@ template class ScriptWaitAction : public Action, public C } protected: - Script *script_; + C *script_; std::tuple var_{}; }; diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index 66f0fb8c5e..f8a3014a89 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" +#include + namespace esphome { namespace sdm_meter { diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h index 0477cd2a62..730791c91b 100644 --- a/esphome/components/selec_meter/selec_meter.h +++ b/esphome/components/selec_meter/selec_meter.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" +#include + namespace esphome { namespace selec_meter { diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index a1c73c385e..760f7600b7 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -1,9 +1,10 @@ -from typing import List import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_ON_VALUE, CONF_OPTION, @@ -15,6 +16,7 @@ from esphome.const import ( CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] @@ -44,8 +46,6 @@ SELECT_OPERATION_OPTIONS = { "LAST": SelectOperation.SELECT_OP_LAST, } -icon = cv.icon - SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -59,8 +59,32 @@ SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e } ) +_UNDEF = object() -async def setup_select_core_(var, config, *, options: List[str]): + +def select_schema( + class_: MockObjClass = _UNDEF, + *, + entity_category: str = _UNDEF, + icon: str = _UNDEF, +): + schema = cv.Schema({}) + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + return SELECT_SCHEMA.extend(schema) + + +async def setup_select_core_(var, config, *, options: list[str]): await setup_entity(var, config) cg.add(var.traits.set_options(options)) @@ -76,14 +100,14 @@ async def setup_select_core_(var, config, *, options: List[str]): await mqtt.register_mqtt_component(mqtt_, config) -async def register_select(var, config, *, options: List[str]): +async def register_select(var, config, *, options: list[str]): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_select(var)) await setup_select_core_(var, config, options=options) -async def new_select(config, *, options: List[str]): +async def new_select(config, *, options: list[str]): var = cg.new_Pvariable(config[CONF_ID]) await register_select(var, config, options=options) return var diff --git a/esphome/components/sen21231/__init__.py b/esphome/components/sen21231/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp new file mode 100644 index 0000000000..aa123dff62 --- /dev/null +++ b/esphome/components/sen21231/sen21231.cpp @@ -0,0 +1,32 @@ +#include "sen21231.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sen21231_sensor { + +static const char *const TAG = "sen21231_sensor.sensor"; + +void Sen21231Sensor::update() { this->read_data_(); } + +void Sen21231Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "SEN21231:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SEN21231 failed!"); + } + ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? "FAILED" : "OK"); + LOG_UPDATE_INTERVAL(this); +} + +void Sen21231Sensor::read_data_() { + person_sensor_results_t results; + this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); + this->publish_state(results.num_faces); + if (results.num_faces == 1) { + ESP_LOGD(TAG, "SEN21231: is facing towards camera: %d", results.faces[0].is_facing); + } +} + +} // namespace sen21231_sensor +} // namespace esphome diff --git a/esphome/components/sen21231/sen21231.h b/esphome/components/sen21231/sen21231.h new file mode 100644 index 0000000000..b4d540df55 --- /dev/null +++ b/esphome/components/sen21231/sen21231.h @@ -0,0 +1,77 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +// ref: +// https://github.com/usefulsensors/person_sensor_pico_c/blob/main/person_sensor.h + +namespace esphome { +namespace sen21231_sensor { +// The I2C address of the person sensor board. +static const uint8_t PERSON_SENSOR_I2C_ADDRESS = 0x62; +static const uint8_t PERSON_SENSOR_REG_MODE = 0x01; +static const uint8_t PERSON_SENSOR_REG_ENABLE_ID = 0x02; +static const uint8_t PERSON_SENSOR_REG_SINGLE_SHOT = 0x03; +static const uint8_t PERSON_SENSOR_REG_CALIBRATE_ID = 0x04; +static const uint8_t PERSON_SENSOR_REG_PERSIST_IDS = 0x05; +static const uint8_t PERSON_SENSOR_REG_ERASE_IDS = 0x06; +static const uint8_t PERSON_SENSOR_REG_DEBUG_MODE = 0x07; + +static const uint8_t PERSON_SENSOR_MAX_FACES_COUNT = 4; +static const uint8_t PERSON_SENSOR_MAX_IDS_COUNT = 7; + +// The results returned from the sensor have a short header providing +// information about the length of the data packet: +// reserved: Currently unused bytes. +// data_size: Length of the entire packet, excluding the header and +// checksum. +// For version 1.0 of the sensor, this should be 40. +using person_sensor_results_header_t = struct { + uint8_t reserved[2]; // Bytes 0-1. + uint16_t data_size; // Bytes 2-3. +}; + +// Each face found has a set of information associated with it: +// box_confidence: How certain we are we have found a face, from 0 to 255. +// box_left: X coordinate of the left side of the box, from 0 to 255. +// box_top: Y coordinate of the top edge of the box, from 0 to 255. +// box_width: Width of the box, where 255 is the full view port size. +// box_height: Height of the box, where 255 is the full view port size. +// id_confidence: How sure the sensor is about the recognition result. +// id: Numerical ID assigned to this face. +// is_looking_at: Whether the person is facing the camera, 0 or 1. +using person_sensor_face_t = struct __attribute__((__packed__)) { + uint8_t box_confidence; // Byte 1. + uint8_t box_left; // Byte 2. + uint8_t box_top; // Byte 3. + uint8_t box_right; // Byte 4. + uint8_t box_bottom; // Byte 5. + int8_t id_confidence; // Byte 6. + int8_t id; // Byte 7 + uint8_t is_facing; // Byte 8. +}; + +// This is the full structure of the packet returned over the wire from the +// sensor when we do an I2C read from the peripheral address. +// The checksum should be the CRC16 of bytes 0 to 38. You shouldn't need to +// verify this in practice, but we found it useful during our own debugging. +using person_sensor_results_t = struct __attribute__((__packed__)) { + person_sensor_results_header_t header; // Bytes 0-4. + int8_t num_faces; // Byte 5. + person_sensor_face_t faces[PERSON_SENSOR_MAX_FACES_COUNT]; // Bytes 6-37. + uint16_t checksum; // Bytes 38-39. +}; + +class Sen21231Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void update() override; + void dump_config() override; + + protected: + void read_data_(); +}; + +} // namespace sen21231_sensor +} // namespace esphome diff --git a/esphome/components/sen21231/sensor.py b/esphome/components/sen21231/sensor.py new file mode 100644 index 0000000000..fb1dc19278 --- /dev/null +++ b/esphome/components/sen21231/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 ICON_MOTION_SENSOR + +CODEOWNERS = ["@shreyaskarnik"] +DEPENDENCIES = ["i2c"] + +sen21231_sensor_ns = cg.esphome_ns.namespace("sen21231_sensor") +Sen21231Sensor = sen21231_sensor_ns.class_( + "Sen21231Sensor", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema(Sen21231Sensor, icon=ICON_MOTION_SENSOR, accuracy_decimals=1) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x62)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 50b9e01f17..e0504eb2b9 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -1,4 +1,5 @@ #include "senseair.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -42,7 +43,7 @@ void SenseAirComponent::update() { return; } - uint16_t calc_checksum = this->senseair_checksum_(response, 11); + uint16_t calc_checksum = crc16(response, 11); uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11]; if (resp_checksum != calc_checksum) { ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum); @@ -60,23 +61,6 @@ void SenseAirComponent::update() { this->co2_sensor_->publish_state(ppm); } -uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { - uint16_t crc = 0xFFFF; - uint8_t i; - while (length--) { - crc ^= *ptr++; - for (i = 0; i < 8; i++) { - if ((crc & 0x01) != 0) { - crc >>= 1; - crc ^= 0xA001; - } else { - crc >>= 1; - } - } - } - return crc; -} - void SenseAirComponent::background_calibration() { // Responses are just echoes but must be read to clear the buffer ESP_LOGD(TAG, "SenseAir Starting background calibration"); @@ -102,7 +86,7 @@ void SenseAirComponent::background_calibration_result() { } // Check if 5th bit (register CI6) is set - ESP_LOGD(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK", + ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK", response[0], response[1], response[2], response[3], response[4], response[5], response[6]); } diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index c03a0848e9..bcec638f79 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -23,7 +23,6 @@ class SenseAirComponent : public PollingComponent, public uart::UARTDevice { void abc_disable(); protected: - uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); bool senseair_write_command_(const uint8_t *command, uint8_t *response, uint8_t response_length); sensor::Sensor *co2_sensor_{nullptr}; diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index 3f0282a5d4..24b706cf36 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/components/i2c/i2c.h" +#include + namespace esphome { namespace sensirion_common { diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index d6ba038057..f0a58d908c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -25,24 +25,33 @@ from esphome.const import ( CONF_STATE_CLASS, CONF_TO, CONF_TRIGGER_ID, + CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_MQTT_ID, CONF_FORCE_UPDATE, - DEVICE_CLASS_DURATION, - DEVICE_CLASS_EMPTY, + CONF_VALUE, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DATE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, DEVICE_CLASS_NITROGEN_MONOXIDE, @@ -53,14 +62,23 @@ from esphome.const import ( DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRECIPITATION, + DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, + DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_STORAGE, + DEVICE_CLASS_WATER, + DEVICE_CLASS_WEIGHT, + DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass @@ -69,20 +87,27 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - DEVICE_CLASS_EMPTY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DATE, + DEVICE_CLASS_DISTANCE, DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_ENERGY_STORAGE, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, DEVICE_CLASS_NITROGEN_MONOXIDE, @@ -93,14 +118,23 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRECIPITATION, + DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, + DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_STORAGE, + DEVICE_CLASS_WATER, + DEVICE_CLASS_WEIGHT, + DEVICE_CLASS_WIND_SPEED, ] sensor_ns = cg.esphome_ns.namespace("sensor") @@ -166,6 +200,7 @@ SensorPublishAction = sensor_ns.class_("SensorPublishAction", automation.Action) Filter = sensor_ns.class_("Filter") QuantileFilter = sensor_ns.class_("QuantileFilter", Filter) MedianFilter = sensor_ns.class_("MedianFilter", Filter) +SkipInitialFilter = sensor_ns.class_("SkipInitialFilter", Filter) MinFilter = sensor_ns.class_("MinFilter", Filter) MaxFilter = sensor_ns.class_("MaxFilter", Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_( @@ -244,48 +279,24 @@ def sensor_schema( state_class: str = _UNDEF, entity_category: str = _UNDEF, ) -> cv.Schema: - schema = SENSOR_SCHEMA + schema = {} + if class_ is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if unit_of_measurement is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement - ): validate_unit_of_measurement - } - ) - if icon is not _UNDEF: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): validate_icon}) - if accuracy_decimals is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ACCURACY_DECIMALS, default=accuracy_decimals - ): validate_accuracy_decimals, - } - ) - if device_class is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if state_class is not _UNDEF: - schema = schema.extend( - {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} - ) - if entity_category is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - return schema + # Not optional. + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_UNIT_OF_MEASUREMENT, unit_of_measurement, validate_unit_of_measurement), + (CONF_ICON, icon, validate_icon), + (CONF_ACCURACY_DECIMALS, accuracy_decimals, validate_accuracy_decimals), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_STATE_CLASS, state_class, validate_state_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return SENSOR_SCHEMA.extend(schema) @FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_) @@ -361,6 +372,11 @@ MIN_SCHEMA = cv.All( ) +@FILTER_REGISTRY.register("skip_initial", SkipInitialFilter, cv.positive_not_null_int) +async def skip_initial_filter_to_code(config, filter_id): + return cg.new_Pvariable(filter_id, config) + + @FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA) async def min_filter_to_code(config, filter_id): return cg.new_Pvariable( @@ -462,9 +478,38 @@ async def lambda_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, lambda_) -@FILTER_REGISTRY.register("delta", DeltaFilter, cv.float_) +DELTA_SCHEMA = cv.Schema( + { + cv.Required(CONF_VALUE): cv.positive_float, + cv.Optional(CONF_TYPE, default="absolute"): cv.one_of( + "absolute", "percentage", lower=True + ), + } +) + + +def validate_delta(config): + try: + value = cv.positive_float(config) + return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "absolute"}) + except cv.Invalid: + pass + try: + value = cv.percentage(config) + return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "percentage"}) + except cv.Invalid: + pass + raise cv.Invalid("Delta filter requires a positive number or percentage value.") + + +@FILTER_REGISTRY.register("delta", DeltaFilter, cv.Any(DELTA_SCHEMA, validate_delta)) async def delta_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + percentage = config[CONF_TYPE] == "percentage" + return cg.new_Pvariable( + filter_id, + config[CONF_VALUE], + percentage, + ) @FILTER_REGISTRY.register("or", OrFilter, validate_filters) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 7a2c98109c..472649ebdc 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -74,6 +74,19 @@ optional MedianFilter::new_value(float value) { return {}; } +// SkipInitialFilter +SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_to_ignore) {} +optional SkipInitialFilter::new_value(float value) { + if (num_to_ignore_ > 0) { + num_to_ignore_--; + ESP_LOGV(TAG, "SkipInitialFilter(%p)::new_value(%f) SKIPPING, %u left", this, value, num_to_ignore_); + return {}; + } + + ESP_LOGV(TAG, "SkipInitialFilter(%p)::new_value(%f) SENDING", this, value); + return value; +} + // QuantileFilter QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile) : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {} @@ -315,19 +328,23 @@ optional ThrottleFilter::new_value(float value) { } // DeltaFilter -DeltaFilter::DeltaFilter(float min_delta) : min_delta_(min_delta), last_value_(NAN) {} +DeltaFilter::DeltaFilter(float delta, bool percentage_mode) + : delta_(delta), current_delta_(delta), percentage_mode_(percentage_mode), last_value_(NAN) {} optional DeltaFilter::new_value(float value) { if (std::isnan(value)) { if (std::isnan(this->last_value_)) { return {}; } else { + if (this->percentage_mode_) { + this->current_delta_ = fabsf(value * this->delta_); + } return this->last_value_ = value; } } - if (std::isnan(this->last_value_)) { - return this->last_value_ = value; - } - if (fabsf(value - this->last_value_) >= this->min_delta_) { + if (std::isnan(this->last_value_) || fabsf(value - this->last_value_) >= this->current_delta_) { + if (this->percentage_mode_) { + this->current_delta_ = fabsf(value * this->delta_); + } return this->last_value_ = value; } return {}; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 6344d34661..05934a26e8 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,9 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include #include +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace sensor { @@ -101,6 +102,24 @@ class MedianFilter : public Filter { size_t window_size_; }; +/** Simple skip filter. + * + * Skips the first N values, then passes everything else. + */ +class SkipInitialFilter : public Filter { + public: + /** Construct a SkipInitialFilter. + * + * @param num_to_ignore How many values to ignore before the filter becomes a no-op. + */ + explicit SkipInitialFilter(size_t num_to_ignore); + + optional new_value(float value) override; + + protected: + size_t num_to_ignore_; +}; + /** Simple min filter. * * Takes the min of the last values and pushes it out every . @@ -324,12 +343,14 @@ class HeartbeatFilter : public Filter, public Component { class DeltaFilter : public Filter { public: - explicit DeltaFilter(float min_delta); + explicit DeltaFilter(float delta, bool percentage_mode); optional new_value(float value) override; protected: - float min_delta_; + float delta_; + float current_delta_; + bool percentage_mode_; float last_value_{NAN}; }; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index a729791e7e..6ce1e193f5 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -20,54 +20,30 @@ std::string state_class_to_string(StateClass state_class) { } } -Sensor::Sensor(const std::string &name) : EntityBase(name), state(NAN), raw_state(NAN) {} -Sensor::Sensor() : Sensor("") {} +Sensor::Sensor() : state(NAN), raw_state(NAN) {} std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->unit_of_measurement(); -#pragma GCC diagnostic pop + return ""; } void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { this->unit_of_measurement_ = unit_of_measurement; } -std::string Sensor::unit_of_measurement() { return ""; } int8_t Sensor::get_accuracy_decimals() { if (this->accuracy_decimals_.has_value()) return *this->accuracy_decimals_; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->accuracy_decimals(); -#pragma GCC diagnostic pop + return 0; } void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } -int8_t Sensor::accuracy_decimals() { return 0; } - -std::string Sensor::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->device_class(); -#pragma GCC diagnostic pop -} -void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } -std::string Sensor::device_class() { return ""; } void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } StateClass Sensor::get_state_class() { if (this->state_class_.has_value()) return *this->state_class_; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->state_class(); -#pragma GCC diagnostic pop + return StateClass::STATE_CLASS_NONE; } -StateClass Sensor::state_class() { return StateClass::STATE_CLASS_NONE; } void Sensor::publish_state(float state) { this->raw_state = state; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index ba9edd68d0..165d013b2a 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -6,6 +6,8 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/filter.h" +#include + namespace esphome { namespace sensor { @@ -29,6 +31,13 @@ namespace sensor { } \ } +#define SUB_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_ = sensor; } + /** * Sensor state classes */ @@ -45,10 +54,9 @@ std::string state_class_to_string(StateClass state_class); * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. */ -class Sensor : public EntityBase { +class Sensor : public EntityBase, public EntityBase_DeviceClass { public: explicit Sensor(); - explicit Sensor(const std::string &name); /// Get the unit of measurement, using the manual override if set. std::string get_unit_of_measurement(); @@ -60,11 +68,6 @@ class Sensor : public EntityBase { /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); - /// Get the device class, using the manual override if set. - std::string get_device_class(); - /// Manually set the device class. - void set_device_class(const std::string &device_class); - /// Get the state class, using the manual override if set. StateClass get_state_class(); /// Manually set the state class. @@ -141,51 +144,25 @@ class Sensor : public EntityBase { /// Return whether this sensor has gotten a full state (that passed through all filters) yet. bool has_state() const; - /** A unique ID for this sensor, empty for no unique id. See unique ID requirements: - * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements + /** Override this method to set the unique ID of this sensor. * - * @return The unique id as a string. + * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). */ virtual std::string unique_id(); void internal_send_state_to_frontend(float state); protected: - /** Override this to set the default unit of measurement. - * - * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) - */ - virtual std::string unit_of_measurement(); // NOLINT - - /** Override this to set the default accuracy in decimals. - * - * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) - */ - virtual int8_t accuracy_decimals(); // NOLINT - - /** Override this to set the default device class. - * - * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) - */ - virtual std::string device_class(); // NOLINT - - /** Override this to set the default state class. - * - * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) - */ - virtual StateClass state_class(); // NOLINT - CallbackManager raw_callback_; ///< Storage for raw state callbacks. CallbackManager callback_; ///< Storage for filtered state callbacks. - bool has_state_{false}; Filter *filter_list_{nullptr}; ///< Store all active filters. optional unit_of_measurement_; ///< Unit of measurement override optional accuracy_decimals_; ///< Accuracy in decimals override - optional device_class_; ///< Device class override optional state_class_{STATE_CLASS_NONE}; ///< State class override bool force_update_{false}; ///< Force update mode + bool has_state_{false}; }; } // namespace sensor diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 1f6c5006a5..3d24f6c409 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -6,8 +6,7 @@ from esphome.const import ( CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, ICON_RADIATOR, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_AQI, STATE_CLASS_MEASUREMENT, ) @@ -67,13 +66,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOC): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_NITROUS_OXIDE, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 7fe46b9518..52f9adc808 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -234,8 +234,8 @@ bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { response_words = 2; } } - uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); - uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100)); + uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); // first parameter are the relative humidity ticks data[0] = rhticks; // secomd parameter are the temperature ticks diff --git a/esphome/components/shelly_dimmer/dev_table.h b/esphome/components/shelly_dimmer/dev_table.h index f4bf7778f2..e73cd1271c 100644 --- a/esphome/components/shelly_dimmer/dev_table.h +++ b/esphome/components/shelly_dimmer/dev_table.h @@ -155,4 +155,5 @@ constexpr stm32_dev_t DEVICES[] = { } // namespace shelly_dimmer } // namespace esphome -#endif + +#endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index 003498c090..20e0e8156b 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -22,11 +22,12 @@ from esphome.const import ( UNIT_WATT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, ) from esphome.core import HexInt, CORE DOMAIN = "shelly_dimmer" -DEPENDENCIES = ["sensor", "uart"] +DEPENDENCIES = ["sensor", "uart", "esp8266"] shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") ShellyDimmer = shelly_dimmer_ns.class_( @@ -73,7 +74,7 @@ def get_firmware(value): def dl(url): try: - req = requests.get(url) + req = requests.get(url, timeout=30) req.raise_for_status() except requests.exceptions.RequestException as e: raise cv.Invalid(f"Could not download firmware file ({url}): {e}") @@ -169,7 +170,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_CURRENT, accuracy_decimals=2, ), # Change the default gamma_correct setting. diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 94fe836742..144236bfe1 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -1,6 +1,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#ifdef USE_ESP8266 + #include "shelly_dimmer.h" #ifdef USE_SHD_FIRMWARE_DATA #include "stm32flash.h" @@ -521,3 +523,5 @@ void ShellyDimmer::reset_dfu_boot_() { } // namespace shelly_dimmer } // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h index b7d476279e..4701f3a32a 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.h +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ESP8266 + #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/components/light/light_output.h" @@ -115,3 +117,5 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public } // namespace shelly_dimmer } // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index e688f2de36..3871d89a2f 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -613,8 +613,9 @@ stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const } } } - if (new_cmds) + if (new_cmds) { ESP_LOGD(TAG, ")"); + } if (stm32_get_ack(stm) != STM32_ERR_OK) { return make_stm32_with_deletor(nullptr); } @@ -1061,4 +1062,5 @@ stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uin } // namespace shelly_dimmer } // namespace esphome -#endif + +#endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 3164aa0687..41ca3c5d6e 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -19,8 +19,8 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void update() override; protected: - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace sht3xd diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index 9fb8fc969e..e195bb9acc 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -98,7 +98,6 @@ async def to_code(config): cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY])) for key, funcName in TYPES.items(): - if key in config: sens = await sensor.new_sensor(config[key]) cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index bdc3e62d2f..0f9123434d 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -19,7 +19,7 @@ void SHT4XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up sht4x..."); if (this->duty_cycle_ > 0.0) { - uint32_t heater_interval = (uint32_t)(this->heater_time_ / this->duty_cycle_); + uint32_t heater_interval = (uint32_t) (this->heater_time_ / this->duty_cycle_); ESP_LOGD(TAG, "Heater interval: %i", heater_interval); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index c44fb9d9c1..084d3bfc35 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -26,8 +26,8 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri protected: SHTCXType type_; uint16_t sensor_id_; - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace shtcx diff --git a/esphome/components/shutdown/button/__init__.py b/esphome/components/shutdown/button/__init__.py index 51cd6d6da2..79d0b23935 100644 --- a/esphome/components/shutdown/button/__init__.py +++ b/esphome/components/shutdown/button/__init__.py @@ -10,11 +10,9 @@ from esphome.const import ( shutdown_ns = cg.esphome_ns.namespace("shutdown") ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component) -CONFIG_SCHEMA = ( - button.button_schema(entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER) - .extend({cv.GenerateID(): cv.declare_id(ShutdownButton)}) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = button.button_schema( + ShutdownButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/sigma_delta_output/__init__.py b/esphome/components/sigma_delta_output/__init__.py new file mode 100644 index 0000000000..3356e61bb2 --- /dev/null +++ b/esphome/components/sigma_delta_output/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/sigma_delta_output/output.py b/esphome/components/sigma_delta_output/output.py new file mode 100644 index 0000000000..49ac9e685a --- /dev/null +++ b/esphome/components/sigma_delta_output/output.py @@ -0,0 +1,66 @@ +from esphome import automation, pins +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_TURN_ON_ACTION, + CONF_TURN_OFF_ACTION, +) + +DEPENDENCIES = [] + + +sigma_delta_output_ns = cg.esphome_ns.namespace("sigma_delta_output") +SigmaDeltaOutput = sigma_delta_output_ns.class_( + "SigmaDeltaOutput", output.FloatOutput, cg.PollingComponent +) + +CONF_STATE_CHANGE_ACTION = "state_change_action" + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend(cv.polling_component_schema("60s")).extend( + { + cv.Required(CONF_ID): cv.declare_id(SigmaDeltaOutput), + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( + single=True + ), + cv.Inclusive( + CONF_TURN_ON_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Inclusive( + CONF_TURN_OFF_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + if CONF_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + if CONF_STATE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_state_change_trigger(), + [(bool, "state")], + config[CONF_STATE_CHANGE_ACTION], + ) + if CONF_TURN_ON_ACTION in config: + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h new file mode 100644 index 0000000000..5a5acd2dfb --- /dev/null +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -0,0 +1,66 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sigma_delta_output { +class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { + public: + Trigger<> *get_turn_on_trigger() { + if (!this->turn_on_trigger_) + this->turn_on_trigger_ = make_unique>(); + return this->turn_on_trigger_.get(); + } + Trigger<> *get_turn_off_trigger() { + if (!this->turn_off_trigger_) + this->turn_off_trigger_ = make_unique>(); + return this->turn_off_trigger_.get(); + } + + Trigger *get_state_change_trigger() { + if (!this->state_change_trigger_) + this->state_change_trigger_ = make_unique>(); + return this->state_change_trigger_.get(); + } + + void set_pin(GPIOPin *pin) { this->pin_ = pin; }; + void write_state(float state) override { this->state_ = state; } + void update() override { + this->accum_ += this->state_; + const bool next_value = this->accum_ > 0; + + if (next_value) { + this->accum_ -= 1.; + } + + if (next_value != this->value_) { + this->value_ = next_value; + if (this->pin_) { + this->pin_->digital_write(next_value); + } + + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(next_value); + } + + if (next_value && this->turn_on_trigger_) { + this->turn_on_trigger_->trigger(); + } else if (!next_value && this->turn_off_trigger_) { + this->turn_off_trigger_->trigger(); + } + } + } + + protected: + GPIOPin *pin_{nullptr}; + + std::unique_ptr> turn_on_trigger_{nullptr}; + std::unique_ptr> turn_off_trigger_{nullptr}; + std::unique_ptr> state_change_trigger_{nullptr}; + + float accum_{0}; + float state_{0.}; + bool value_{false}; +}; +} // namespace sigma_delta_output +} // namespace esphome diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 564b685b37..698e3cda9e 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -18,15 +18,42 @@ Sim800LReceivedMessageTrigger = sim800l_ns.class_( "Sim800LReceivedMessageTrigger", automation.Trigger.template(cg.std_string, cg.std_string), ) +Sim800LIncomingCallTrigger = sim800l_ns.class_( + "Sim800LIncomingCallTrigger", + automation.Trigger.template(cg.std_string), +) +Sim800LCallConnectedTrigger = sim800l_ns.class_( + "Sim800LCallConnectedTrigger", + automation.Trigger.template(), +) +Sim800LCallDisconnectedTrigger = sim800l_ns.class_( + "Sim800LCallDisconnectedTrigger", + automation.Trigger.template(), +) + +Sim800LReceivedUssdTrigger = sim800l_ns.class_( + "Sim800LReceivedUssdTrigger", + automation.Trigger.template(cg.std_string), +) # Actions Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) +Sim800LSendUssdAction = sim800l_ns.class_("Sim800LSendUssdAction", automation.Action) Sim800LDialAction = sim800l_ns.class_("Sim800LDialAction", automation.Action) +Sim800LConnectAction = sim800l_ns.class_("Sim800LConnectAction", automation.Action) +Sim800LDisconnectAction = sim800l_ns.class_( + "Sim800LDisconnectAction", automation.Action +) CONF_SIM800L_ID = "sim800l_id" CONF_ON_SMS_RECEIVED = "on_sms_received" +CONF_ON_USSD_RECEIVED = "on_ussd_received" +CONF_ON_INCOMING_CALL = "on_incoming_call" +CONF_ON_CALL_CONNECTED = "on_call_connected" +CONF_ON_CALL_DISCONNECTED = "on_call_disconnected" CONF_RECIPIENT = "recipient" CONF_MESSAGE = "message" +CONF_USSD = "ussd" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -39,6 +66,34 @@ CONFIG_SCHEMA = cv.All( ), } ), + cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Sim800LIncomingCallTrigger + ), + } + ), + cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Sim800LCallConnectedTrigger + ), + } + ), + cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Sim800LCallDisconnectedTrigger + ), + } + ), + cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Sim800LReceivedUssdTrigger + ), + } + ), } ) .extend(cv.polling_component_schema("5s")) @@ -59,6 +114,19 @@ async def to_code(config): await automation.build_automation( trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf ) + for conf in config.get(CONF_ON_INCOMING_CALL, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "caller_id")], conf) + for conf in config.get(CONF_ON_CALL_CONNECTED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CALL_DISCONNECTED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_USSD_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "ussd")], conf) SIM800L_SEND_SMS_SCHEMA = cv.Schema( @@ -98,3 +166,44 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_RECIPIENT], args, cg.std_string) cg.add(var.set_recipient(template_)) return var + + +@automation.register_action( + "sim800l.connect", + Sim800LConnectAction, + cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), +) +async def sim800l_connect_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +SIM800L_SEND_USSD_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_USSD): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "sim800l.send_ussd", Sim800LSendUssdAction, SIM800L_SEND_USSD_SCHEMA +) +async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_USSD], args, cg.std_string) + cg.add(var.set_ussd(template_)) + return var + + +@automation.register_action( + "sim800l.disconnect", + Sim800LDisconnectAction, + cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), +) +async def sim800l_disconnect_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index a935978747..4f7aa228e9 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -16,20 +16,38 @@ void Sim800LComponent::update() { this->write(26); } + if (this->expect_ack_) + return; + if (state_ == STATE_INIT) { if (this->registered_ && this->send_pending_) { this->send_cmd_("AT+CSCS=\"GSM\""); - this->state_ = STATE_SENDINGSMS1; + this->state_ = STATE_SENDING_SMS_1; } else if (this->registered_ && this->dial_pending_) { this->send_cmd_("AT+CSCS=\"GSM\""); this->state_ = STATE_DIALING1; + } else if (this->registered_ && this->connect_pending_) { + this->connect_pending_ = false; + ESP_LOGI(TAG, "Connecting..."); + this->send_cmd_("ATA"); + this->state_ = STATE_ATA_SENT; + } else if (this->registered_ && this->send_ussd_pending_) { + this->send_cmd_("AT+CSCS=\"GSM\""); + this->state_ = STATE_SEND_USSD1; + } else if (this->registered_ && this->disconnect_pending_) { + this->disconnect_pending_ = false; + ESP_LOGI(TAG, "Disconnecting..."); + this->send_cmd_("ATH"); + } else if (this->registered_ && this->call_state_ != 6) { + send_cmd_("AT+CLCC"); + this->state_ = STATE_CHECK_CALL; + return; } else { this->send_cmd_("AT"); - this->state_ = STATE_CHECK_AT; + this->state_ = STATE_SETUP_CMGF; } this->expect_ack_ = true; - } - if (state_ == STATE_RECEIVEDSMS) { + } else if (state_ == STATE_RECEIVED_SMS) { // Serial Buffer should have flushed. // Send cmd to delete received sms char delete_cmd[20]; @@ -49,16 +67,29 @@ void Sim800LComponent::send_cmd_(const std::string &message) { } void Sim800LComponent::parse_cmd_(std::string message) { - ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_); - if (message.empty()) return; + ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_); + + if (this->state_ != STATE_RECEIVE_SMS) { + if (message == "RING") { + // Incoming call... + this->state_ = STATE_PARSE_CLIP; + this->expect_ack_ = false; + } else if (message == "NO CARRIER") { + if (this->call_state_ != 6) { + this->call_state_ = 6; + this->call_disconnected_callback_.call(); + } + } + } + + bool ok = message == "OK"; if (this->expect_ack_) { - bool ok = message == "OK"; this->expect_ack_ = false; if (!ok) { - if (this->state_ == STATE_CHECK_AT && message == "AT") { + if (this->state_ == STATE_SETUP_CMGF && message == "AT") { // Expected ack but AT echo received this->state_ = STATE_DISABLE_ECHO; this->expect_ack_ = true; @@ -68,6 +99,10 @@ void Sim800LComponent::parse_cmd_(std::string message) { return; } } + } else if (ok && (this->state_ != STATE_PARSE_SMS_RESPONSE && this->state_ != STATE_CHECK_CALL && + this->state_ != STATE_RECEIVE_SMS && this->state_ != STATE_DIALING2)) { + ESP_LOGW(TAG, "Received unexpected OK. Ignoring"); + return; } switch (this->state_) { @@ -75,36 +110,96 @@ void Sim800LComponent::parse_cmd_(std::string message) { // While we were waiting for update to check for messages, this notifies a message // is available. bool message_available = message.compare(0, 6, "+CMTI:") == 0; - if (!message_available) + if (!message_available) { + if (message == "RING") { + // Incoming call... + this->state_ = STATE_PARSE_CLIP; + } else if (message == "NO CARRIER") { + if (this->call_state_ != 6) { + this->call_state_ = 6; + this->call_disconnected_callback_.call(); + } + } else if (message.compare(0, 6, "+CUSD:") == 0) { + // Incoming USSD MESSAGE + this->state_ = STATE_CHECK_USSD; + } break; + } + // Else fall thru ... } case STATE_CHECK_SMS: send_cmd_("AT+CMGL=\"ALL\""); - this->state_ = STATE_PARSE_SMS; + this->state_ = STATE_PARSE_SMS_RESPONSE; this->parse_index_ = 0; break; case STATE_DISABLE_ECHO: send_cmd_("ATE0"); - this->state_ = STATE_CHECK_AT; + this->state_ = STATE_SETUP_CMGF; this->expect_ack_ = true; break; - case STATE_CHECK_AT: + case STATE_SETUP_CMGF: send_cmd_("AT+CMGF=1"); + this->state_ = STATE_SETUP_CLIP; + this->expect_ack_ = true; + break; + case STATE_SETUP_CLIP: + send_cmd_("AT+CLIP=1"); this->state_ = STATE_CREG; this->expect_ack_ = true; break; + case STATE_SETUP_USSD: + send_cmd_("AT+CUSD=1"); + this->state_ = STATE_CREG; + this->expect_ack_ = true; + break; + case STATE_SEND_USSD1: + this->send_cmd_("AT+CUSD=1, \"" + this->ussd_ + "\""); + this->state_ = STATE_SEND_USSD2; + this->expect_ack_ = true; + break; + case STATE_SEND_USSD2: + ESP_LOGD(TAG, "SendUssd2: '%s'", message.c_str()); + if (message == "OK") { + // Dialing + ESP_LOGD(TAG, "Dialing ussd code: '%s' done.", this->ussd_.c_str()); + this->state_ = STATE_CHECK_USSD; + this->send_ussd_pending_ = false; + } else { + this->set_registered_(false); + this->state_ = STATE_INIT; + this->send_cmd_("AT+CMEE=2"); + this->write(26); + } + break; + case STATE_CHECK_USSD: + ESP_LOGD(TAG, "Check ussd code: '%s'", message.c_str()); + if (message.compare(0, 6, "+CUSD:") == 0) { + this->state_ = STATE_RECEIVED_USSD; + this->ussd_ = ""; + size_t start = 10; + size_t end = message.find_last_of(','); + if (end > start) { + this->ussd_ = message.substr(start + 1, end - start - 2); + this->ussd_received_callback_.call(this->ussd_); + } + } + // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS + if (message == "OK") + this->state_ = STATE_INIT; + break; case STATE_CREG: send_cmd_("AT+CREG?"); - this->state_ = STATE_CREGWAIT; + this->state_ = STATE_CREG_WAIT; break; - case STATE_CREGWAIT: { + case STATE_CREG_WAIT: { // 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' || message[9] == '5'); if (registered) { - if (!this->registered_) + if (!this->registered_) { ESP_LOGD(TAG, "Registered OK"); + } this->state_ = STATE_CSQ; this->expect_ack_ = true; } else { @@ -112,10 +207,10 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (message[7] == '0') { // Network registration is disable, enable it send_cmd_("AT+CREG=1"); this->expect_ack_ = true; - this->state_ = STATE_CHECK_AT; + this->state_ = STATE_SETUP_CMGF; } else { // Keep waiting registration - this->state_ = STATE_CREG; + this->state_ = STATE_INIT; } } set_registered_(registered); @@ -145,9 +240,6 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->expect_ack_ = true; this->state_ = STATE_CHECK_SMS; break; - case STATE_PARSE_SMS: - this->state_ = STATE_PARSE_SMS_RESPONSE; - break; case STATE_PARSE_SMS_RESPONSE: if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) { size_t start = 7; @@ -158,10 +250,11 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (item == 1) { // Slot Index this->parse_index_ = parse_number(message.substr(start, end - start)).value_or(0); } - // item 2 = STATUS, usually "REC UNERAD" + // item 2 = STATUS, usually "REC UNREAD" if (item == 3) { // recipient // Add 1 and remove 2 from substring to get rid of "quotes" this->sender_ = message.substr(start + 1, end - start - 2); + this->message_.clear(); break; } // item 4 = "" @@ -174,42 +267,83 @@ void Sim800LComponent::parse_cmd_(std::string message) { ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str()); return; } - this->state_ = STATE_RECEIVESMS; + this->state_ = STATE_RECEIVE_SMS; + } + // Otherwise we receive another OK + if (ok) { + send_cmd_("AT+CLCC"); + this->state_ = STATE_CHECK_CALL; } - // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS - if (message == "OK") - this->state_ = STATE_INIT; break; - case STATE_RECEIVESMS: + case STATE_CHECK_CALL: + if (message.compare(0, 6, "+CLCC:") == 0 && this->parse_index_ == 0) { + this->expect_ack_ = true; + size_t start = 7; + size_t end = message.find(',', start); + uint8_t item = 0; + while (end != start) { + item++; + // item 1 call index for +CHLD + // item 2 dir 0 Mobile originated; 1 Mobile terminated + if (item == 3) { // stat + uint8_t current_call_state = parse_number(message.substr(start, end - start)).value_or(6); + if (current_call_state != this->call_state_) { + ESP_LOGD(TAG, "Call state is now: %d", current_call_state); + if (current_call_state == 0) + this->call_connected_callback_.call(); + } + this->call_state_ = current_call_state; + break; + } + // item 4 = "" + // item 5 = Received timestamp + start = end + 1; + end = message.find(',', start); + } + + if (item < 2) { + ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str()); + return; + } + } else if (ok) { + if (this->call_state_ != 6) { + // no call in progress + this->call_state_ = 6; // Disconnect + this->call_disconnected_callback_.call(); + } + } + this->state_ = STATE_INIT; + break; + case STATE_RECEIVE_SMS: /* Our recipient is set and the message body is in message kick ESPHome callback now */ - ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); - ESP_LOGD(TAG, "%s", message.c_str()); - this->callback_.call(message, this->sender_); - /* If the message is multiline, next lines will contain message data. - If there were other messages in the list, next line will be +CMGL: ... - At the end of the list the new line and the OK should be received. - To keep this simple just first line of message if considered, then - the next state will swallow all received data and in next poll event - this message index is marked for deletion. - */ - this->state_ = STATE_RECEIVEDSMS; + if (ok || message.compare(0, 6, "+CMGL:") == 0) { + ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); + ESP_LOGD(TAG, "%s", this->message_.c_str()); + this->sms_received_callback_.call(this->message_, this->sender_); + this->state_ = STATE_RECEIVED_SMS; + } else { + if (this->message_.length() > 0) + this->message_ += "\n"; + this->message_ += message; + } break; - case STATE_RECEIVEDSMS: + case STATE_RECEIVED_SMS: + case STATE_RECEIVED_USSD: // Let the buffer flush. Next poll will request to delete the parsed index message. break; - case STATE_SENDINGSMS1: + case STATE_SENDING_SMS_1: this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\""); - this->state_ = STATE_SENDINGSMS2; + this->state_ = STATE_SENDING_SMS_2; break; - case STATE_SENDINGSMS2: + case STATE_SENDING_SMS_2: if (message == ">") { // Send sms body - ESP_LOGD(TAG, "Sending message: '%s'", this->outgoing_message_.c_str()); + ESP_LOGI(TAG, "Sending to %s message: '%s'", this->recipient_.c_str(), this->outgoing_message_.c_str()); this->write_str(this->outgoing_message_.c_str()); this->write(26); - this->state_ = STATE_SENDINGSMS3; + this->state_ = STATE_SENDING_SMS_3; } else { set_registered_(false); this->state_ = STATE_INIT; @@ -217,7 +351,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->write(26); } break; - case STATE_SENDINGSMS3: + case STATE_SENDING_SMS_3: if (message.compare(0, 6, "+CMGS:") == 0) { ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str()); this->send_pending_ = false; @@ -230,23 +364,55 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_DIALING2; break; case STATE_DIALING2: - if (message == "OK") { - // Dialing - ESP_LOGD(TAG, "Dialing: '%s'", this->recipient_.c_str()); - this->state_ = STATE_INIT; + if (ok) { + ESP_LOGI(TAG, "Dialing: '%s'", this->recipient_.c_str()); this->dial_pending_ = false; } else { this->set_registered_(false); - this->state_ = STATE_INIT; this->send_cmd_("AT+CMEE=2"); this->write(26); } + this->state_ = STATE_INIT; + break; + case STATE_PARSE_CLIP: + if (message.compare(0, 6, "+CLIP:") == 0) { + std::string caller_id; + size_t start = 7; + size_t end = message.find(',', start); + uint8_t item = 0; + while (end != start) { + item++; + if (item == 1) { // Slot Index + // Add 1 and remove 2 from substring to get rid of "quotes" + caller_id = message.substr(start + 1, end - start - 2); + break; + } + // item 4 = "" + // item 5 = Received timestamp + start = end + 1; + end = message.find(',', start); + } + if (this->call_state_ != 4) { + this->call_state_ = 4; + ESP_LOGI(TAG, "Incoming call from %s", caller_id.c_str()); + incoming_call_callback_.call(caller_id); + } + this->state_ = STATE_INIT; + } + break; + case STATE_ATA_SENT: + ESP_LOGI(TAG, "Call connected"); + if (this->call_state_ != 0) { + this->call_state_ = 0; + this->call_connected_callback_.call(); + } + this->state_ = STATE_INIT; break; default: - ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); + ESP_LOGW(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); break; } -} +} // namespace sim800l void Sim800LComponent::loop() { // Read message @@ -265,7 +431,7 @@ void Sim800LComponent::loop() { byte = '?'; // need to be valid utf8 string for log functions. this->read_buffer_[this->read_pos_] = byte; - if (this->state_ == STATE_SENDINGSMS2 && this->read_pos_ == 0 && byte == '>') + if (this->state_ == STATE_SENDING_SMS_2 && this->read_pos_ == 0 && byte == '>') this->read_buffer_[++this->read_pos_] = ASCII_LF; if (this->read_buffer_[this->read_pos_] == ASCII_LF) { @@ -276,13 +442,23 @@ void Sim800LComponent::loop() { this->read_pos_++; } } + if (state_ == STATE_INIT && this->registered_ && + (this->call_state_ != 6 // A call is in progress + || this->send_pending_ || this->dial_pending_ || this->connect_pending_ || this->disconnect_pending_)) { + this->update(); + } } void Sim800LComponent::send_sms(const std::string &recipient, const std::string &message) { - ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str()); this->recipient_ = recipient; this->outgoing_message_ = message; this->send_pending_ = true; +} + +void Sim800LComponent::send_ussd(const std::string &ussd_code) { + ESP_LOGD(TAG, "Sending USSD code: %s", ussd_code.c_str()); + this->ussd_ = ussd_code; + this->send_ussd_pending_ = true; this->update(); } void Sim800LComponent::dump_config() { @@ -295,11 +471,11 @@ void Sim800LComponent::dump_config() { #endif } void Sim800LComponent::dial(const std::string &recipient) { - ESP_LOGD(TAG, "Dialing %s", recipient.c_str()); this->recipient_ = recipient; this->dial_pending_ = true; - this->update(); } +void Sim800LComponent::connect() { this->connect_pending_ = true; } +void Sim800LComponent::disconnect() { this->disconnect_pending_ = true; } void Sim800LComponent::set_registered_(bool registered) { this->registered_ = registered; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 3535b96283..bf7efd6915 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -16,31 +16,35 @@ namespace esphome { namespace sim800l { -const uint8_t SIM800L_READ_BUFFER_LENGTH = 255; +const uint16_t SIM800L_READ_BUFFER_LENGTH = 1024; enum State { STATE_IDLE = 0, STATE_INIT, - STATE_CHECK_AT, + STATE_SETUP_CMGF, + STATE_SETUP_CLIP, STATE_CREG, - STATE_CREGWAIT, + STATE_CREG_WAIT, STATE_CSQ, STATE_CSQ_RESPONSE, - STATE_IDLEWAIT, - STATE_SENDINGSMS1, - STATE_SENDINGSMS2, - STATE_SENDINGSMS3, + STATE_SENDING_SMS_1, + STATE_SENDING_SMS_2, + STATE_SENDING_SMS_3, STATE_CHECK_SMS, - STATE_PARSE_SMS, STATE_PARSE_SMS_RESPONSE, - STATE_RECEIVESMS, - STATE_READSMS, - STATE_RECEIVEDSMS, - STATE_DELETEDSMS, + STATE_RECEIVE_SMS, + STATE_RECEIVED_SMS, STATE_DISABLE_ECHO, - STATE_PARSE_SMS_OK, STATE_DIALING1, - STATE_DIALING2 + STATE_DIALING2, + STATE_PARSE_CLIP, + STATE_ATA_SENT, + STATE_CHECK_CALL, + STATE_SETUP_USSD, + STATE_SEND_USSD1, + STATE_SEND_USSD2, + STATE_CHECK_USSD, + STATE_RECEIVED_USSD }; class Sim800LComponent : public uart::UARTDevice, public PollingComponent { @@ -58,10 +62,25 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { void set_rssi_sensor(sensor::Sensor *rssi_sensor) { rssi_sensor_ = rssi_sensor; } #endif void add_on_sms_received_callback(std::function callback) { - this->callback_.add(std::move(callback)); + this->sms_received_callback_.add(std::move(callback)); + } + void add_on_incoming_call_callback(std::function callback) { + this->incoming_call_callback_.add(std::move(callback)); + } + void add_on_call_connected_callback(std::function callback) { + this->call_connected_callback_.add(std::move(callback)); + } + void add_on_call_disconnected_callback(std::function callback) { + this->call_disconnected_callback_.add(std::move(callback)); + } + void add_on_ussd_received_callback(std::function callback) { + this->ussd_received_callback_.add(std::move(callback)); } void send_sms(const std::string &recipient, const std::string &message); + void send_ussd(const std::string &ussd_code); void dial(const std::string &recipient); + void connect(); + void disconnect(); protected: void send_cmd_(const std::string &message); @@ -76,6 +95,7 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { sensor::Sensor *rssi_sensor_{nullptr}; #endif std::string sender_; + std::string message_; char read_buffer_[SIM800L_READ_BUFFER_LENGTH]; size_t read_pos_{0}; uint8_t parse_index_{0}; @@ -86,10 +106,19 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; + std::string ussd_; bool send_pending_; bool dial_pending_; + bool connect_pending_; + bool disconnect_pending_; + bool send_ussd_pending_; + uint8_t call_state_{6}; - CallbackManager callback_; + CallbackManager sms_received_callback_; + CallbackManager incoming_call_callback_; + CallbackManager call_connected_callback_; + CallbackManager call_disconnected_callback_; + CallbackManager ussd_received_callback_; }; class Sim800LReceivedMessageTrigger : public Trigger { @@ -100,6 +129,33 @@ class Sim800LReceivedMessageTrigger : public Trigger { } }; +class Sim800LIncomingCallTrigger : public Trigger { + public: + explicit Sim800LIncomingCallTrigger(Sim800LComponent *parent) { + parent->add_on_incoming_call_callback([this](const std::string &caller_id) { this->trigger(caller_id); }); + } +}; + +class Sim800LCallConnectedTrigger : public Trigger<> { + public: + explicit Sim800LCallConnectedTrigger(Sim800LComponent *parent) { + parent->add_on_call_connected_callback([this]() { this->trigger(); }); + } +}; + +class Sim800LCallDisconnectedTrigger : public Trigger<> { + public: + explicit Sim800LCallDisconnectedTrigger(Sim800LComponent *parent) { + parent->add_on_call_disconnected_callback([this]() { this->trigger(); }); + } +}; +class Sim800LReceivedUssdTrigger : public Trigger { + public: + explicit Sim800LReceivedUssdTrigger(Sim800LComponent *parent) { + parent->add_on_ussd_received_callback([this](const std::string &ussd) { this->trigger(ussd); }); + } +}; + template class Sim800LSendSmsAction : public Action { public: Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} @@ -116,6 +172,20 @@ template class Sim800LSendSmsAction : public Action { Sim800LComponent *parent_; }; +template class Sim800LSendUssdAction : public Action { + public: + Sim800LSendUssdAction(Sim800LComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, ussd) + + void play(Ts... x) { + auto ussd_code = this->ussd_.value(x...); + this->parent_->send_ussd(ussd_code); + } + + protected: + Sim800LComponent *parent_; +}; + template class Sim800LDialAction : public Action { public: Sim800LDialAction(Sim800LComponent *parent) : parent_(parent) {} @@ -129,6 +199,25 @@ template class Sim800LDialAction : public Action { protected: Sim800LComponent *parent_; }; +template class Sim800LConnectAction : public Action { + public: + Sim800LConnectAction(Sim800LComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->connect(); } + + protected: + Sim800LComponent *parent_; +}; + +template class Sim800LDisconnectAction : public Action { + public: + Sim800LDisconnectAction(Sim800LComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->disconnect(); } + + protected: + Sim800LComponent *parent_; +}; } // namespace sim800l } // namespace esphome diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 6af0283483..d6b2cdfe12 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -53,12 +53,15 @@ void SlowPWMOutput::loop() { void SlowPWMOutput::dump_config() { ESP_LOGCONFIG(TAG, "Slow PWM Output:"); LOG_PIN(" Pin: ", this->pin_); - if (this->state_change_trigger_) + if (this->state_change_trigger_) { ESP_LOGCONFIG(TAG, " State change automation configured"); - if (this->turn_on_trigger_) + } + if (this->turn_on_trigger_) { ESP_LOGCONFIG(TAG, " Turn on automation configured"); - if (this->turn_off_trigger_) + } + if (this->turn_off_trigger_) { ESP_LOGCONFIG(TAG, " Turn off automation configured"); + } ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); ESP_LOGCONFIG(TAG, " Restart cycle on state change: %s", YESNO(this->restart_cycle_on_state_change_)); LOG_FLOAT_OUTPUT(this); @@ -67,7 +70,7 @@ void SlowPWMOutput::dump_config() { void SlowPWMOutput::write_state(float state) { this->state_ = state; if (this->restart_cycle_on_state_change_) - this->period_start_time_ = millis(); + this->restart_cycle(); } } // namespace slow_pwm diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index be45736864..3e5a3e2a40 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -14,6 +14,7 @@ class SlowPWMOutput : public output::FloatOutput, public Component { void set_restart_cycle_on_state_change(bool restart_cycle_on_state_change) { restart_cycle_on_state_change_ = restart_cycle_on_state_change; } + void restart_cycle() { this->period_start_time_ = millis(); } /// Initialize pin void setup() override; diff --git a/esphome/components/sm10bit_base/__init__.py b/esphome/components/sm10bit_base/__init__.py new file mode 100644 index 0000000000..8722bd35a9 --- /dev/null +++ b/esphome/components/sm10bit_base/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" +CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels" + +sm10bit_base_ns = cg.esphome_ns.namespace("sm10bit_base") +Sm10BitBase = sm10bit_base_ns.class_("Sm10BitBase", cg.Component) + +SM10BIT_BASE_CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range( + min=0, max=15 + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def register_sm10bit_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS])) + cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS])) + + return var diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp new file mode 100644 index 0000000000..9c7abb48e2 --- /dev/null +++ b/esphome/components/sm10bit_base/sm10bit_base.cpp @@ -0,0 +1,112 @@ +#include "sm10bit_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm10bit_base { + +static const char *const TAG = "sm10bit_base"; + +static const uint8_t SM10BIT_ADDR_STANDBY = 0x0; +static const uint8_t SM10BIT_ADDR_START_3CH = 0x8; +static const uint8_t SM10BIT_ADDR_START_2CH = 0x10; +static const uint8_t SM10BIT_ADDR_START_5CH = 0x18; + +// Power current values +// HEX | Binary | RGB level | White level | Config value +// 0x0 | 0000 | RGB 10mA | CW 5mA | 0 +// 0x1 | 0001 | RGB 20mA | CW 10mA | 1 +// 0x2 | 0010 | RGB 30mA | CW 15mA | 2 - Default spec color value +// 0x3 | 0011 | RGB 40mA | CW 20mA | 3 +// 0x4 | 0100 | RGB 50mA | CW 25mA | 4 - Default spec white value +// 0x5 | 0101 | RGB 60mA | CW 30mA | 5 +// 0x6 | 0110 | RGB 70mA | CW 35mA | 6 +// 0x7 | 0111 | RGB 80mA | CW 40mA | 7 +// 0x8 | 1000 | RGB 90mA | CW 45mA | 8 +// 0x9 | 1001 | RGB 100mA | CW 50mA | 9 +// 0xA | 1010 | RGB 110mA | CW 55mA | 10 +// 0xB | 1011 | RGB 120mA | CW 60mA | 11 +// 0xC | 1100 | RGB 130mA | CW 65mA | 12 +// 0xD | 1101 | RGB 140mA | CW 70mA | 13 +// 0xE | 1110 | RGB 150mA | CW 75mA | 14 +// 0xF | 1111 | RGB 160mA | CW 80mA | 15 + +void Sm10BitBase::loop() { + if (!this->update_) + return; + + uint8_t data[12]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; + for (int i = 1; i < 12; i++) + data[i] = 0; + this->write_buffer_(data, 12); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_2CH; + data[1] = 0 << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_3CH; + data[1] = this->max_power_color_channels_ << 4 | 0; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } else { + // All channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH; + data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } + + this->update_ = false; +} + +void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} +void Sm10BitBase::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void Sm10BitBase::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace sm10bit_base +} // namespace esphome diff --git a/esphome/components/sm10bit_base/sm10bit_base.h b/esphome/components/sm10bit_base/sm10bit_base.h new file mode 100644 index 0000000000..c8e92e352f --- /dev/null +++ b/esphome/components/sm10bit_base/sm10bit_base.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace sm10bit_base { + +class Sm10BitBase : public Component { + public: + class Channel; + + void set_model(uint8_t model_id) { model_id_ = model_id; } + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_max_power_color_channels(uint8_t max_power_color_channels) { + max_power_color_channels_ = max_power_color_channels; + } + void set_max_power_white_channels(uint8_t max_power_white_channels) { + max_power_white_channels_ = max_power_white_channels; + } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void setup() override; + void dump_config() override; + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(Sm10BitBase *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + Sm10BitBase *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint16_t value); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t model_id_; + uint8_t max_power_color_channels_{2}; + uint8_t max_power_white_channels_{4}; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm10bit_base +} // namespace esphome diff --git a/esphome/components/sm2235/__init__.py b/esphome/components/sm2235/__init__.py new file mode 100644 index 0000000000..ae6cb336ad --- /dev/null +++ b/esphome/components/sm2235/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sm10bit_base + +AUTO_LOAD = ["sm10bit_base", "output"] +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +sm2235_ns = cg.esphome_ns.namespace("sm2235") + +SM2235 = sm2235_ns.class_("SM2235", sm10bit_base.Sm10BitBase) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2235), + } +).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA) + + +async def to_code(config): + var = await sm10bit_base.register_sm10bit_base(config) + cg.add(var.set_model(0xC0)) diff --git a/esphome/components/sm2235/output.py b/esphome/components/sm2235/output.py new file mode 100644 index 0000000000..c4f63b451a --- /dev/null +++ b/esphome/components/sm2235/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM2235 + +DEPENDENCIES = ["sm2235"] +CODEOWNERS = ["@Cossid"] + +Channel = SM2235.class_("Channel", output.FloatOutput) + +CONF_SM2235_ID = "sm2235_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_SM2235_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp new file mode 100644 index 0000000000..f953d41957 --- /dev/null +++ b/esphome/components/sm2235/sm2235.cpp @@ -0,0 +1,27 @@ +#include "sm2235.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm2235 { + +static const char *const TAG = "sm2235"; + +void SM2235::setup() { + ESP_LOGCONFIG(TAG, "Setting up sm2235 Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} + +void SM2235::dump_config() { + ESP_LOGCONFIG(TAG, "sm2235:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +} // namespace sm2235 +} // namespace esphome diff --git a/esphome/components/sm2235/sm2235.h b/esphome/components/sm2235/sm2235.h new file mode 100644 index 0000000000..56d1782055 --- /dev/null +++ b/esphome/components/sm2235/sm2235.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sm10bit_base/sm10bit_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sm2235 { + +class SM2235 : public sm10bit_base::Sm10BitBase { + public: + SM2235() = default; + + void setup() override; + void dump_config() override; +}; + +} // namespace sm2235 +} // namespace esphome diff --git a/esphome/components/sm2335/__init__.py b/esphome/components/sm2335/__init__.py new file mode 100644 index 0000000000..6d6e0e311c --- /dev/null +++ b/esphome/components/sm2335/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sm10bit_base + +AUTO_LOAD = ["sm10bit_base", "output"] +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +sm2335_ns = cg.esphome_ns.namespace("sm2335") + +SM2335 = sm2335_ns.class_("SM2335", sm10bit_base.Sm10BitBase) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2335), + } +).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA) + + +async def to_code(config): + var = await sm10bit_base.register_sm10bit_base(config) + cg.add(var.set_model(0xC0)) diff --git a/esphome/components/sm2335/output.py b/esphome/components/sm2335/output.py new file mode 100644 index 0000000000..52b6321db1 --- /dev/null +++ b/esphome/components/sm2335/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM2335 + +DEPENDENCIES = ["sm2335"] +CODEOWNERS = ["@Cossid"] + +Channel = SM2335.class_("Channel", output.FloatOutput) + +CONF_SM2335_ID = "sm2335_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_SM2335_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp new file mode 100644 index 0000000000..b6c482b5bb --- /dev/null +++ b/esphome/components/sm2335/sm2335.cpp @@ -0,0 +1,27 @@ +#include "sm2335.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm2335 { + +static const char *const TAG = "sm2335"; + +void SM2335::setup() { + ESP_LOGCONFIG(TAG, "Setting up sm2335 Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} + +void SM2335::dump_config() { + ESP_LOGCONFIG(TAG, "sm2335:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +} // namespace sm2335 +} // namespace esphome diff --git a/esphome/components/sm2335/sm2335.h b/esphome/components/sm2335/sm2335.h new file mode 100644 index 0000000000..c8cf825189 --- /dev/null +++ b/esphome/components/sm2335/sm2335.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sm10bit_base/sm10bit_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sm2335 { + +class SM2335 : public sm10bit_base::Sm10BitBase { + public: + SM2335() = default; + + void setup() override; + void dump_config() override; +}; + +} // namespace sm2335 +} // namespace esphome diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp index c726faec48..365271cec9 100644 --- a/esphome/components/sm300d2/sm300d2.cpp +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -42,7 +42,7 @@ void SM300D2Sensor::update() { this->status_clear_warning(); - ESP_LOGW(TAG, "Successfully read SM300D2 data"); + ESP_LOGD(TAG, "Successfully read SM300D2 data"); const uint16_t co2 = (response[2] * 256) + response[3]; const uint16_t formaldehyde = (response[4] * 256) + response[5]; diff --git a/esphome/components/sn74hc165/__init__.py b/esphome/components/sn74hc165/__init__.py new file mode 100644 index 0000000000..85d0220a88 --- /dev/null +++ b/esphome/components/sn74hc165/__init__.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_NUMBER, + CONF_INVERTED, + CONF_DATA_PIN, + CONF_CLOCK_PIN, + CONF_INPUT, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = [] +MULTI_CONF = True + +sn74hc165_ns = cg.esphome_ns.namespace("sn74hc165") + +SN74HC165Component = sn74hc165_ns.class_("SN74HC165Component", cg.Component) +SN74HC165GPIOPin = sn74hc165_ns.class_( + "SN74HC165GPIOPin", cg.GPIOPin, cg.Parented.template(SN74HC165Component) +) + +CONF_SN74HC165 = "sn74hc165" +CONF_LOAD_PIN = "load_pin" +CONF_CLOCK_INHIBIT_PIN = "clock_inhibit_pin" +CONF_SR_COUNT = "sr_count" +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(SN74HC165Component), + cv.Required(CONF_DATA_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LOAD_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_CLOCK_INHIBIT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(min=1, max=256), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + load_pin = await cg.gpio_pin_expression(config[CONF_LOAD_PIN]) + cg.add(var.set_load_pin(load_pin)) + if CONF_CLOCK_INHIBIT_PIN in config: + clock_inhibit_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_INHIBIT_PIN]) + cg.add(var.set_clock_inhibit_pin(clock_inhibit_pin)) + + cg.add(var.set_sr_count(config[CONF_SR_COUNT])) + + +def _validate_input_mode(value): + if value is not True: + raise cv.Invalid("Only input mode is supported") + return value + + +SN74HC165_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(SN74HC165GPIOPin), + cv.Required(CONF_SN74HC165): cv.use_id(SN74HC165Component), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=2048, max_included=False), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=True): cv.All( + cv.boolean, _validate_input_mode + ), + }, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC165, SN74HC165_PIN_SCHEMA) +async def sn74hc165_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_SN74HC165]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + return var diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp new file mode 100644 index 0000000000..7efe8a4c14 --- /dev/null +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -0,0 +1,72 @@ +#include "sn74hc165.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sn74hc165 { + +static const char *const TAG = "sn74hc165"; + +void SN74HC165Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SN74HC165..."); + + // initialize pins + this->clock_pin_->setup(); + this->data_pin_->setup(); + this->load_pin_->setup(); + this->clock_pin_->digital_write(false); + this->load_pin_->digital_write(false); + + if (this->clock_inhibit_pin_ != nullptr) { + this->clock_inhibit_pin_->setup(); + this->clock_inhibit_pin_->digital_write(true); + } + + // read state from shift register + this->read_gpio_(); +} + +void SN74HC165Component::loop() { this->read_gpio_(); } + +void SN74HC165Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC165:"); } + +bool SN74HC165Component::digital_read_(uint16_t pin) { + if (pin >= this->sr_count_ * 8) { + ESP_LOGE(TAG, "Pin %u is out of range! Maximum pin number with %u chips in series is %u", pin, this->sr_count_, + (this->sr_count_ * 8) - 1); + return false; + } + return this->input_bits_[pin]; +} + +void SN74HC165Component::read_gpio_() { + this->load_pin_->digital_write(false); + delayMicroseconds(10); + this->load_pin_->digital_write(true); + delayMicroseconds(10); + + if (this->clock_inhibit_pin_ != nullptr) + this->clock_inhibit_pin_->digital_write(false); + + for (uint8_t i = 0; i < this->sr_count_; i++) { + for (uint8_t j = 0; j < 8; j++) { + this->input_bits_[(i * 8) + (7 - j)] = this->data_pin_->digital_read(); + + this->clock_pin_->digital_write(true); + delayMicroseconds(10); + this->clock_pin_->digital_write(false); + delayMicroseconds(10); + } + } + + if (this->clock_inhibit_pin_ != nullptr) + this->clock_inhibit_pin_->digital_write(true); +} + +float SN74HC165Component::get_setup_priority() const { return setup_priority::IO; } + +bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } + +std::string SN74HC165GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC165", 18, pin_); } + +} // namespace sn74hc165 +} // namespace esphome diff --git a/esphome/components/sn74hc165/sn74hc165.h b/esphome/components/sn74hc165/sn74hc165.h new file mode 100644 index 0000000000..c349d079ae --- /dev/null +++ b/esphome/components/sn74hc165/sn74hc165.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace sn74hc165 { + +class SN74HC165Component : public Component { + public: + SN74HC165Component() = default; + + void setup() override; + void loop() override; + float get_setup_priority() const override; + void dump_config() override; + + void set_data_pin(GPIOPin *pin) { this->data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { this->clock_pin_ = pin; } + void set_load_pin(GPIOPin *pin) { this->load_pin_ = pin; } + void set_clock_inhibit_pin(GPIOPin *pin) { this->clock_inhibit_pin_ = pin; } + void set_sr_count(uint8_t count) { + this->sr_count_ = count; + this->input_bits_.resize(count * 8); + } + + protected: + friend class SN74HC165GPIOPin; + bool digital_read_(uint16_t pin); + void read_gpio_(); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *load_pin_; + GPIOPin *clock_inhibit_pin_; + uint8_t sr_count_; + std::vector input_bits_; +}; + +/// Helper class to expose a SC74HC165 pin as an internal input GPIO pin. +class SN74HC165GPIOPin : public GPIOPin, public Parented { + public: + void setup() override {} + void pin_mode(gpio::Flags flags) override {} + bool digital_read() override; + void digital_write(bool value) override{}; + std::string dump_summary() const override; + + void set_pin(uint16_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + + protected: + uint16_t pin_; + bool inverted_; +}; + +} // namespace sn74hc165 +} // namespace esphome diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 630abc8bca..92b6d8d0e5 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -17,7 +17,9 @@ MULTI_CONF = True sn74hc595_ns = cg.esphome_ns.namespace("sn74hc595") SN74HC595Component = sn74hc595_ns.class_("SN74HC595Component", cg.Component) -SN74HC595GPIOPin = sn74hc595_ns.class_("SN74HC595GPIOPin", cg.GPIOPin) +SN74HC595GPIOPin = sn74hc595_ns.class_( + "SN74HC595GPIOPin", cg.GPIOPin, cg.Parented.template(SN74HC595Component) +) CONF_SN74HC595 = "sn74hc595" CONF_LATCH_PIN = "latch_pin" @@ -30,7 +32,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(1, 4), + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(min=1, max=256), } ).extend(cv.COMPONENT_SCHEMA) @@ -60,7 +62,7 @@ SN74HC595_PIN_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=31), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=2048, max_included=False), cv.Optional(CONF_MODE, default={}): cv.All( { cv.Optional(CONF_OUTPUT, default=True): cv.All( @@ -76,10 +78,8 @@ SN74HC595_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, SN74HC595_PIN_SCHEMA) async def sn74hc595_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - parent = await cg.get_variable(config[CONF_SN74HC595]) - cg.add(var.set_parent(parent)) + await cg.register_parented(var, config[CONF_SN74HC595]) - num = config[CONF_NUMBER] - cg.add(var.set_pin(num)) + cg.add(var.set_pin(config[CONF_NUMBER])) cg.add(var.set_inverted(config[CONF_INVERTED])) return var diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index 5ebf50e5cb..1895b1d5a6 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -28,24 +28,21 @@ void SN74HC595Component::setup() { void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } -bool SN74HC595Component::digital_read_(uint8_t pin) { return this->output_bits_ >> pin; } - -void SN74HC595Component::digital_write_(uint8_t pin, bool value) { - uint32_t mask = 1UL << pin; - this->output_bits_ &= ~mask; - if (value) - this->output_bits_ |= mask; +void SN74HC595Component::digital_write_(uint16_t pin, bool value) { + if (pin >= this->sr_count_ * 8) { + ESP_LOGE(TAG, "Pin %u is out of range! Maximum pin number with %u chips in series is %u", pin, this->sr_count_, + (this->sr_count_ * 8) - 1); + return; + } + this->output_bits_[pin] = value; this->write_gpio_(); } -bool SN74HC595Component::write_gpio_() { - for (int i = this->sr_count_ - 1; i >= 0; i--) { - uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); - for (int j = 0; j < 8; j++) { - this->data_pin_->digital_write(data & (1 << (7 - j))); - this->clock_pin_->digital_write(true); - this->clock_pin_->digital_write(false); - } +void SN74HC595Component::write_gpio_() { + for (auto bit = this->output_bits_.rbegin(); bit != this->output_bits_.rend(); bit++) { + this->data_pin_->digital_write(*bit); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); } // pulse latch to activate new values @@ -56,8 +53,6 @@ bool SN74HC595Component::write_gpio_() { if (this->have_oe_pin_) { this->oe_pin_->digital_write(false); } - - return true; } float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 784019c3a6..64bf06d881 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -2,6 +2,9 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include namespace esphome { namespace sn74hc595 { @@ -21,13 +24,15 @@ class SN74HC595Component : public Component { oe_pin_ = pin; have_oe_pin_ = true; } - void set_sr_count(uint8_t count) { sr_count_ = count; } + void set_sr_count(uint8_t count) { + sr_count_ = count; + this->output_bits_.resize(count * 8); + } protected: friend class SN74HC595GPIOPin; - bool digital_read_(uint8_t pin); - void digital_write_(uint8_t pin, bool value); - bool write_gpio_(); + void digital_write_(uint16_t pin, bool value); + void write_gpio_(); GPIOPin *data_pin_; GPIOPin *clock_pin_; @@ -35,11 +40,11 @@ class SN74HC595Component : public Component { GPIOPin *oe_pin_; uint8_t sr_count_; bool have_oe_pin_{false}; - uint32_t output_bits_{0x00}; + std::vector output_bits_; }; /// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. -class SN74HC595GPIOPin : public GPIOPin { +class SN74HC595GPIOPin : public GPIOPin, public Parented { public: void setup() override {} void pin_mode(gpio::Flags flags) override {} @@ -47,13 +52,11 @@ class SN74HC595GPIOPin : public GPIOPin { void digital_write(bool value) override; std::string dump_summary() const override; - void set_parent(SN74HC595Component *parent) { parent_ = parent; } - void set_pin(uint8_t pin) { pin_ = pin; } + void set_pin(uint16_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } protected: - SN74HC595Component *parent_; - uint8_t pin_; + uint16_t pin_; bool inverted_; }; diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 21fcb96842..3af21a9b23 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -10,6 +10,9 @@ #ifdef USE_ESP8266 #include "sntp.h" #endif +#ifdef USE_RP2040 +#include "lwip/apps/sntp.h" +#endif // Yes, the server names are leaked, but that's fine. #ifdef CLANG_TIDY diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 8e9502be6d..81203fdc31 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -13,6 +13,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_IMPLEMENTATION, esp8266=IMPLEMENTATION_LWIP_TCP, esp32=IMPLEMENTATION_BSD_SOCKETS, + rp2040=IMPLEMENTATION_LWIP_TCP, ): cv.one_of( IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" ), diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 6636bcb3eb..2dea4af277 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -18,18 +18,23 @@ std::string format_sockaddr(const struct sockaddr_storage &storage) { if (storage.ss_family == AF_INET) { const struct sockaddr_in *addr = reinterpret_cast(&storage); char buf[INET_ADDRSTRLEN]; - const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } else if (storage.ss_family == AF_INET6) { + if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr) + return std::string{buf}; + } +#if LWIP_IPV6 + else if (storage.ss_family == AF_INET6) { const struct sockaddr_in6 *addr = reinterpret_cast(&storage); char buf[INET6_ADDRSTRLEN]; - const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; + // Format IPv4-mapped IPv6 addresses as regular IPv4 addresses + if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && + addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && + inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) { + return std::string{buf}; + } + if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr) + return std::string{buf}; } +#endif return {}; } @@ -134,6 +139,11 @@ class BSDSocketImpl : public Socket { return ::writev(fd_, iov, iovcnt); #endif } + + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { + return ::sendto(fd_, buf, len, flags, to, tolen); + } + int setblocking(bool blocking) override { int fl = ::fcntl(fd_, F_GETFL, 0); if (blocking) { diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index a383c0071d..20d8fdb8c9 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -15,19 +15,28 @@ /* Address families. */ #define AF_UNSPEC 0 #define AF_INET 2 -#define AF_INET6 10 #define PF_INET AF_INET -#define PF_INET6 AF_INET6 #define PF_UNSPEC AF_UNSPEC + #define IPPROTO_IP 0 #define IPPROTO_TCP 6 + +#if LWIP_IPV6 +#define AF_INET6 10 +#define PF_INET6 AF_INET6 + #define IPPROTO_IPV6 41 #define IPPROTO_ICMPV6 58 +#endif #define TCP_NODELAY 0x01 #define F_GETFL 3 #define F_SETFL 4 + +#ifdef O_NONBLOCK +#undef O_NONBLOCK +#endif #define O_NONBLOCK 1 #define SHUT_RD 0 @@ -58,6 +67,7 @@ struct sockaddr_in { char sin_zero[SIN_ZERO_LEN]; }; +#if LWIP_IPV6 // NOLINTNEXTLINE(readability-identifier-naming) struct sockaddr_in6 { uint8_t sin6_len; /* length of this structure */ @@ -67,6 +77,7 @@ struct sockaddr_in6 { struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Set of interfaces for scope */ }; +#endif // NOLINTNEXTLINE(readability-identifier-naming) struct sockaddr { @@ -91,7 +102,7 @@ struct iovec { size_t iov_len; }; -#ifdef USE_ESP8266 +#if defined(USE_ESP8266) || defined(USE_RP2040) // arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define #ifdef INADDR_ANY #undef INADDR_ANY diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index f5bb57bb93..bd59b81caa 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -467,6 +467,10 @@ class LWIPRawImpl : public Socket { } return written; } + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { + // return ::sendto(fd_, buf, len, flags, to, tolen); + return 0; + } int setblocking(bool blocking) override { if (pcb_ == nullptr) { errno = ECONNRESET; diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 22a4c11df8..d00ddaeae2 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -1,7 +1,8 @@ #include "socket.h" -#include "esphome/core/log.h" -#include #include +#include +#include +#include "esphome/core/log.h" namespace esphome { namespace socket { @@ -14,6 +15,39 @@ std::unique_ptr socket_ip(int type, int protocol) { #endif } +socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { +#if LWIP_IPV6 + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return 0; + } + auto *server = reinterpret_cast(addr); + memset(server, 0, sizeof(sockaddr_in6)); + server->sin6_family = AF_INET6; + server->sin6_port = htons(port); + + if (ip_address.find('.') != std::string::npos) { + server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str()); + } else { + ip6_addr_t ip6; + inet6_aton(ip_address.c_str(), &ip6); + memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); + } + return sizeof(sockaddr_in6); +#else + if (addrlen < sizeof(sockaddr_in)) { + errno = EINVAL; + return 0; + } + auto *server = reinterpret_cast(addr); + memset(server, 0, sizeof(sockaddr_in)); + server->sin_family = AF_INET; + server->sin_addr.s_addr = inet_addr(ip_address.c_str()); + server->sin_port = htons(port); + return sizeof(sockaddr_in); +#endif +} + socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { #if LWIP_IPV6 if (addrlen < sizeof(sockaddr_in6)) { @@ -23,7 +57,7 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po auto *server = reinterpret_cast(addr); memset(server, 0, sizeof(sockaddr_in6)); server->sin6_family = AF_INET6; - server->sin6_port = port; + server->sin6_port = htons(port); server->sin6_addr = in6addr_any; return sizeof(sockaddr_in6); #else @@ -35,7 +69,7 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po memset(server, 0, sizeof(sockaddr_in)); server->sin_family = AF_INET; server->sin_addr.s_addr = ESPHOME_INADDR_ANY; - server->sin_port = port; + server->sin_port = htons(port); return sizeof(sockaddr_in); #endif } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index ecf117deeb..7400ba306f 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -1,9 +1,9 @@ #pragma once -#include #include +#include -#include "headers.h" #include "esphome/core/optional.h" +#include "headers.h" namespace esphome { namespace socket { @@ -34,6 +34,8 @@ class Socket { virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0; virtual ssize_t write(const void *buf, size_t len) = 0; virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0; + virtual ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen); + virtual int setblocking(bool blocking) = 0; virtual int loop() { return 0; }; }; @@ -44,7 +46,10 @@ std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. std::unique_ptr socket_ip(int type, int protocol); -/// Set a sockaddr to the any address for the IP version used by socket_ip(). +/// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). +socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port); + +/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); } // namespace socket diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index dc6b719f1b..6ae80296fd 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -71,8 +71,9 @@ void SonoffD1Output::skip_command_() { } // Warn about unexpected bytes in the protocol with UART dimmer - if (garbage) + if (garbage) { ESP_LOGW(TAG, "[%04d] Skip %d bytes from the dimmer", this->write_count_, garbage); + } } // This assumes some data is already available diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index c917fe1ad8..e0fc9efb42 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -17,6 +17,7 @@ spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") MULTI_CONF = True +CONF_FORCE_SW = "force_sw" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -25,6 +26,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_FORCE_SW, default=False): cv.boolean, } ), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), @@ -39,6 +41,7 @@ async def to_code(config): clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk(clk)) + cg.add(var.set_force_sw(config[CONF_FORCE_SW])) if CONF_MISO_PIN in config: miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN]) cg.add(var.set_miso(miso)) @@ -46,9 +49,7 @@ async def to_code(config): mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("SPI", None) - if CORE.is_esp8266: + if CORE.using_arduino: cg.add_library("SPI", None) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 864f6ae39d..141bfb9448 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -25,7 +25,7 @@ void SPIComponent::setup() { this->clk_->digital_write(true); #ifdef USE_SPI_ARDUINO_BACKEND - bool use_hw_spi = true; + bool use_hw_spi = !this->force_sw_; const bool has_miso = this->miso_ != nullptr; const bool has_mosi = this->mosi_ != nullptr; int8_t clk_pin = -1, miso_pin = -1, mosi_pin = -1; @@ -79,7 +79,7 @@ void SPIComponent::setup() { #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) this->hw_spi_ = new SPIClass(FSPI); // NOLINT(cppcoreguidelines-owning-memory) #else - this->hw_spi_ = new SPIClass(VSPI); // NOLINT(cppcoreguidelines-owning-memory) + this->hw_spi_ = new SPIClass(HSPI); // NOLINT(cppcoreguidelines-owning-memory) #endif // USE_ESP32_VARIANT } spi_bus_num++; diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 7f0b0f481a..bacdad723b 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -74,6 +74,7 @@ class SPIComponent : public Component { void set_clk(GPIOPin *clk) { clk_ = clk; } void set_miso(GPIOPin *miso) { miso_ = miso; } void set_mosi(GPIOPin *mosi) { mosi_ = mosi; } + void set_force_sw(bool force_sw) { force_sw_ = force_sw; } void setup() override; @@ -105,7 +106,11 @@ class SPIComponent : public Component { void write_byte(uint8_t data) { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { +#ifdef USE_RP2040 + this->hw_spi_->transfer(data); +#else this->hw_spi_->write(data); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -116,7 +121,11 @@ class SPIComponent : public Component { void write_byte16(const uint16_t data) { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { +#ifdef USE_RP2040 + this->hw_spi_->transfer16(data); +#else this->hw_spi_->write16(data); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -130,7 +139,11 @@ class SPIComponent : public Component { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { for (size_t i = 0; i < length; i++) { +#ifdef USE_RP2040 + this->hw_spi_->transfer16(data[i]); +#else this->hw_spi_->write16(data[i]); +#endif } return; } @@ -145,7 +158,11 @@ class SPIComponent : public Component { #ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { auto *data_c = const_cast(data); +#ifdef USE_RP2040 + this->hw_spi_->transfer(data_c, length); +#else this->hw_spi_->writeBytes(data_c, length); +#endif return; } #endif // USE_SPI_ARDUINO_BACKEND @@ -178,7 +195,11 @@ class SPIComponent : public Component { if (this->miso_ != nullptr) { this->hw_spi_->transfer(data, length); } else { +#ifdef USE_RP2040 + this->hw_spi_->transfer(data, length); +#else this->hw_spi_->writeBytes(data, length); +#endif } return; } @@ -205,7 +226,11 @@ class SPIComponent : public Component { } else if (CLOCK_POLARITY && CLOCK_PHASE) { data_mode = SPI_MODE3; } +#ifdef USE_RP2040 + SPISettings settings(DATA_RATE, static_cast(BIT_ORDER), data_mode); +#else SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); +#endif this->hw_spi_->beginTransaction(settings); } else { #endif // USE_SPI_ARDUINO_BACKEND @@ -236,6 +261,7 @@ class SPIComponent : public Component { GPIOPin *miso_{nullptr}; GPIOPin *mosi_{nullptr}; GPIOPin *active_cs_{nullptr}; + bool force_sw_{false}; #ifdef USE_SPI_ARDUINO_BACKEND SPIClass *hw_spi_{nullptr}; #endif // USE_SPI_ARDUINO_BACKEND diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 4e80cfa021..6aa76dcd2f 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -2,22 +2,36 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +from esphome.components import number from esphome.components import switch from esphome.const import ( + CONF_ENTITY_CATEGORY, CONF_ID, + CONF_INITIAL_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, CONF_NAME, CONF_REPEAT, + CONF_RESTORE_VALUE, CONF_RUN_DURATION, + CONF_STEP, + CONF_UNIT_OF_MEASUREMENT, + ENTITY_CATEGORY_CONFIG, + UNIT_MINUTE, + UNIT_SECOND, ) -AUTO_LOAD = ["switch"] +AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" +CONF_DIVIDER = "divider" CONF_ENABLE_SWITCH = "enable_switch" CONF_MAIN_SWITCH = "main_switch" CONF_MANUAL_SELECTION_DELAY = "manual_selection_delay" CONF_MULTIPLIER = "multiplier" +CONF_MULTIPLIER_NUMBER = "multiplier_number" +CONF_NEXT_PREV_IGNORE_DISABLED = "next_prev_ignore_disabled" CONF_PUMP_OFF_SWITCH_ID = "pump_off_switch_id" CONF_PUMP_ON_SWITCH_ID = "pump_on_switch_id" CONF_PUMP_PULSE_DURATION = "pump_pulse_duration" @@ -29,7 +43,11 @@ CONF_PUMP_SWITCH = "pump_switch" CONF_PUMP_SWITCH_ID = "pump_switch_id" CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY = "pump_switch_off_during_valve_open_delay" CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch" +CONF_REPEAT_NUMBER = "repeat_number" CONF_REVERSE_SWITCH = "reverse_switch" +CONF_RUN_DURATION_NUMBER = "run_duration_number" +CONF_SET_ACTION = "set_action" +CONF_STANDBY_SWITCH = "standby_switch" CONF_VALVE_NUMBER = "valve_number" CONF_VALVE_OPEN_DELAY = "valve_open_delay" CONF_VALVE_OVERLAP = "valve_overlap" @@ -42,10 +60,14 @@ CONF_VALVES = "valves" sprinkler_ns = cg.esphome_ns.namespace("sprinkler") Sprinkler = sprinkler_ns.class_("Sprinkler", cg.Component) +SprinklerControllerNumber = sprinkler_ns.class_( + "SprinklerControllerNumber", number.Number, cg.Component +) SprinklerControllerSwitch = sprinkler_ns.class_( "SprinklerControllerSwitch", switch.Switch, cg.Component ) +SetDividerAction = sprinkler_ns.class_("SetDividerAction", automation.Action) SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action) QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action) ClearQueuedValvesAction = sprinkler_ns.class_( @@ -66,6 +88,19 @@ ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid(f"{CONF_MAX_VALUE} must be greater than {CONF_MIN_VALUE}") + + if (config[CONF_INITIAL_VALUE] > config[CONF_MAX_VALUE]) or ( + config[CONF_INITIAL_VALUE] < config[CONF_MIN_VALUE] + ): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) + return config + + def validate_sprinkler(config): for sprinkler_controller_index, sprinkler_controller in enumerate(config): if len(sprinkler_controller[CONF_VALVES]) <= 1: @@ -103,9 +138,18 @@ def validate_sprinkler(config): f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" ) + if ( + CONF_REPEAT in sprinkler_controller + and CONF_REPEAT_NUMBER in sprinkler_controller + ): + raise cv.Invalid( + f"Do not specify {CONF_REPEAT} when using {CONF_REPEAT_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) + for valve in sprinkler_controller[CONF_VALVES]: if ( CONF_VALVE_OVERLAP in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OVERLAP] ): raise cv.Invalid( @@ -113,6 +157,7 @@ def validate_sprinkler(config): ) if ( CONF_VALVE_OPEN_DELAY in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OPEN_DELAY] ): @@ -169,6 +214,14 @@ def validate_sprinkler(config): raise cv.Invalid( f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" ) + if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve: + raise cv.Invalid( + f"Either {CONF_RUN_DURATION} or {CONF_RUN_DURATION_NUMBER} must be specified for each valve" + ) + if CONF_RUN_DURATION in valve and CONF_RUN_DURATION_NUMBER in valve: + raise cv.Invalid( + f"Do not specify {CONF_RUN_DURATION} when using {CONF_RUN_DURATION_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) return config @@ -189,11 +242,20 @@ SPRINKLER_ACTION_REPEAT_SCHEMA = cv.maybe_simple_value( SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), + cv.Optional(CONF_RUN_DURATION): cv.templatable(cv.positive_time_period_seconds), cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), }, key=CONF_VALVE_NUMBER, ) +SPRINKLER_ACTION_SET_DIVIDER_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_DIVIDER): cv.templatable(cv.positive_int), + }, + key=CONF_DIVIDER, +) + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), @@ -223,13 +285,40 @@ SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA = cv.Schema( SPRINKLER_VALVE_SCHEMA = cv.Schema( { cv.Optional(CONF_ENABLE_SWITCH): cv.maybe_simple_value( - switch.switch_schema(SprinklerControllerSwitch), + switch.switch_schema( + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", + ), key=CONF_NAME, ), cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), - cv.Required(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=900): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=86400): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=1): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND + ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Required(CONF_VALVE_SWITCH): cv.maybe_simple_value( switch.switch_schema(SprinklerControllerSwitch), key=CONF_NAME, @@ -243,8 +332,13 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sprinkler), + cv.Optional(CONF_NAME): cv.string, cv.Optional(CONF_AUTO_ADVANCE_SWITCH): cv.maybe_simple_value( - switch.switch_schema(SprinklerControllerSwitch), + switch.switch_schema( + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", + ), key=CONF_NAME, ), cv.Optional(CONF_MAIN_SWITCH): cv.maybe_simple_value( @@ -252,15 +346,72 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( key=CONF_NAME, ), cv.Optional(CONF_QUEUE_ENABLE_SWITCH): cv.maybe_simple_value( - switch.switch_schema(SprinklerControllerSwitch), + switch.switch_schema( + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", + ), key=CONF_NAME, ), cv.Optional(CONF_REVERSE_SWITCH): cv.maybe_simple_value( - switch.switch_schema(SprinklerControllerSwitch), + switch.switch_schema( + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", + ), key=CONF_NAME, ), + cv.Optional(CONF_STANDBY_SWITCH): cv.maybe_simple_value( + switch.switch_schema( + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", + ), + key=CONF_NAME, + ), + cv.Optional(CONF_NEXT_PREV_IGNORE_DISABLED, default=False): cv.boolean, cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, + cv.Optional(CONF_MULTIPLIER_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=1): cv.positive_float, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_REPEAT): cv.positive_int, + cv.Optional(CONF_REPEAT_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Exclusive( @@ -284,7 +435,8 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( ): cv.positive_time_period_seconds, cv.Required(CONF_VALVES): cv.ensure_list(SPRINKLER_VALVE_SCHEMA), } -).extend(cv.ENTITY_BASE_SCHEMA) +).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.All( cv.ensure_list(SPRINKLER_CONTROLLER_SCHEMA), @@ -292,6 +444,19 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "sprinkler.set_divider", + SetDividerAction, + SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, +) +async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.float_) + cg.add(var.set_divider(template_)) + return var + + @automation.register_action( "sprinkler.set_multiplier", SetMultiplierAction, @@ -376,6 +541,9 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) cg.add(var.set_valve_to_start(template_)) + if CONF_RUN_DURATION in config: + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) return var @@ -403,16 +571,19 @@ async def sprinkler_simple_action_to_code(config, action_id, template_arg, args) async def to_code(config): for sprinkler_controller in config: - if len(sprinkler_controller[CONF_VALVES]) > 1: - var = cg.new_Pvariable( - sprinkler_controller[CONF_ID], - sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME], - ) + var = cg.new_Pvariable(sprinkler_controller[CONF_ID]) + + if CONF_NAME in sprinkler_controller: + cg.add(var.set_name(sprinkler_controller[CONF_NAME])) else: - var = cg.new_Pvariable( - sprinkler_controller[CONF_ID], - sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][CONF_NAME], - ) + if len(sprinkler_controller[CONF_VALVES]) > 1: + name = sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME] + else: + name = sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][ + CONF_NAME + ] + cg.add(var.set_name(name)) + await cg.register_component(var, sprinkler_controller) if len(sprinkler_controller[CONF_VALVES]) > 1: @@ -446,6 +617,79 @@ async def to_code(config): ) cg.add(var.set_controller_reverse_switch(sw_rev_var)) + if CONF_STANDBY_SWITCH in sprinkler_controller: + sw_stb_var = await switch.new_switch( + sprinkler_controller[CONF_STANDBY_SWITCH] + ) + await cg.register_component( + sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH] + ) + cg.add(var.set_controller_standby_switch(sw_stb_var)) + + if CONF_MULTIPLIER_NUMBER in sprinkler_controller: + num_mult_var = await number.new_number( + sprinkler_controller[CONF_MULTIPLIER_NUMBER], + min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MIN_VALUE + ], + max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MAX_VALUE + ], + step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER] + ) + cg.add( + num_mult_var.set_initial_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_mult_var.set_restore_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]: + await automation.build_automation( + num_mult_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_multiplier_number(num_mult_var)) + + if CONF_REPEAT_NUMBER in sprinkler_controller: + num_repeat_var = await number.new_number( + sprinkler_controller[CONF_REPEAT_NUMBER], + min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE], + max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE], + step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER] + ) + cg.add( + num_repeat_var.set_initial_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_repeat_var.set_restore_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]: + await automation.build_automation( + num_repeat_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_repeat_number(num_repeat_var)) + for valve in sprinkler_controller[CONF_VALVES]: sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH]) await cg.register_component(sw_valve_var, valve[CONF_VALVE_SWITCH]) @@ -461,6 +705,12 @@ async def to_code(config): else: cg.add(var.add_valve(sw_valve_var)) + cg.add( + var.set_next_prev_ignore_disabled_valves( + sprinkler_controller[CONF_NEXT_PREV_IGNORE_DISABLED] + ) + ) + if CONF_MANUAL_SELECTION_DELAY in sprinkler_controller: cg.add( var.set_manual_selection_delay( @@ -515,6 +765,11 @@ async def to_code(config): for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for valve_index, valve in enumerate(sprinkler_controller[CONF_VALVES]): + if CONF_RUN_DURATION not in valve: + valve[CONF_RUN_DURATION] = valve[CONF_RUN_DURATION_NUMBER][ + CONF_INITIAL_VALUE + ] + if CONF_VALVE_SWITCH_ID in valve: valve_switch = await cg.get_variable(valve[CONF_VALVE_SWITCH_ID]) cg.add( @@ -552,6 +807,35 @@ async def to_code(config): ) ) + if CONF_RUN_DURATION_NUMBER in valve: + num_rd_var = await number.new_number( + valve[CONF_RUN_DURATION_NUMBER], + min_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MIN_VALUE], + max_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MAX_VALUE], + step=valve[CONF_RUN_DURATION_NUMBER][CONF_STEP], + ) + await cg.register_component(num_rd_var, valve[CONF_RUN_DURATION_NUMBER]) + + cg.add( + num_rd_var.set_initial_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_rd_var.set_restore_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in valve[CONF_RUN_DURATION_NUMBER]: + await automation.build_automation( + num_rd_var.get_set_trigger(), + [(float, "x")], + valve[CONF_RUN_DURATION_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.configure_valve_run_duration_number(valve_index, num_rd_var)) + for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for controller_to_add in config: diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index dd0ea44633..59c6cd50e1 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -7,6 +7,18 @@ namespace esphome { namespace sprinkler { +template class SetDividerAction : public Action { + public: + explicit SetDividerAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(uint32_t, divider) + + void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + template class SetMultiplierAction : public Action { public: explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -98,8 +110,12 @@ template class StartSingleValveAction : public Action { explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} TEMPLATABLE_VALUE(size_t, valve_to_start) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...)); } + void play(Ts... x) override { + this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index ab694c8412..52a6cd2af4 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -75,6 +75,34 @@ void SprinklerSwitch::sync_valve_state(bool latch_state) { } } +void SprinklerControllerNumber::setup() { + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) { + value = this->initial_value_; + } else { + value = this->traits.get_min_value(); + } + } + } + this->publish_state(value); +} + +void SprinklerControllerNumber::control(float value) { + this->set_trigger_->trigger(value); + + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} + +void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } + SprinklerControllerSwitch::SprinklerControllerSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} @@ -101,43 +129,18 @@ void SprinklerControllerSwitch::write_state(bool state) { this->turn_off_trigger_->trigger(); } - if (this->optimistic_) - this->publish_state(state); + this->publish_state(state); } -void SprinklerControllerSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -bool SprinklerControllerSwitch::assumed_state() { return this->assumed_state_; } void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } -void SprinklerControllerSwitch::setup() { - if (!this->restore_state_) - return; +void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); } - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; - - ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); - if (*restored) { - this->turn_on(); - } else { - this->turn_off(); - } -} - -void SprinklerControllerSwitch::dump_config() { - LOG_SWITCH("", "Sprinkler Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); - ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); -} - -void SprinklerControllerSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } - -void SprinklerControllerSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } +void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); } SprinklerValveOperator::SprinklerValveOperator() {} SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) @@ -328,6 +331,8 @@ SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } +void SprinklerValveRunRequest::set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } + void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } void SprinklerValveRunRequest::set_valve(size_t valve_number) { @@ -345,6 +350,7 @@ void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_ void SprinklerValveRunRequest::reset() { this->has_valve_ = false; + this->origin_ = USER; this->run_duration_ = 0; this->valve_op_ = nullptr; } @@ -362,10 +368,13 @@ optional SprinklerValveRunRequest::valve_as_opt() { SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } -Sprinkler::Sprinkler() {} -Sprinkler::Sprinkler(const std::string &name) : EntityBase(name) {} +SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } -void Sprinkler::setup() { this->all_valves_off_(true); } +void Sprinkler::setup() { + this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); + this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); + this->all_valves_off_(true); +} void Sprinkler::loop() { for (auto &p : this->pump_) { @@ -404,8 +413,6 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll if (enable_sw != nullptr) { new_valve->enable_switch = enable_sw; - new_valve->enable_switch->set_optimistic(true); - new_valve->enable_switch->set_restore_state(true); } } @@ -433,20 +440,30 @@ void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { this->auto_adv_sw_ = auto_adv_switch; - auto_adv_switch->set_optimistic(true); - auto_adv_switch->set_restore_state(true); } void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { this->queue_enable_sw_ = queue_enable_switch; - queue_enable_switch->set_optimistic(true); - queue_enable_switch->set_restore_state(true); } void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { this->reverse_sw_ = reverse_switch; - reverse_switch->set_optimistic(true); - reverse_switch->set_restore_state(true); +} + +void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) { + this->standby_sw_ = standby_switch; + + this->sprinkler_standby_turn_on_automation_ = make_unique>(standby_switch->get_turn_on_trigger()); + this->sprinkler_standby_shutdown_action_ = make_unique>(this); + this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()}); +} + +void Sprinkler::set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { + this->multiplier_number_ = multiplier_number; +} + +void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { + this->repeat_number_ = repeat_number; } void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { @@ -499,14 +516,46 @@ void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_: } } -void Sprinkler::set_multiplier(const optional multiplier) { - if (multiplier.has_value()) { - if (multiplier.value() > 0) { - this->multiplier_ = multiplier.value(); - } +void Sprinkler::configure_valve_run_duration_number(size_t valve_number, + SprinklerControllerNumber *run_duration_number) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].run_duration_number = run_duration_number; } } +void Sprinkler::set_divider(optional divider) { + if (!divider.has_value()) { + return; + } + if (divider.value() > 0) { + this->set_multiplier(1.0 / divider.value()); + this->set_repeat(divider.value() - 1); + } else if (divider.value() == 0) { + this->set_multiplier(1.0); + this->set_repeat(0); + } +} + +void Sprinkler::set_multiplier(const optional multiplier) { + if ((!multiplier.has_value()) || (multiplier.value() < 0)) { + return; + } + this->multiplier_ = multiplier.value(); + if (this->multiplier_number_ == nullptr) { + return; + } + if (this->multiplier_number_->state == multiplier.value()) { + return; + } + auto call = this->multiplier_number_->make_call(); + call.set_value(multiplier.value()); + call.perform(); +} + +void Sprinkler::set_next_prev_ignore_disabled_valves(bool ignore_disabled) { + this->next_prev_ignore_disabled_ = ignore_disabled; +} + void Sprinkler::set_pump_start_delay(uint32_t start_delay) { this->start_delay_is_valve_delay_ = false; this->start_delay_ = start_delay; @@ -559,47 +608,118 @@ void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { } void Sprinkler::set_valve_run_duration(const optional valve_number, const optional run_duration) { - if (valve_number.has_value() && run_duration.has_value()) { - if (this->is_a_valid_valve(valve_number.value())) { - this->valve_[valve_number.value()].run_duration = run_duration.value(); - } + if (!valve_number.has_value() || !run_duration.has_value()) { + return; } + if (!this->is_a_valid_valve(valve_number.value())) { + return; + } + this->valve_[valve_number.value()].run_duration = run_duration.value(); + if (this->valve_[valve_number.value()].run_duration_number == nullptr) { + return; + } + if (this->valve_[valve_number.value()].run_duration_number->state == run_duration.value()) { + return; + } + auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + call.set_value(run_duration.value() / 60.0); + } else { + call.set_value(run_duration.value()); + } + call.perform(); } void Sprinkler::set_auto_advance(const bool auto_advance) { - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(auto_advance); + if (this->auto_adv_sw_ == nullptr) { + return; + } + if (this->auto_adv_sw_->state == auto_advance) { + return; + } + if (auto_advance) { + this->auto_adv_sw_->turn_on(); + } else { + this->auto_adv_sw_->turn_off(); } } -void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; } +void Sprinkler::set_repeat(optional repeat) { + this->target_repeats_ = repeat; + if (this->repeat_number_ == nullptr) { + return; + } + if (this->repeat_number_->state == repeat.value()) { + return; + } + auto call = this->repeat_number_->make_call(); + call.set_value(repeat.value_or(0)); + call.perform(); +} void Sprinkler::set_queue_enable(bool queue_enable) { - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(queue_enable); + if (this->queue_enable_sw_ == nullptr) { + return; + } + if (this->queue_enable_sw_->state == queue_enable) { + return; + } + if (queue_enable) { + this->queue_enable_sw_->turn_on(); + } else { + this->queue_enable_sw_->turn_off(); } } void Sprinkler::set_reverse(const bool reverse) { - if (this->reverse_sw_ != nullptr) { - this->reverse_sw_->publish_state(reverse); + if (this->reverse_sw_ == nullptr) { + return; + } + if (this->reverse_sw_->state == reverse) { + return; + } + if (reverse) { + this->reverse_sw_->turn_on(); + } else { + this->reverse_sw_->turn_off(); + } +} + +void Sprinkler::set_standby(const bool standby) { + if (this->standby_sw_ == nullptr) { + return; + } + if (this->standby_sw_->state == standby) { + return; + } + if (standby) { + this->standby_sw_->turn_on(); + } else { + this->standby_sw_->turn_off(); } } uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { - if (this->is_a_valid_valve(valve_number)) { - return this->valve_[valve_number].run_duration; + if (!this->is_a_valid_valve(valve_number)) { + return 0; } - return 0; + if (this->valve_[valve_number].run_duration_number != nullptr) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); + } else { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); + } + } + return this->valve_[valve_number].run_duration; } uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { uint32_t run_duration = 0; if (this->is_a_valid_valve(valve_number)) { - run_duration = this->valve_[valve_number].run_duration; + run_duration = this->valve_run_duration(valve_number); } - run_duration = static_cast(roundf(run_duration * this->multiplier_)); + run_duration = static_cast(roundf(run_duration * this->multiplier())); // run_duration must not be less than any of these if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || (run_duration < this->switching_delay_.value_or(0) * 2)) { @@ -615,16 +735,24 @@ bool Sprinkler::auto_advance() { return false; } -float Sprinkler::multiplier() { return this->multiplier_; } +float Sprinkler::multiplier() { + if (this->multiplier_number_ != nullptr) { + return this->multiplier_number_->state; + } + return this->multiplier_; +} -optional Sprinkler::repeat() { return this->target_repeats_; } +optional Sprinkler::repeat() { + if (this->repeat_number_ != nullptr) { + return static_cast(roundf(this->repeat_number_->state)); + } + return this->target_repeats_; +} optional Sprinkler::repeat_count() { // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count - if (this->auto_adv_sw_ != nullptr) { - if (this->active_req_.has_request() && this->auto_adv_sw_->state) { - return this->repeat_count_; - } + if (this->active_req_.has_request() && this->auto_advance()) { + return this->repeat_count_; } return nullopt; } @@ -643,7 +771,22 @@ bool Sprinkler::reverse() { return false; } +bool Sprinkler::standby() { + if (this->standby_sw_ != nullptr) { + return this->standby_sw_->state; + } + return false; +} + void Sprinkler::start_from_queue() { + if (this->standby()) { + ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken"); + return; + } if (this->queued_valves_.empty()) { return; // if there is nothing in the queue, don't do anything } @@ -651,25 +794,29 @@ void Sprinkler::start_from_queue() { return; // if there is already a valve running from the queue, do nothing } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(true); - } + this->set_auto_advance(false); + this->set_queue_enable(true); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; this->fsm_kick_(); // will automagically pick up from the queue (it has priority) } void Sprinkler::start_full_cycle() { + if (this->standby()) { + ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken"); + return; + } if (this->auto_advance() && this->active_valve().has_value()) { return; // if auto-advance is already enabled and there is already a valve running, do nothing } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_queue_enable(false); + this->prep_full_cycle_(); this->repeat_count_ = 0; // if there is no active valve already, start the first valve in the cycle @@ -678,20 +825,25 @@ void Sprinkler::start_full_cycle() { } } -void Sprinkler::start_single_valve(const optional valve_number) { +void Sprinkler::start_single_valve(const optional valve_number, optional run_duration) { + if (this->standby()) { + ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken"); + return; + } if (!valve_number.has_value() || (valve_number == this->active_valve())) { return; } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_auto_advance(false); + this->set_queue_enable(false); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; - this->fsm_request_(valve_number.value()); + this->fsm_request_(valve_number.value(), run_duration.value_or(0)); } void Sprinkler::queue_valve(optional valve_number, optional run_duration) { @@ -714,8 +866,17 @@ void Sprinkler::next_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = this->next_valve_number_( - this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1))); + this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "next_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows only " + "enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -728,8 +889,17 @@ void Sprinkler::previous_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = - this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0))); + this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "previous_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows " + "only enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -758,7 +928,7 @@ void Sprinkler::pause() { return; // we can't pause if we're already paused or if there is no active valve } this->paused_valve_ = this->active_valve(); - this->resume_duration_ = this->time_remaining(); + this->resume_duration_ = this->time_remaining_active_valve(); this->shutdown(false); ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); @@ -769,7 +939,7 @@ void Sprinkler::resume() { ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); - this->reset_resume_(); + this->reset_resume(); } else { ESP_LOGD(TAG, "No valve to resume!"); } @@ -783,6 +953,11 @@ void Sprinkler::resume_or_start_full_cycle() { } } +void Sprinkler::reset_resume() { + this->paused_valve_.reset(); + this->resume_duration_.reset(); +} + const char *Sprinkler::valve_name(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch->get_name().c_str(); @@ -790,6 +965,13 @@ const char *Sprinkler::valve_name(const size_t valve_number) { return nullptr; } +optional Sprinkler::active_valve_request_is_from() { + if (this->active_req_.has_request()) { + return this->active_req_.request_is_from(); + } + return nullopt; +} + optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } optional Sprinkler::paused_valve() { return this->paused_valve_; } @@ -824,8 +1006,7 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or - // is - // STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now + // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) || ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) { @@ -876,7 +1057,93 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { } } -optional Sprinkler::time_remaining() { +uint32_t Sprinkler::total_cycle_time_all_valves() { + uint32_t total_time_remaining = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + } + + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve)) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve) && !this->valve_cycle_complete_(valve)) { + if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_queue_time() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (auto &valve : this->queued_valves_) { + if (valve.run_duration) { + total_time_remaining += valve.run_duration; + } else { + total_time_remaining += this->valve_run_duration_adjusted(valve.valve_number); + } + valve_count++; + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +optional Sprinkler::time_remaining_active_valve() { if (this->active_req_.has_request()) { // first try to return the value based on active_req_... if (this->active_req_.valve_operator() != nullptr) { return this->active_req_.valve_operator()->time_remaining(); @@ -890,6 +1157,40 @@ optional Sprinkler::time_remaining() { return nullopt; } +optional Sprinkler::time_remaining_current_operation() { + auto total_time_remaining = this->time_remaining_active_valve(); + + if (total_time_remaining.has_value()) { + if (this->auto_advance()) { + total_time_remaining = total_time_remaining.value() + this->total_cycle_time_enabled_incomplete_valves(); + total_time_remaining = + total_time_remaining.value() + + (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0))); + } + + if (this->queue_enabled()) { + total_time_remaining = total_time_remaining.value() + this->total_queue_time(); + } + return total_time_remaining; + } + return nullopt; +} + +bool Sprinkler::any_controller_is_active() { + if (this->state_ != IDLE) { + return true; + } + + for (auto &controller : this->other_controllers_) { + if (controller != this) { // dummy check + if (controller->controller_state() != IDLE) { + return true; + } + } + } + return false; +} + SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch; @@ -925,8 +1226,6 @@ SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { return nullptr; } -uint32_t Sprinkler::hash_base() { return 3129891955UL; } - bool Sprinkler::valve_is_enabled_(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { if (this->valve_[valve_number].enable_switch != nullptr) { @@ -952,30 +1251,60 @@ bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { return false; } -size_t Sprinkler::next_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve + 1 < this->number_of_valves())) - return first_valve + 1; +optional Sprinkler::next_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(0); + size_t start = first_valve.has_value() ? 1 : 0; - return 0; + if (!this->is_a_valid_valve(valve)) { + valve = 0; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve + offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest -= this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } -size_t Sprinkler::previous_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve - 1 >= 0)) - return first_valve - 1; +optional Sprinkler::previous_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(this->number_of_valves() - 1); + size_t start = first_valve.has_value() ? 1 : 0; - return this->number_of_valves() - 1; + if (!this->is_a_valid_valve(valve)) { + valve = this->number_of_valves() - 1; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve - offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest += this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } optional Sprinkler::next_valve_number_in_cycle_(const optional first_valve) { - if (this->reverse_sw_ != nullptr) { - if (this->reverse_sw_->state) { - return this->previous_enabled_incomplete_valve_number_(first_valve); - } + if (this->reverse()) { + return this->previous_valve_number_(first_valve, false, false); } - return this->next_enabled_incomplete_valve_number_(first_valve); + return this->next_valve_number_(first_valve, false, false); } -void Sprinkler::load_next_valve_run_request_(optional first_valve) { +void Sprinkler::load_next_valve_run_request_(const optional first_valve) { if (this->next_req_.has_request()) { if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); @@ -983,58 +1312,37 @@ void Sprinkler::load_next_valve_run_request_(optional first_valve) { return; // there is already a request pending } else if (this->queue_enabled() && !this->queued_valves_.empty()) { this->next_req_.set_valve(this->queued_valves_.back().valve_number); + this->next_req_.set_request_from(QUEUE); if (this->queued_valves_.back().run_duration) { this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); - } else { + this->queued_valves_.pop_back(); + } else if (this->multiplier()) { this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); + this->queued_valves_.pop_back(); + } else { + this->next_req_.reset(); } - this->queued_valves_.pop_back(); - } else if (this->auto_adv_sw_ != nullptr) { - if (this->auto_adv_sw_->state) { - if (this->next_valve_number_in_cycle_(first_valve).has_value()) { - // if there is another valve to run as a part of a cycle, load that - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + } else if (this->auto_advance() && this->multiplier()) { + if (this->next_valve_number_in_cycle_(first_valve).has_value()) { + // if there is another valve to run as a part of a cycle, load that + this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + this->next_req_.set_request_from(CYCLE); + this->next_req_.set_run_duration( + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + } else if ((this->repeat_count_++ < this->repeat().value_or(0))) { + ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, this->repeat().value_or(0) + 1); + // if there are repeats remaining and no more valves were left in the cycle, start a new cycle + this->prep_full_cycle_(); + if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case... + this->next_req_.set_valve(this->next_valve_number_in_cycle_().value_or(0)); + this->next_req_.set_request_from(CYCLE); this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); - } else if ((this->repeat_count_++ < this->target_repeats_.value_or(0))) { - ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, - this->target_repeats_.value_or(0) + 1); - // if there are repeats remaining and no more valves were left in the cycle, start a new cycle - this->prep_full_cycle_(); - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); - this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_().value_or(0))); } } } } -optional Sprinkler::next_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->next_valve_number_(first_valve.value_or(this->number_of_valves() - 1)); - - while (new_valve_number != first_valve.value_or(this->number_of_valves() - 1)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->next_valve_number_(new_valve_number); - } - } - return nullopt; -} - -optional Sprinkler::previous_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->previous_valve_number_(first_valve.value_or(0)); - - while (new_valve_number != first_valve.value_or(0)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->previous_valve_number_(new_valve_number); - } - } - return nullopt; -} - bool Sprinkler::any_valve_is_enabled_() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { if (this->valve_is_enabled_(valve_number)) @@ -1053,8 +1361,9 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); - ESP_LOGD(TAG, "Starting valve %u for %u seconds, cycle %u of %u", req->valve(), run_duration, - this->repeat_count_ + 1, this->target_repeats_.value_or(0) + 1); + ESP_LOGD(TAG, "%s is starting valve %u for %u seconds, cycle %u of %u", + this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1, + this->repeat().value_or(0) + 1); req->set_valve_operator(&vo); vo.set_controller(this); vo.set_valve(&this->valve_[req->valve()]); @@ -1080,15 +1389,14 @@ void Sprinkler::all_valves_off_(const bool include_pump) { } void Sprinkler::prep_full_cycle_() { - if (this->auto_adv_sw_ != nullptr) { - if (!this->auto_adv_sw_->state) { - this->auto_adv_sw_->publish_state(true); - } - } + this->set_auto_advance(true); + if (!this->any_valve_is_enabled_()) { for (auto &valve : this->valve_) { if (valve.enable_switch != nullptr) { - valve.enable_switch->publish_state(true); + if (!valve.enable_switch->state) { + valve.enable_switch->turn_on(); + } } } } @@ -1101,11 +1409,6 @@ void Sprinkler::reset_cycle_states_() { } } -void Sprinkler::reset_resume_() { - this->paused_valve_.reset(); - this->resume_duration_.reset(); -} - void Sprinkler::fsm_request_(size_t requested_valve, uint32_t requested_run_duration) { this->next_req_.set_valve(requested_valve); this->next_req_.set_run_duration(requested_run_duration); @@ -1169,14 +1472,19 @@ void Sprinkler::fsm_transition_() { void Sprinkler::fsm_transition_from_shutdown_() { this->load_next_valve_run_request_(); - this->active_req_.set_valve(this->next_req_.valve()); - this->active_req_.set_run_duration(this->next_req_.run_duration()); - this->next_req_.reset(); - this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); - this->start_timer_(sprinkler::TIMER_SM); - this->start_valve_(&this->active_req_); - this->state_ = ACTIVE; + if (this->next_req_.has_request()) { // there is a valve to run... + this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); + this->active_req_.set_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + this->set_timer_duration_(sprinkler::TIMER_SM, + this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + this->state_ = ACTIVE; + } } void Sprinkler::fsm_transition_from_valve_run_() { @@ -1186,7 +1494,9 @@ void Sprinkler::fsm_transition_from_valve_run_() { } if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished - this->mark_valve_cycle_complete_(this->active_req_.valve()); + if ((this->active_req_.request_is_from() == CYCLE) || (this->active_req_.request_is_from() == USER)) { + this->mark_valve_cycle_complete_(this->active_req_.valve()); + } } else { ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); for (auto &vo : this->valve_op_) { @@ -1201,6 +1511,7 @@ void Sprinkler::fsm_transition_from_valve_run_() { this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); @@ -1230,6 +1541,22 @@ void Sprinkler::fsm_transition_to_shutdown_() { this->start_timer_(sprinkler::TIMER_SM); } +std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { + switch (origin) { + case USER: + return "USER"; + + case CYCLE: + return "CYCLE"; + + case QUEUE: + return "QUEUE"; + + default: + return "UNKNOWN"; + } +} + std::string Sprinkler::state_as_str_(SprinklerState state) { switch (state) { case IDLE: @@ -1300,8 +1627,8 @@ void Sprinkler::dump_config() { if (this->manual_selection_delay_.has_value()) { ESP_LOGCONFIG(TAG, " Manual Selection Delay: %u seconds", this->manual_selection_delay_.value_or(0)); } - if (this->target_repeats_.has_value()) { - ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->target_repeats_.value_or(0)); + if (this->repeat().has_value()) { + ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->repeat().value_or(0)); } if (this->start_delay_) { if (this->start_delay_is_valve_delay_) { @@ -1329,7 +1656,7 @@ void Sprinkler::dump_config() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { ESP_LOGCONFIG(TAG, " Valve %u:", valve_number); ESP_LOGCONFIG(TAG, " Name: %s", this->valve_name(valve_number)); - ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_[valve_number].run_duration); + ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_run_duration(valve_number)); if (this->valve_[valve_number].valve_switch.pulse_duration()) { ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", this->valve_[valve_number].valve_switch.pulse_duration()); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 1243a844fa..7a8285ae73 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -3,11 +3,16 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" +#include + namespace esphome { namespace sprinkler { +const std::string min_str = "min"; + enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! IDLE, // system/valve is off @@ -22,7 +27,14 @@ enum SprinklerTimerIndex : uint8_t { TIMER_VALVE_SELECTION = 1, }; +enum SprinklerValveRunRequestOrigin : uint8_t { + USER, + CYCLE, + QUEUE, +}; + class Sprinkler; // this component +class SprinklerControllerNumber; // number components that appear in the front end; based on number core class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps @@ -74,6 +86,7 @@ struct SprinklerTimer { }; struct SprinklerValve { + SprinklerControllerNumber *run_duration_number; SprinklerControllerSwitch *controller_switch; SprinklerControllerSwitch *enable_switch; SprinklerSwitch valve_switch; @@ -86,6 +99,25 @@ struct SprinklerValve { std::unique_ptr> valve_turn_on_automation; }; +class SprinklerControllerNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + Trigger *get_set_trigger() const { return set_trigger_; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + float initial_value_{NAN}; + bool restore_value_{true}; + Trigger *set_trigger_ = new Trigger(); + + ESPPreferenceObject pref_; +}; + class SprinklerControllerSwitch : public switch_::Switch, public Component { public: SprinklerControllerSwitch(); @@ -94,27 +126,19 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; - void set_optimistic(bool optimistic); - void set_assumed_state(bool assumed_state); void loop() override; float get_setup_priority() const override; protected: - bool assumed_state() override; - void write_state(bool state) override; optional()>> f_; - bool optimistic_{false}; - bool assumed_state_{false}; Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; class SprinklerValveOperator { @@ -158,6 +182,7 @@ class SprinklerValveRunRequest { SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); bool has_request(); bool has_valve_operator(); + void set_request_from(SprinklerValveRunRequestOrigin origin); void set_run_duration(uint32_t run_duration); void set_valve(size_t valve_number); void set_valve_operator(SprinklerValveOperator *valve_op); @@ -166,23 +191,24 @@ class SprinklerValveRunRequest { size_t valve(); optional valve_as_opt(); SprinklerValveOperator *valve_operator(); + SprinklerValveRunRequestOrigin request_is_from(); protected: bool has_valve_{false}; size_t valve_number_{0}; uint32_t run_duration_{0}; SprinklerValveOperator *valve_op_{nullptr}; + SprinklerValveRunRequestOrigin origin_{USER}; }; -class Sprinkler : public Component, public EntityBase { +class Sprinkler : public Component { public: - Sprinkler(); - Sprinkler(const std::string &name); - void setup() override; void loop() override; void dump_config() override; + void set_name(const std::string &name) { this->name_ = name; } + /// add a valve to the controller void add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw = nullptr); @@ -194,6 +220,11 @@ class Sprinkler : public Component, public EntityBase { void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch); void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch); void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch); + void set_controller_standby_switch(SprinklerControllerSwitch *standby_switch); + + /// configure important controller number components + void set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number); + void set_controller_repeat_number(SprinklerControllerNumber *repeat_number); /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); @@ -205,9 +236,18 @@ class Sprinkler : public Component, public EntityBase { void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, switch_::Switch *pump_switch_on, uint32_t pulse_duration); + /// configure a valve's run duration number component + void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number); + + /// sets the multiplier value to '1 / divider' and sets repeat value to divider + void set_divider(optional divider); + /// value multiplied by configured run times -- used to extend or shorten the cycle void set_multiplier(optional multiplier); + /// enable/disable skipping of disabled valves by the next and previous actions + void set_next_prev_ignore_disabled_valves(bool ignore_disabled); + /// set how long the pump should start after the valve (when the pump is starting) void set_pump_start_delay(uint32_t start_delay); @@ -248,6 +288,9 @@ class Sprinkler : public Component, public EntityBase { /// if reverse is true, controller will iterate through all enabled valves in reverse (descending) order void set_reverse(bool reverse); + /// if standby is true, controller will refuse to activate any valves + void set_standby(bool standby); + /// returns valve_number's run duration in seconds uint32_t valve_run_duration(size_t valve_number); @@ -272,6 +315,9 @@ class Sprinkler : public Component, public EntityBase { /// returns true if reverse is enabled bool reverse(); + /// returns true if standby is enabled + bool standby(); + /// starts the controller from the first valve in the queue and disables auto_advance. /// if the queue is empty, does nothing. void start_from_queue(); @@ -281,7 +327,7 @@ class Sprinkler : public Component, public EntityBase { void start_full_cycle(); /// activates a single valve and disables auto_advance. - void start_single_valve(optional valve_number); + void start_single_valve(optional valve_number, optional run_duration = nullopt); /// adds a valve into the queue. queued valves have priority over valves to be run as a part of a full cycle. /// NOTE: queued valves will always run, regardless of auto-advance and/or valve enable switches. @@ -308,9 +354,15 @@ class Sprinkler : public Component, public EntityBase { /// if a cycle was suspended using pause(), resumes it. otherwise calls start_full_cycle() void resume_or_start_full_cycle(); + /// resets resume state + void reset_resume(); + /// returns a pointer to a valve's name string object; returns nullptr if valve_number is invalid const char *valve_name(size_t valve_number); + /// returns what invoked the valve that is currently active, if any. check with 'has_value()' + optional active_valve_request_is_from(); + /// returns the number of the valve that is currently active, if any. check with 'has_value()' optional active_valve(); @@ -336,8 +388,29 @@ class Sprinkler : public Component, public EntityBase { /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller void set_pump_state(SprinklerSwitch *pump_switch, bool state); - /// returns the amount of time remaining in seconds for the active valve, if any. check with 'has_value()' - optional time_remaining(); + /// returns the amount of time in seconds required for all valves + uint32_t total_cycle_time_all_valves(); + + /// returns the amount of time in seconds required for all enabled valves + uint32_t total_cycle_time_enabled_valves(); + + /// returns the amount of time in seconds required for all enabled & incomplete valves, not including the active valve + uint32_t total_cycle_time_enabled_incomplete_valves(); + + /// returns the amount of time in seconds required for all valves in the queue + uint32_t total_queue_time(); + + /// returns the amount of time remaining in seconds for the active valve, if any + optional time_remaining_active_valve(); + + /// returns the amount of time remaining in seconds for all valves remaining, including the active valve, if any + optional time_remaining_current_operation(); + + /// returns true if this or any sprinkler controller this controller knows about is active + bool any_controller_is_active(); + + /// returns the current state of the sprinkler controller + SprinklerState controller_state() { return this->state_; }; /// returns a pointer to a valve's control switch object SprinklerControllerSwitch *control_switch(size_t valve_number); @@ -355,8 +428,6 @@ class Sprinkler : public Component, public EntityBase { SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index); protected: - uint32_t hash_base() override; - /// returns true if valve number is enabled bool valve_is_enabled_(size_t valve_number); @@ -366,9 +437,13 @@ class Sprinkler : public Component, public EntityBase { /// returns true if valve's cycle is flagged as complete bool valve_cycle_complete_(size_t valve_number); - /// returns the number of the next/previous valve in the vector - size_t next_valve_number_(size_t first_valve); - size_t previous_valve_number_(size_t first_valve); + /// returns the number of the next valve in the vector or nullopt if no valves match criteria + optional next_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); + + /// returns the number of the previous valve in the vector or nullopt if no valves match criteria + optional previous_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); /// returns the number of the next valve that should be activated in a full cycle. /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') @@ -380,11 +455,6 @@ class Sprinkler : public Component, public EntityBase { /// if no valve is next (for example, a full cycle is complete), next_req_ is reset via reset(). void load_next_valve_run_request_(optional first_valve = nullopt); - /// returns the number of the next/previous valve that should be activated. - /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') - optional next_enabled_incomplete_valve_number_(optional first_valve); - optional previous_enabled_incomplete_valve_number_(optional first_valve); - /// returns true if any valve is enabled bool any_valve_is_enabled_(); @@ -401,9 +471,6 @@ class Sprinkler : public Component, public EntityBase { /// resets the cycle state for all valves void reset_cycle_states_(); - /// resets resume state - void reset_resume_(); - /// make a request of the state machine void fsm_request_(size_t requested_valve, uint32_t requested_run_duration = 0); @@ -422,7 +489,10 @@ class Sprinkler : public Component, public EntityBase { /// starts up the system from IDLE state void fsm_transition_to_shutdown_(); - /// return the current FSM state as a string + /// return the specified SprinklerValveRunRequestOrigin as a string + std::string req_as_str_(SprinklerValveRunRequestOrigin origin); + + /// return the specified SprinklerState state as a string std::string state_as_str_(SprinklerState state); /// Start/cancel/get status of valve timers @@ -439,11 +509,13 @@ class Sprinkler : public Component, public EntityBase { /// callback functions for timers void valve_selection_callback_(); void sm_timer_callback_(); - void pump_stop_delay_callback_(); /// Maximum allowed queue size const uint8_t max_queue_size_{100}; + /// When set to true, the next and previous actions will skip disabled valves + bool next_prev_ignore_disabled_{false}; + /// Pump should be off during valve_open_delay interval bool pump_switch_off_during_valve_open_delay_{false}; @@ -458,6 +530,8 @@ class Sprinkler : public Component, public EntityBase { uint32_t start_delay_{0}; uint32_t stop_delay_{0}; + std::string name_; + /// Sprinkler controller state SprinklerState state_{IDLE}; @@ -504,9 +578,7 @@ class Sprinkler : public Component, public EntityBase { std::vector valve_op_{2}; /// Valve control timers - std::vector timer_{ - {this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}, - {this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}}; + std::vector timer_{}; /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) std::vector other_controllers_; @@ -516,12 +588,19 @@ class Sprinkler : public Component, public EntityBase { SprinklerControllerSwitch *controller_sw_{nullptr}; SprinklerControllerSwitch *queue_enable_sw_{nullptr}; SprinklerControllerSwitch *reverse_sw_{nullptr}; + SprinklerControllerSwitch *standby_sw_{nullptr}; + + /// Number components we'll present to the front end + SprinklerControllerNumber *multiplier_number_{nullptr}; + SprinklerControllerNumber *repeat_number_{nullptr}; std::unique_ptr> sprinkler_shutdown_action_; + std::unique_ptr> sprinkler_standby_shutdown_action_; std::unique_ptr> sprinkler_resumeorstart_action_; std::unique_ptr> sprinkler_turn_off_automation_; std::unique_ptr> sprinkler_turn_on_automation_; + std::unique_ptr> sprinkler_standby_turn_on_automation_; }; } // namespace sprinkler diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index f2e4ef5811..48143b9e1a 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -27,6 +27,7 @@ MODELS = { "SSD1306_96X16": SSD1306Model.SSD1306_MODEL_96_16, "SSD1306_64X48": SSD1306Model.SSD1306_MODEL_64_48, "SSD1306_64X32": SSD1306Model.SSD1306_MODEL_64_32, + "SSD1306_72X40": SSD1306Model.SSD1306_MODEL_72_40, "SH1106_128X32": SSD1306Model.SH1106_MODEL_128_32, "SH1106_128X64": SSD1306Model.SH1106_MODEL_128_64, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 2ba990637e..730e1c8f35 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -59,6 +59,7 @@ void SSD1306::setup() { // Set Y offset (0xD3) this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(0x00 + this->offset_y_); + // Set start line at line 0 (0x40) this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); @@ -100,6 +101,7 @@ void SSD1306::setup() { case SH1107_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: + case SSD1306_MODEL_72_40: this->command(0x12); break; } @@ -118,6 +120,9 @@ void SSD1306::setup() { case SH1107_MODEL_128_64: this->command(0x35); break; + case SSD1306_MODEL_72_40: + this->command(0x20); + break; default: this->command(0x00); break; @@ -156,6 +161,10 @@ void SSD1306::display() { this->command(0x20 + this->offset_x_); this->command(0x20 + this->offset_x_ + this->get_width_internal() - 1); break; + case SSD1306_MODEL_72_40: + this->command(0x1C + this->offset_x_); + this->command(0x1C + this->offset_x_ + this->get_width_internal() - 1); + break; default: this->command(0 + this->offset_x_); // Page start address, 0 this->command(this->get_width_internal() + this->offset_x_ - 1); @@ -225,6 +234,8 @@ int SSD1306::get_height_internal() { case SSD1306_MODEL_64_48: case SH1106_MODEL_64_48: return 48; + case SSD1306_MODEL_72_40: + return 40; default: return 0; } @@ -246,6 +257,8 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_64_48: case SH1107_MODEL_128_64: return 64; + case SSD1306_MODEL_72_40: + return 72; default: return 0; } @@ -294,6 +307,8 @@ const char *SSD1306::model_str_() { return "SSD1306 96x16"; case SSD1306_MODEL_64_48: return "SSD1306 64x48"; + case SSD1306_MODEL_72_40: + return "SSD1306 72x40"; case SH1106_MODEL_128_32: return "SH1106 128x32"; case SH1106_MODEL_128_64: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 2f5baffb69..7402ae3af2 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -13,6 +13,7 @@ enum SSD1306Model { SSD1306_MODEL_96_16, SSD1306_MODEL_64_48, SSD1306_MODEL_64_32, + SSD1306_MODEL_72_40, SH1106_MODEL_128_32, SH1106_MODEL_128_64, SH1106_MODEL_96_16, diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 64b09c0672..96734eb618 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -53,8 +53,14 @@ void HOT I2CSSD1306::write_display_data() { } } } else { + size_t block_size = 16; + if ((this->get_buffer_length_() & 8) == 8) { + // use smaller block size for e.g. 72x40 displays where buffer size is multiple of 8, not 16 + block_size = 8; + } + for (uint32_t i = 0; i < this->get_buffer_length_();) { - uint8_t data[16]; + uint8_t data[block_size]; for (uint8_t &j : data) j = this->buffer_[i++]; this->write_bytes(0x40, data, sizeof(data)); diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 4cb8d17a3d..4223a013a4 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -76,6 +76,8 @@ void SSD1327::setup() { this->command(0x55); this->command(SSD1327_SETVCOMHVOLTAGE); // Set High Voltage Level of COM Pin this->command(0x1C); + this->command(SSD1327_SETGPIO); // Switch voltage converter on (for Aliexpress display) + this->command(0x03); this->command(SSD1327_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index c276be2f5a..d18e305cc2 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -4,7 +4,6 @@ from esphome import pins from esphome.components import display, spi from esphome.const import ( CONF_BACKLIGHT_PIN, - CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_ID, @@ -69,7 +68,6 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MODEL): ST7789V_MODEL, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, cv.Optional(CONF_HEIGHT): cv.int_, @@ -79,7 +77,7 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.polling_component_schema("5s")) - .extend(spi.spi_device_schema()), + .extend(spi.spi_device_schema(cs_pin_required=False)), validate_st7789v, ) diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index 63fa0ba72f..f336d24e24 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -74,7 +74,7 @@ void ST7920::goto_xy_(uint16_t x, uint16_t y) { void HOT ST7920::write_display_data() { uint8_t i, j, b; - for (j = 0; j < (uint8_t)(this->get_height_internal() / 2); j++) { + for (j = 0; j < (uint8_t) (this->get_height_internal() / 2); j++) { this->goto_xy_(0, j); this->enable(); for (i = 0; i < 16; i++) { // 16 bytes from line #0+ diff --git a/esphome/components/status_led/light/__init__.py b/esphome/components/status_led/light/__init__.py index 8896046998..d6a4a245e6 100644 --- a/esphome/components/status_led/light/__init__.py +++ b/esphome/components/status_led/light/__init__.py @@ -1,26 +1,35 @@ from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import light -from esphome.const import CONF_OUTPUT_ID, CONF_PIN +from esphome.components import light, output +from esphome.const import CONF_OUTPUT, CONF_OUTPUT_ID, CONF_PIN from .. import status_led_ns +AUTO_LOAD = ["output"] + StatusLEDLightOutput = status_led_ns.class_( "StatusLEDLightOutput", light.LightOutput, cg.Component ) -CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(StatusLEDLightOutput), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - } +CONFIG_SCHEMA = cv.All( + light.BINARY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(StatusLEDLightOutput), + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } + ), + cv.has_at_least_one_key(CONF_PIN, CONF_OUTPUT), ) async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - pin = await cg.gpio_pin_expression(config[CONF_PIN]) - cg.add(var.set_pin(pin)) + if CONF_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + if CONF_OUTPUT in config: + out = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) await cg.register_component(var, config) - # cg.add(cg.App.register_component(var)) await light.register_light(var, config) diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index 760c89f972..b47d1f5bd0 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -15,10 +15,10 @@ void StatusLEDLightOutput::loop() { } if ((new_state & STATUS_LED_ERROR) != 0u) { - this->pin_->digital_write(millis() % 250u < 150u); + this->output_state_(millis() % 250u < 150u); this->last_app_state_ = new_state; } else if ((new_state & STATUS_LED_WARNING) != 0u) { - this->pin_->digital_write(millis() % 1500u < 250u); + this->output_state_(millis() % 1500u < 250u); this->last_app_state_ = new_state; } else if (new_state != this->last_app_state_) { // if no error/warning -> restore light state or turn off @@ -26,17 +26,16 @@ void StatusLEDLightOutput::loop() { if (lightstate_) lightstate_->current_values_as_binary(&state); - - this->pin_->digital_write(state); - this->last_app_state_ = new_state; - ESP_LOGD(TAG, "Restoring light state %s", ONOFF(state)); + + this->output_state_(state); + this->last_app_state_ = new_state; } } void StatusLEDLightOutput::setup_state(light::LightState *state) { lightstate_ = state; - ESP_LOGD(TAG, "'%s': Setting initital state", state->get_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting initial state", state->get_name().c_str()); this->write_state(state); } @@ -47,16 +46,18 @@ void StatusLEDLightOutput::write_state(light::LightState *state) { // if in warning/error, don't overwrite the status_led // once it is back to OK, the loop will restore the state if ((App.get_app_state() & (STATUS_LED_ERROR | STATUS_LED_WARNING)) == 0u) { - this->pin_->digital_write(binary); ESP_LOGD(TAG, "'%s': Setting state %s", state->get_name().c_str(), ONOFF(binary)); + this->output_state_(binary); } } void StatusLEDLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up Status LED..."); - this->pin_->setup(); - this->pin_->digital_write(false); + if (this->pin_ != nullptr) { + this->pin_->setup(); + this->pin_->digital_write(false); + } } void StatusLEDLightOutput::dump_config() { @@ -64,5 +65,12 @@ void StatusLEDLightOutput::dump_config() { LOG_PIN(" Pin: ", this->pin_); } +void StatusLEDLightOutput::output_state_(bool state) { + if (this->pin_ != nullptr) + this->pin_->digital_write(state); + if (this->output_ != nullptr) + this->output_->set_state(state); +} + } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index e90d381e3c..e711a2e749 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/light/light_output.h" +#include "esphome/components/output/binary_output.h" namespace esphome { namespace status_led { @@ -10,6 +11,7 @@ namespace status_led { class StatusLEDLightOutput : public light::LightOutput, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_output(output::BinaryOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); @@ -31,9 +33,11 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { float get_loop_priority() const override { return 50.0f; } protected: - GPIOPin *pin_; + GPIOPin *pin_{nullptr}; + output::BinaryOutput *output_{nullptr}; light::LightState *lightstate_{}; uint32_t last_app_state_{0xFFFF}; + void output_state_(bool state); }; } // namespace status_led diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 54f6aa4205..f2367fd62a 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -14,7 +14,6 @@ from esphome.core import CORE, coroutine_with_priority IS_PLATFORM_COMPONENT = True -# pylint: disable=invalid-name stepper_ns = cg.esphome_ns.namespace("stepper") Stepper = stepper_ns.class_("Stepper") diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 5a3da1abbe..b65410cbed 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -66,7 +66,7 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: - if not ignore_missing: + if not ignore_missing and "password" not in path: _LOGGER.warning( "Found '%s' (see %s) which looks like a substitution, but '%s' was " "not declared", diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 113c14d431..5f9179682a 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -269,7 +269,7 @@ struct SunAtLocation { num_t jd = julian_day(date) + added_d; num_t eot = SunAtTime(jd).equation_of_time() * 240; - time_t new_timestamp = (time_t)(date.timestamp + added_d * 86400 - eot); + time_t new_timestamp = (time_t) (date.timestamp + added_d * 86400 - eot); return time::ESPTime::from_epoch_utc(new_timestamp); } }; @@ -287,18 +287,17 @@ HorizontalCoordinate Sun::calc_coords_() { */ return sun.true_coordinate(m); } -optional Sun::calc_event_(bool rising, double zenith) { +optional Sun::calc_event_(time::ESPTime date, bool rising, double zenith) { SunAtLocation sun{location_}; - auto now = this->time_->utcnow(); - if (!now.is_valid()) + if (!date.is_valid()) return {}; // Calculate UT1 timestamp at 0h - auto today = now; + auto today = date; today.hour = today.minute = today.second = 0; today.recalc_timestamp_utc(); auto it = sun.event(rising, today, zenith); - if (it.has_value() && it->timestamp < now.timestamp) { + if (it.has_value() && it->timestamp < date.timestamp) { // We're calculating *next* sunrise/sunset, but calculated event // is today, so try again tomorrow time_t new_timestamp = today.timestamp + 24 * 60 * 60; @@ -307,9 +306,19 @@ optional Sun::calc_event_(bool rising, double zenith) { } return it; } +optional Sun::calc_event_(bool rising, double zenith) { + auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith); + return it; +} optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +optional Sun::sunrise(time::ESPTime date, double elevation) { + return this->calc_event_(date, true, 90 - elevation); +} +optional Sun::sunset(time::ESPTime date, double elevation) { + return this->calc_event_(date, false, 90 - elevation); +} double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index efc6a1ab0a..9547b2f280 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -59,6 +59,8 @@ class Sun { optional sunrise(double elevation); optional sunset(double elevation); + optional sunrise(time::ESPTime date, double elevation); + optional sunset(time::ESPTime date, double elevation); double elevation(); double azimuth(); @@ -66,6 +68,7 @@ class Sun { protected: internal::HorizontalCoordinate calc_coords_(); optional calc_event_(bool rising, double zenith); + optional calc_event_(time::ESPTime date, bool rising, double zenith); time::RealTimeClock *time_; internal::GeoLocation location_; diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 336c7d38d6..21cbe3dfe4 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_MQTT_ID, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_RESTORE_MODE, CONF_TRIGGER_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, @@ -33,6 +34,19 @@ switch_ns = cg.esphome_ns.namespace("switch_") Switch = switch_ns.class_("Switch", cg.EntityBase) SwitchPtr = Switch.operator("ptr") +SwitchRestoreMode = switch_ns.enum("SwitchRestoreMode") + +RESTORE_MODES = { + "RESTORE_DEFAULT_OFF": SwitchRestoreMode.SWITCH_RESTORE_DEFAULT_OFF, + "RESTORE_DEFAULT_ON": SwitchRestoreMode.SWITCH_RESTORE_DEFAULT_ON, + "ALWAYS_OFF": SwitchRestoreMode.SWITCH_ALWAYS_OFF, + "ALWAYS_ON": SwitchRestoreMode.SWITCH_ALWAYS_ON, + "RESTORE_INVERTED_DEFAULT_OFF": SwitchRestoreMode.SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + "RESTORE_INVERTED_DEFAULT_ON": SwitchRestoreMode.SWITCH_RESTORE_INVERTED_DEFAULT_ON, + "DISABLED": SwitchRestoreMode.SWITCH_RESTORE_DISABLED, +} + + ToggleAction = switch_ns.class_("ToggleAction", automation.Action) TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) @@ -50,7 +64,7 @@ SwitchTurnOffTrigger = switch_ns.class_( validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) -SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( +_SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), cv.Optional(CONF_INVERTED): cv.boolean, @@ -78,8 +92,15 @@ def switch_schema( device_class: str = _UNDEF, icon: str = _UNDEF, block_inverted: bool = False, + default_restore_mode: str = "ALWAYS_OFF", ): - schema = SWITCH_SCHEMA + schema = _SWITCH_SCHEMA.extend( + { + cv.Optional(CONF_RESTORE_MODE, default=default_restore_mode): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), + } + ) if class_ is not _UNDEF: schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if entity_category is not _UNDEF: @@ -111,6 +132,9 @@ def switch_schema( return schema +SWITCH_SCHEMA = switch_schema() # for compatibility + + async def setup_switch_core_(var, config): await setup_entity(var, config) @@ -130,6 +154,8 @@ async def setup_switch_core_(var, config): if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + async def register_switch(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 099bd4819b..96611b0b87 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -6,8 +6,7 @@ namespace switch_ { static const char *const TAG = "switch"; -Switch::Switch(const std::string &name) : EntityBase(name), state(false) {} -Switch::Switch() : Switch("") {} +Switch::Switch() : state(false) {} void Switch::turn_on() { ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); @@ -22,18 +21,37 @@ void Switch::toggle() { this->write_state(this->inverted_ == this->state); } optional Switch::get_initial_state() { + if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) + return {}; + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; return initial_state; } +optional Switch::get_initial_state_with_restore_mode() { + if (restore_mode & RESTORE_MODE_DISABLED_MASK) { + return {}; + } + bool initial_state = restore_mode & RESTORE_MODE_ON_MASK; // default value *_OFF or *_ON + if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) { // For RESTORE_* + optional restored_state = this->get_initial_state(); + if (restored_state.has_value()) { + // Invert value if any of the *_INVERTED_* modes + initial_state = restore_mode & RESTORE_MODE_INVERTED_MASK ? !restored_state.value() : restored_state.value(); + } + } + return initial_state; +} void Switch::publish_state(bool state) { if (!this->publish_dedup_.next(state)) return; this->state = state != this->inverted_; - this->rtc_.save(&this->state); + if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) + this->rtc_.save(&this->state); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); } @@ -45,12 +63,34 @@ void Switch::add_on_state_callback(std::function &&callback) { void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } bool Switch::is_inverted() const { return this->inverted_; } -std::string Switch::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return ""; +void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj) { + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + if (!obj->get_icon().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + } + if (obj->assumed_state()) { + ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); + } + if (obj->is_inverted()) { + ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); + } + if (!obj->get_device_class().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + } + const LogString *onoff = LOG_STR(""), *inverted = onoff, *restore; + if (obj->restore_mode & RESTORE_MODE_DISABLED_MASK) { + restore = LOG_STR("disabled"); + } else { + onoff = obj->restore_mode & RESTORE_MODE_ON_MASK ? LOG_STR("ON") : LOG_STR("OFF"); + inverted = obj->restore_mode & RESTORE_MODE_INVERTED_MASK ? LOG_STR("inverted ") : LOG_STR(""); + restore = obj->restore_mode & RESTORE_MODE_PERSISTENT_MASK ? LOG_STR("restore defaults to") : LOG_STR("always"); + } + + ESP_LOGCONFIG(tag, "%s Restore Mode: %s%s %s", prefix, LOG_STR_ARG(inverted), LOG_STR_ARG(restore), + LOG_STR_ARG(onoff)); + } } -void Switch::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } } // namespace switch_ } // namespace esphome diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index c521c4024b..9daac4ee23 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -8,32 +8,30 @@ namespace esphome { namespace switch_ { -#define LOG_SWITCH(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - if ((obj)->assumed_state()) { \ - ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ - } \ - if ((obj)->is_inverted()) { \ - ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ - } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - } +// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled +const int RESTORE_MODE_ON_MASK = 0x01; +const int RESTORE_MODE_PERSISTENT_MASK = 0x02; +const int RESTORE_MODE_INVERTED_MASK = 0x04; +const int RESTORE_MODE_DISABLED_MASK = 0x08; + +enum SwitchRestoreMode { + SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, + SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK, + SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK, + SWITCH_RESTORE_DEFAULT_ON = RESTORE_MODE_PERSISTENT_MASK | RESTORE_MODE_ON_MASK, + SWITCH_RESTORE_INVERTED_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK | RESTORE_MODE_INVERTED_MASK, + SWITCH_RESTORE_INVERTED_DEFAULT_ON = RESTORE_MODE_PERSISTENT_MASK | RESTORE_MODE_INVERTED_MASK | RESTORE_MODE_ON_MASK, + SWITCH_RESTORE_DISABLED = RESTORE_MODE_DISABLED_MASK, +}; /** Base class for all switches. * * A switch is basically just a combination of a binary sensor (for reporting switch values) * and a write_state method that writes a state to the hardware. */ -class Switch : public EntityBase { +class Switch : public EntityBase, public EntityBase_DeviceClass { public: explicit Switch(); - explicit Switch(const std::string &name); /** Publish a state to the front-end from the back-end. * @@ -47,6 +45,9 @@ class Switch : public EntityBase { /// The current reported state of the binary sensor. bool state; + /// Indicates whether or not state is to be retrieved from flash and how + SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF}; + /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. @@ -80,8 +81,19 @@ class Switch : public EntityBase { */ void add_on_state_callback(std::function &&callback); + /** Returns the initial state of the switch, as persisted previously, + or empty if never persisted. + */ optional get_initial_state(); + /** Returns the initial state of the switch, after applying restore mode rules. + * If restore mode is disabled, this function will return an optional with no value + * (.has_value() is false), leaving it up to the component to decide the state. + * For example, the component could read the state from hardware and determine the current + * state. + */ + optional get_initial_state_with_restore_mode(); + /** Return whether this switch uses an assumed state - i.e. if both the ON/OFF actions should be displayed in Home * Assistant because the real state is unknown. * @@ -91,10 +103,7 @@ class Switch : public EntityBase { bool is_inverted() const; - /// Get the device class for this switch. - std::string get_device_class(); - /// Set the Home Assistant device class for this switch. - void set_device_class(const std::string &device_class); + void set_restore_mode(SwitchRestoreMode restore_mode) { this->restore_mode = restore_mode; } protected: /** Write the given state to hardware. You should implement this @@ -111,8 +120,10 @@ class Switch : public EntityBase { bool inverted_{false}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; - optional device_class_; }; +#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) +void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj); + } // namespace switch_ } // namespace esphome diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 60cbae6aa6..d0a84b99ff 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -42,6 +42,9 @@ void SX1509Component::dump_config() { void SX1509Component::loop() { if (this->has_keypad_) { + if (millis() - this->last_loop_timestamp_ < min_loop_period_) + return; + this->last_loop_timestamp_ = millis(); uint16_t key_data = this->read_key_data(); for (auto *binary_sensor : this->keypad_binary_sensors_) binary_sensor->process(key_data); diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 5f0697b534..8e3b41e233 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -6,6 +6,8 @@ #include "sx1509_gpio_pin.h" #include "sx1509_registers.h" +#include + namespace esphome { namespace sx1509 { @@ -67,6 +69,9 @@ class SX1509Component : public Component, public i2c::I2CDevice { uint8_t debounce_time_ = 1; std::vector keypad_binary_sensors_; + uint32_t last_loop_timestamp_ = 0; + const uint32_t min_loop_period_ = 15; // ms + void setup_keypad_(); void set_debounce_config_(uint8_t config_value); void set_debounce_time_(uint8_t time); diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index 2c6e0b0c32..56b51ae311 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -13,7 +13,7 @@ bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pi void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } std::string SX1509GPIOPin::dump_summary() const { char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_); + snprintf(buffer, sizeof(buffer), "%u via sx1509", pin_); return buffer; } diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 276bf65ebf..88c59eb761 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -287,7 +287,7 @@ void TCS34725Component::update() { } // calculate register value from timing - uint8_t regval_atime = (uint8_t)(256.f - integration_time_next / 2.4f); + uint8_t regval_atime = (uint8_t) (256.f - integration_time_next / 2.4f); ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d", this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_); diff --git a/esphome/components/tee501/__init__.py b/esphome/components/tee501/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tee501/sensor.py b/esphome/components/tee501/sensor.py new file mode 100644 index 0000000000..329fc724bd --- /dev/null +++ b/esphome/components/tee501/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +tee501_ns = cg.esphome_ns.namespace("tee501") + +TEE501Component = tee501_ns.class_( + "TEE501Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + TEE501Component, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp new file mode 100644 index 0000000000..22329d40cd --- /dev/null +++ b/esphome/components/tee501/tee501.cpp @@ -0,0 +1,85 @@ +#include "tee501.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tee501 { + +static const char *const TAG = "tee501"; + +void TEE501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TEE501..."); + uint8_t address[] = {0x70, 0x29}; + this->write(address, 2, false); + uint8_t identification[9]; + this->read(identification, 9); + if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +} + +void TEE501Component::dump_config() { + ESP_LOGCONFIG(TAG, "TEE501:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with TEE501 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "TEE501", this); +} + +float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } +void TEE501Component::update() { + uint8_t address_1[] = {0x2C, 0x1B}; + this->write(address_1, 2, true); + this->set_timeout(50, [this]() { + uint8_t i2c_response[3]; + this->read(i2c_response, 3); + if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return; + } + float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]); + if (temperature > 55536) { + temperature = (temperature - 65536) / 100; + } else { + temperature = temperature / 100; + } + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + }); +} + +unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { + unsigned char crc_val = 0xFF; + unsigned char i = 0; + unsigned char j = 0; + for (i = from; i <= to; i++) { + int cur_val = buf[i]; + for (j = 0; j < 8; j++) { + if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal + { + crc_val = ((crc_val << 1) ^ 0x31); + } else { + crc_val = (crc_val << 1); + } + cur_val = cur_val << 1; + } + } + return crc_val; +} + +} // namespace tee501 +} // namespace esphome diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h new file mode 100644 index 0000000000..fc655e58c9 --- /dev/null +++ b/esphome/components/tee501/tee501.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tee501 { + +/// This class implements support for the tee501 of temperature i2c sensors. +class TEE501Component : 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: + unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace tee501 +} // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h index 2be34cfb78..0c6217853e 100644 --- a/esphome/components/teleinfo/teleinfo.h +++ b/esphome/components/teleinfo/teleinfo.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include + namespace esphome { namespace teleinfo { /* diff --git a/esphome/components/template/button/__init__.py b/esphome/components/template/button/__init__.py index 2ad5e54c80..e0101dfc8f 100644 --- a/esphome/components/template/button/__init__.py +++ b/esphome/components/template/button/__init__.py @@ -1,15 +1,10 @@ -import esphome.config_validation as cv from esphome.components import button from .. import template_ns TemplateButton = template_ns.class_("TemplateButton", button.Button) -CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateButton), - } -) +CONFIG_SCHEMA = button.button_schema(TemplateButton) async def to_code(config): diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index a628da70d2..8844ddd6ab 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -73,6 +73,7 @@ async def to_code(config): await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) + cg.add(var.set_has_stop(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 47c651e643..b16e439943 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -109,6 +109,7 @@ void TemplateCover::control(const CoverCall &call) { CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); + traits.set_supports_stop(this->has_stop_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -116,6 +117,7 @@ CoverTraits TemplateCover::get_traits() { Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 3b9dcea50b..4ff5caf1db 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -26,6 +26,7 @@ class TemplateCover : public cover::Cover, public Component { void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_tilt_lambda(std::function()> &&tilt_f); + void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } @@ -48,6 +49,7 @@ class TemplateCover : public cover::Cover, public Component { bool optimistic_{false}; Trigger<> *open_trigger_; Trigger<> *close_trigger_; + bool has_stop_{false}; Trigger<> *stop_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 3dec7066d3..b9a507c7e9 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -46,9 +46,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend( + number.number_schema(TemplateNumber) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateNumber), cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, @@ -58,7 +58,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_VALUE): cv.float_, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate_min_max, validate, ) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 4eba77119d..d116cbb8ae 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -43,9 +43,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - select.SELECT_SCHEMA.extend( + select.select_schema(TemplateSelect) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateSelect), cv.Required(CONF_OPTIONS): cv.All( cv.ensure_list(cv.string_strict), cv.Length(min=1) ), @@ -55,7 +55,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate, ) diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index b3e545d3e9..5db346b99f 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -43,15 +43,16 @@ void TemplateSwitch::setup() { if (!this->restore_state_) return; - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; + optional initial_state = this->get_initial_state_with_restore_mode(); - ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); - if (*restored) { - this->turn_on(); - } else { - this->turn_off(); + if (initial_state.has_value()) { + ESP_LOGD(TAG, " Restored state %s", ONOFF(initial_state.value())); + // if it has a value, restore_mode is not "DISABLED", therefore act on the switch: + if (initial_state.value()) { + this->turn_on(); + } else { + this->turn_off(); + } } } void TemplateSwitch::dump_config() { diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index dd3dbddc43..3ed6b72d8f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -23,7 +23,6 @@ from esphome.util import Registry IS_PLATFORM_COMPONENT = True -# pylint: disable=invalid-name text_sensor_ns = cg.esphome_ns.namespace("text_sensor") TextSensor = text_sensor_ns.class_("TextSensor", cg.EntityBase) TextSensorPtr = TextSensor.operator("ptr") diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index ba77913d1b..80edae2b6c 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -51,7 +51,7 @@ optional ToUpperFilter::new_value(std::string value) { // ToLowerFilter optional ToLowerFilter::new_value(std::string value) { for (char &c : value) - c = ::toupper(c); + c = ::tolower(c); return value; } diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 38f35e6172..4e36532945 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace esphome { namespace text_sensor { diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index d76ab7e27d..f10cd50267 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -6,9 +6,6 @@ namespace text_sensor { static const char *const TAG = "text_sensor"; -TextSensor::TextSensor() : TextSensor("") {} -TextSensor::TextSensor(const std::string &name) : EntityBase(name) {} - void TextSensor::publish_state(const std::string &state) { this->raw_state = state; this->raw_callback_.call(state); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 340f7ff9ed..996af02f7e 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -5,6 +5,8 @@ #include "esphome/core/helpers.h" #include "esphome/components/text_sensor/filter.h" +#include + namespace esphome { namespace text_sensor { @@ -19,11 +21,15 @@ namespace text_sensor { } \ } +#define SUB_TEXT_SENSOR(name) \ + protected: \ + text_sensor::TextSensor *name##_text_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } + class TextSensor : public EntityBase { public: - explicit TextSensor(); - explicit TextSensor(const std::string &name); - /// Getter-syntax for .state. std::string get_state() const; /// Getter-syntax for .raw_state @@ -52,6 +58,10 @@ class TextSensor : public EntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) + /** Override this method to set the unique ID of this sensor. + * + * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). + */ virtual std::string unique_id(); bool has_state(); diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 5e26e6d6de..9a57f6a337 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, CONF_FAN_ONLY_COOLING, @@ -69,6 +70,8 @@ from esphome.const import ( ) CONF_PRESET_CHANGE = "preset_change" +CONF_DEFAULT_PRESET = "default_preset" +CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from" CODEOWNERS = ["@kbx81"] @@ -80,6 +83,13 @@ ThermostatClimate = thermostat_ns.class_( ThermostatClimateTargetTempConfig = thermostat_ns.struct( "ThermostatClimateTargetTempConfig" ) +OnBootRestoreFrom = thermostat_ns.enum("OnBootRestoreFrom") +ON_BOOT_RESTORE_FROM = { + "MEMORY": OnBootRestoreFrom.MEMORY, + "DEFAULT_PRESET": OnBootRestoreFrom.DEFAULT_PRESET, +} +validate_on_boot_restore_from = cv.enum(ON_BOOT_RESTORE_FROM, upper=True) + ClimateMode = climate_ns.enum("ClimateMode") CLIMATE_MODES = { "OFF": ClimateMode.CLIMATE_MODE_OFF, @@ -125,6 +135,17 @@ def validate_temperature_preset(preset, root_config, name, requirements): ) +def generate_comparable_preset(config, name): + comparable_preset = f"{CONF_PRESET}:\n" f" - {CONF_NAME}: {name}\n" + + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_LOW}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]}\n" + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_HIGH}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]}\n" + + return comparable_preset + + def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { @@ -253,6 +274,7 @@ def validate_thermostat(config): CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, ], } for req_config_item, config_triggers in requirements.items(): @@ -277,13 +299,32 @@ def validate_thermostat(config): CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], } - # Validate temperature requirements for default configuraation - validate_temperature_preset(config, config, "default", requirements) + # Legacy high/low configs + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + comparable_preset = generate_comparable_preset(config, "Your new preset") - # Validate temperature requirements for away configuration + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} is no longer valid. Please switch to using a preset for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + comparable_preset = generate_comparable_preset(config, "Your new preset") + + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} is no longer valid. Please switch to using a preset for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) + + # Legacy away mode - raise an error instructing the user to switch to presets if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] - validate_temperature_preset(away, config, "away", requirements) + comparable_preset = generate_comparable_preset(config[CONF_AWAY_CONFIG], "Away") + + raise cv.Invalid( + f"{CONF_AWAY_CONFIG} is no longer valid. Please switch to using a preset named " + "Away" + " for an equivalent experience.\nEquivalent configuration:\n\n" + f"{comparable_preset}" + ) # Validate temperature requirements for presets if CONF_PRESET in config: @@ -292,7 +333,12 @@ def validate_thermostat(config): preset_config, config, preset_config[CONF_NAME], requirements ) - # Verify default climate mode is valid given above configuration + # Warn about using the removed CONF_DEFAULT_MODE and advise users + if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None: + raise cv.Invalid( + f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}." + ) + default_mode = config[CONF_DEFAULT_MODE] requirements = { "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], @@ -369,6 +415,7 @@ def validate_thermostat(config): "MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION], "FOCUS": [CONF_FAN_MODE_FOCUS_ACTION], "DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION], + "QUIET": [CONF_FAN_MODE_QUIET_ACTION], } for preset_config in config[CONF_PRESET]: @@ -403,6 +450,38 @@ def validate_thermostat(config): f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" ) + # If a default preset is requested then ensure that preset is defined + if CONF_DEFAULT_PRESET in config: + default_preset = config[CONF_DEFAULT_PRESET] + + if CONF_PRESET not in config: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} is specified but no presets are defined" + ) + + presets = config[CONF_PRESET] + found_preset = False + + for preset in presets: + if preset[CONF_NAME] == default_preset: + found_preset = True + break + + if found_preset is False: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} set to '{default_preset}' but no such preset has been defined. Available presets: {[preset[CONF_NAME] for preset in presets]}" + ) + + # If restoring default preset on boot is true then ensure we have a default preset + if ( + CONF_ON_BOOT_RESTORE_FROM in config + and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + ): + if CONF_DEFAULT_PRESET not in config: + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" + ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" @@ -424,12 +503,13 @@ def validate_thermostat(config): CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, ] for config_req_action in requirements: if config_req_action in config: return config raise cv.Invalid( - f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" + f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION}, {CONF_FAN_MODE_QUIET_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" ) return config @@ -487,6 +567,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation( single=True ), + cv.Optional(CONF_FAN_MODE_QUIET_ACTION): automation.validate_automation( + single=True + ), cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation( single=True ), @@ -502,9 +585,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_TARGET_TEMPERATURE_CHANGE_ACTION ): automation.validate_automation(single=True), - cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( - validate_climate_mode - ), + cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, + cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( @@ -542,6 +624,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), + cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( single=True ), @@ -564,9 +647,10 @@ async def to_code(config): CONF_COOL_ACTION in config or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) ) + if two_points_available: + cg.add(var.set_supports_two_points(True)) sens = await cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) cg.add( var.set_set_point_minimum_differential( config[CONF_SET_POINT_MINIMUM_DIFFERENTIAL] @@ -579,23 +663,6 @@ async def to_code(config): cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) - if two_points_available is True: - cg.add(var.set_supports_two_points(True)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: - cg.add(var.set_supports_two_points(False)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: - cg.add(var.set_supports_two_points(False)) - normal_config = ThermostatClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) - if CONF_MAX_COOLING_RUN_TIME in config: cg.add( var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) @@ -661,7 +728,6 @@ async def to_code(config): cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) - cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config)) await automation.build_automation( var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] @@ -777,6 +843,11 @@ async def to_code(config): var.get_fan_mode_diffuse_trigger(), [], config[CONF_FAN_MODE_DIFFUSE_ACTION] ) cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_FAN_MODE_QUIET_ACTION in config: + await automation.build_automation( + var.get_fan_mode_quiet_trigger(), [], config[CONF_FAN_MODE_QUIET_ACTION] + ) + cg.add(var.set_supports_fan_mode_quiet(True)) if CONF_SWING_BOTH_ACTION in config: await automation.build_automation( var.get_swing_mode_both_trigger(), [], config[CONF_SWING_BOTH_ACTION] @@ -808,27 +879,8 @@ async def to_code(config): config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], ) - if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] - - if two_points_available is True: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: - away_config = ThermostatClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) - cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config)) - if CONF_PRESET in config: for preset_config in config[CONF_PRESET]: - name = preset_config[CONF_NAME] standard_preset = None if name.upper() in climate.CLIMATE_PRESETS: @@ -872,6 +924,19 @@ async def to_code(config): else: cg.add(var.set_custom_preset_config(name, preset_target_variable)) + if CONF_DEFAULT_PRESET in config: + default_preset_name = config[CONF_DEFAULT_PRESET] + + # if the name is a built in preset use the appropriate naming format + if default_preset_name.upper() in climate.CLIMATE_PRESETS: + climate_preset = climate.CLIMATE_PRESETS[default_preset_name.upper()] + cg.add(var.set_default_preset(climate_preset)) + else: + cg.add(var.set_default_preset(default_preset_name)) + + if CONF_ON_BOOT_RESTORE_FROM in config: + cg.add(var.set_on_boot_restore_from(config[CONF_ON_BOOT_RESTORE_FROM])) + if CONF_PRESET_CHANGE in config: await automation.build_automation( var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index dc4e1e437e..51da663a0c 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -25,15 +25,27 @@ void ThermostatClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - // restore all climate data, if possible - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->to_call(this).perform(); - } else { - // restore from defaults, change_away handles temps for us - this->mode = this->default_mode_; - this->change_preset_(climate::CLIMATE_PRESET_HOME); + + auto use_default_preset = true; + + if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + use_default_preset = false; + restore->to_call(this).perform(); + } } + + // Either we failed to restore state or the user has requested we always apply the default preset + if (use_default_preset) { + if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { + this->change_preset_(this->default_preset_); + } else if (!this->default_custom_preset_.empty()) { + this->change_custom_preset_(this->default_custom_preset_); + } + } + // refresh the climate action based on the restored settings, we'll publish_state() later this->switch_to_action_(this->compute_action_(), false); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); @@ -235,6 +247,8 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); if (supports_fan_mode_diffuse_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); + if (supports_fan_mode_quiet_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); if (supports_swing_mode_both_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); @@ -582,6 +596,10 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo trig = this->fan_mode_diffuse_trigger_; ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); break; + case climate::CLIMATE_FAN_QUIET: + trig = this->fan_mode_quiet_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_QUIET mode"); + break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value @@ -923,9 +941,9 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::dump_preset_config_(const std::string &preset, - const ThermostatClimateTargetTempConfig &config) { - const auto *preset_name = preset.c_str(); +void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, + bool is_default_preset) { + ESP_LOGCONFIG(TAG, " %s Is Default: %s", preset_name, YESNO(is_default_preset)); if (this->supports_heat_) { if (this->supports_two_points_) { @@ -962,9 +980,19 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || + this->preset.value() != preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } else { + ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } this->custom_preset.reset(); this->preset = preset; } else { @@ -976,9 +1004,19 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str()); - this->change_preset_internal_(config->second); + ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || + this->custom_preset.value() != custom_preset) { + // Fire any preset changed trigger if defined + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + this->refresh(); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + } else { + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + } this->preset.reset(); this->custom_preset = custom_preset; } else { @@ -986,39 +1024,46 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) } } -void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { +bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { + bool something_changed = false; + if (this->supports_two_points_) { - this->target_temperature_low = config.default_temperature_low; - this->target_temperature_high = config.default_temperature_high; + if (this->target_temperature_low != config.default_temperature_low) { + this->target_temperature_low = config.default_temperature_low; + something_changed = true; + } + if (this->target_temperature_high != config.default_temperature_high) { + this->target_temperature_high = config.default_temperature_high; + something_changed = true; + } } else { - this->target_temperature = config.default_temperature; + if (this->target_temperature != config.default_temperature) { + this->target_temperature = config.default_temperature; + something_changed = true; + } } - // Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call - // also specifies them then the control's version will override these for that call - if (config.mode_.has_value()) { - this->mode = *config.mode_; + // Note: The mode, fan_mode and swing_mode can all be defined in the preset but if the climate.control call + // also specifies them then the climate.control call's values will override the preset's values for that call + if (config.mode_.has_value() && (this->mode != config.mode_.value())) { ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + this->mode = *config.mode_; + something_changed = true; } - if (config.fan_mode_.has_value()) { - this->fan_mode = *config.fan_mode_; + if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + this->fan_mode = *config.fan_mode_; + something_changed = true; } - if (config.swing_mode_.has_value()) { + if (config.swing_mode_.has_value() && (this->swing_mode != config.swing_mode_.value())) { ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); this->swing_mode = *config.swing_mode_; + something_changed = true; } - // Fire any preset changed trigger if defined - if (this->preset != preset) { - Trigger<> *trig = this->preset_change_trigger_; - assert(trig != nullptr); - trig->trigger(); - } - - this->refresh(); + return something_changed; } void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, @@ -1054,6 +1099,7 @@ ThermostatClimate::ThermostatClimate() fan_mode_middle_trigger_(new Trigger<>()), fan_mode_focus_trigger_(new Trigger<>()), fan_mode_diffuse_trigger_(new Trigger<>()), + fan_mode_quiet_trigger_(new Trigger<>()), swing_mode_both_trigger_(new Trigger<>()), swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), @@ -1061,7 +1107,15 @@ ThermostatClimate::ThermostatClimate() temperature_change_trigger_(new Trigger<>()), preset_change_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } +void ThermostatClimate::set_default_preset(const std::string &custom_preset) { + this->default_custom_preset_ = custom_preset; +} + +void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } + +void ThermostatClimate::set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from) { + this->on_boot_restore_from_ = on_boot_restore_from; +} void ThermostatClimate::set_set_point_minimum_differential(float differential) { this->set_point_minimum_differential_ = differential; } @@ -1161,6 +1215,9 @@ void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; } +void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) { + this->supports_fan_mode_quiet_ = supports_fan_mode_quiet; +} void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { this->supports_swing_mode_both_ = supports_swing_mode_both; } @@ -1203,6 +1260,7 @@ Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->f Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() const { return this->fan_mode_quiet_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } @@ -1213,8 +1271,9 @@ Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->p void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); - if (this->supports_two_points_) + if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + } ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:"); @@ -1246,7 +1305,8 @@ void ThermostatClimate::dump_config() { } if (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_) { + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || + this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); } @@ -1259,10 +1319,12 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s", YESNO(this->supports_fan_only_action_uses_fan_mode_timer_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_COOLING: %s", YESNO(this->supports_fan_only_cooling_)); - if (this->supports_cool_) + if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); - if (this->supports_heat_) + } + if (this->supports_heat_) { ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); + } ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); @@ -1273,6 +1335,7 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE QUIET: %s", YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); @@ -1284,7 +1347,7 @@ void ThermostatClimate::dump_config() { const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second); + this->dump_preset_config_(preset_name, it.second, it.first == this->default_preset_); } ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); @@ -1292,8 +1355,10 @@ void ThermostatClimate::dump_config() { const auto *preset_name = it.first.c_str(); ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second); + this->dump_preset_config_(preset_name, it.second, it.first == this->default_custom_preset_); } + ESP_LOGCONFIG(TAG, " On boot, restore from: %s", + this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY"); } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index a5498dc53d..677b4ad324 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -4,7 +4,9 @@ #include "esphome/core/automation.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" + #include +#include namespace esphome { namespace thermostat { @@ -22,6 +24,7 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; +enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; struct ThermostatClimateTimer { const std::string name; bool active; @@ -57,7 +60,9 @@ class ThermostatClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_default_mode(climate::ClimateMode default_mode); + void set_default_preset(const std::string &custom_preset); + void set_default_preset(climate::ClimatePreset preset); + void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -96,6 +101,7 @@ class ThermostatClimate : public climate::Climate, public Component { 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); + void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet); void set_supports_swing_mode_both(bool supports_swing_mode_both); void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); void set_supports_swing_mode_off(bool supports_swing_mode_off); @@ -127,6 +133,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_fan_mode_middle_trigger() const; Trigger<> *get_fan_mode_focus_trigger() const; Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_fan_mode_quiet_trigger() const; Trigger<> *get_swing_mode_both_trigger() const; Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; @@ -165,7 +172,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset - void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); + /// Returns true if something was changed + bool change_preset_internal_(const ThermostatClimateTargetTempConfig &config); /// Return the traits of this controller. climate::ClimateTraits traits() override; @@ -225,7 +233,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool supplemental_cooling_required_(); bool supplemental_heating_required_(); - void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config); + void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, + bool is_default_preset); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -270,6 +279,7 @@ class ThermostatClimate : public climate::Climate, public Component { bool supports_fan_mode_middle_{false}; bool supports_fan_mode_focus_{false}; bool supports_fan_mode_diffuse_{false}; + bool supports_fan_mode_quiet_{false}; /// Whether the controller supports various swing modes. /// @@ -365,6 +375,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the fan to "diffuse" position. Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + /// The trigger to call when the controller should switch the fan to "quiet" position. + Trigger<> *fan_mode_quiet_trigger_{nullptr}; + /// The trigger to call when the controller should switch the swing mode to "both". Trigger<> *swing_mode_both_trigger_{nullptr}; @@ -397,7 +410,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// These are used to determine when a trigger/action needs to be called climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; - climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; @@ -441,6 +453,15 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; + + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; + + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 7e16d7141f..af2b6c720c 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -6,6 +6,8 @@ namespace esphome { namespace time { static const char *const TAG = "automation"; +static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider + // there has been a drastic time synchronization void CronTrigger::add_second(uint8_t second) { this->seconds_[second] = true; } void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; } @@ -23,12 +25,17 @@ void CronTrigger::loop() { return; if (this->last_check_.has_value()) { - if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > 900) { + if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { // We went back in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped back!"); } else if (*this->last_check_ >= time) { // already handled this one return; + } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + // We went ahead in time (a lot), probably caused by time synchronization + ESP_LOGW(TAG, "Time has jumped ahead!"); + this->last_check_ = time; + return; } while (true) { diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 6167aac4f7..e97413e420 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -4,6 +4,8 @@ #include "esphome/core/automation.h" #include "real_time_clock.h" +#include + namespace esphome { namespace time { diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 7b5f0aa49b..de76676a4d 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -4,6 +4,9 @@ #ifdef USE_ESP8266 #include "sys/time.h" #endif +#ifdef USE_RP2040 +#include +#endif #include namespace esphome { diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index 9625781c96..a14a08ccad 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -16,6 +16,7 @@ time_based_ns = cg.esphome_ns.namespace("time_based") TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component) CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" +CONF_MANUAL_CONTROL = "manual_control" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -26,6 +27,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_MANUAL_CONTROL, default=False): cv.boolean, cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -51,4 +53,5 @@ async def to_code(config): ) cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) + cg.add(var.set_manual_control(config[CONF_MANUAL_CONTROL])) 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 522252e907..50376224a9 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,6 +51,7 @@ void TimeBasedCover::loop() { float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; } CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); traits.set_supports_toggle(true); traits.set_is_assumed_state(this->assumed_state_); @@ -79,6 +80,14 @@ void TimeBasedCover::control(const CoverCall &call) { auto pos = *call.get_position(); if (pos == this->position) { // already at target + if (this->manual_control_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { + // for covers with manual control switch, we can't rely on the computed position, so if + // the command triggered again, we'll assume it's in the opposite direction anyway. + auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; + this->position = pos == COVER_CLOSED ? COVER_OPEN : COVER_CLOSED; + this->target_position_ = pos; + this->start_direction_(op); + } // for covers with built in end stop, we should send the command again if (this->has_built_in_endstop_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 517ab77cb3..b7a826d237 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_manual_control(bool value) { this->manual_control_ = value; } void set_assumed_state(bool value) { this->assumed_state_ = value; } protected: @@ -44,6 +45,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 manual_control_{false}; bool assumed_state_{false}; cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; diff --git a/esphome/components/tm1621/__init__.py b/esphome/components/tm1621/__init__.py new file mode 100644 index 0000000000..2e88d4f366 --- /dev/null +++ b/esphome/components/tm1621/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/tm1621/display.py b/esphome/components/tm1621/display.py new file mode 100644 index 0000000000..edbc5f6928 --- /dev/null +++ b/esphome/components/tm1621/display.py @@ -0,0 +1,47 @@ +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_DATA_PIN, + CONF_CS_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_READ_PIN, + CONF_WRITE_PIN, +) + +tm1621_ns = cg.esphome_ns.namespace("tm1621") +TM1621Display = tm1621_ns.class_("TM1621Display", cg.PollingComponent) +TM1621DisplayRef = TM1621Display.operator("ref") + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1621Display), + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_READ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WRITE_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.polling_component_schema("1s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await display.register_display(var, config) + + cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(cs)) + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + read = await cg.gpio_pin_expression(config[CONF_READ_PIN]) + cg.add(var.set_read_pin(read)) + write = await cg.gpio_pin_expression(config[CONF_WRITE_PIN]) + cg.add(var.set_write_pin(write)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(TM1621DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp new file mode 100644 index 0000000000..ebaa5a3457 --- /dev/null +++ b/esphome/components/tm1621/tm1621.cpp @@ -0,0 +1,283 @@ +#include "tm1621.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tm1621 { + +static const char *const TAG = "tm1621"; + +const uint8_t TM1621_PULSE_WIDTH = 10; // microseconds (Sonoff = 100) + +const uint8_t TM1621_SYS_EN = 0x01; // 0b00000001 +const uint8_t TM1621_LCD_ON = 0x03; // 0b00000011 +const uint8_t TM1621_TIMER_DIS = 0x04; // 0b00000100 +const uint8_t TM1621_WDT_DIS = 0x05; // 0b00000101 +const uint8_t TM1621_TONE_OFF = 0x08; // 0b00001000 +const uint8_t TM1621_BIAS = 0x29; // 0b00101001 = LCD 1/3 bias 4 commons option +const uint8_t TM1621_IRQ_DIS = 0x80; // 0b100x0xxx + +enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; + +const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, + TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; + +const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +// 0 1 2 3 4 5 6 7 8 9 - off +const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, + {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; + +void TM1621Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1621..."); + + this->cs_pin_->setup(); // OUTPUT + this->cs_pin_->digital_write(true); + this->data_pin_->setup(); // OUTPUT + this->data_pin_->digital_write(true); + this->read_pin_->setup(); // OUTPUT + this->read_pin_->digital_write(true); + this->write_pin_->setup(); // OUTPUT + this->write_pin_->digital_write(true); + + this->state_ = 100; + + this->cs_pin_->digital_write(false); + delayMicroseconds(80); + this->read_pin_->digital_write(false); + delayMicroseconds(15); + this->write_pin_->digital_write(false); + delayMicroseconds(25); + this->data_pin_->digital_write(false); + delayMicroseconds(TM1621_PULSE_WIDTH); + this->data_pin_->digital_write(true); + + for (uint8_t tm1621_command : TM1621_COMMANDS) { + this->send_command_(tm1621_command); + } + + this->send_address_(0x00); + for (uint32_t segment = 0; segment < 16; segment++) { + this->send_common_(0); + } + this->stop_(); + + snprintf(this->row_[0], sizeof(this->row_[0]), "----"); + snprintf(this->row_[1], sizeof(this->row_[1]), "----"); + + this->display(); +} +void TM1621Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1621:"); + LOG_PIN(" CS Pin: ", this->cs_pin_); + LOG_PIN(" DATA Pin: ", this->data_pin_); + LOG_PIN(" READ Pin: ", this->read_pin_); + LOG_PIN(" WRITE Pin: ", this->write_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1621Display::update() { + // memset(this->row, 0, sizeof(this->row)); + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +float TM1621Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void TM1621Display::bit_delay_() { delayMicroseconds(100); } + +void TM1621Display::stop_() { + this->cs_pin_->digital_write(true); // Stop command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + this->data_pin_->digital_write(true); // Reset data +} + +void TM1621Display::display() { + // Tm1621.row[x] = "text", "----", " " or a number with one decimal like "0.4", "237.5", "123456.7" + // "123456.7" will be shown as "9999" being a four digit overflow + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Row1 '%s', Row2 '%s'"), Tm1621.row[0], Tm1621.row[1]); + + uint8_t buffer[8] = {0}; // TM1621 16-segment 4-bit common buffer + char row[4]; + for (uint32_t j = 0; j < 2; j++) { + // 0.4V => " 04", 0.0A => " ", 1234.5V => "1234" + uint32_t len = strlen(this->row_[j]); + char *dp = nullptr; // Expect number larger than "123" + int row_idx = len - 3; // "1234.5" + if (len <= 5) { // "----", " ", "0.4", "237.5" + dp = strchr(this->row_[j], '.'); + row_idx = len - 1; + } else if (len > 6) { // "12345.6" + snprintf(this->row_[j], sizeof(this->row_[j]), "9999"); + row_idx = 3; + } + row[3] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + if ((row_idx >= 0) && dp) { + row_idx--; + } + row[2] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + row[1] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + row[0] = (row_idx >= 0) ? this->row_[j][row_idx--] : ' '; + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Dump%d %4_H"), j +1, row); + + char command[10]; + char needle[2] = {0}; + for (uint32_t i = 0; i < 4; i++) { + needle[0] = row[i]; + int index = this->get_command_code_(command, sizeof(command), (const char *) needle, TM1621_KCHAR); + if (-1 == index) { + index = 11; + } + uint32_t bidx = (0 == j) ? i : 7 - i; + buffer[bidx] = TM1621_DIGIT_ROW[j][index]; + } + if (dp) { + if (0 == j) { + buffer[2] |= 0x80; // Row 1 decimal point + } else { + buffer[5] |= 0x08; // Row 2 decimal point + } + } + } + + if (this->fahrenheit_) { + buffer[1] |= 0x80; + } + if (this->celsius_) { + buffer[3] |= 0x80; + } + if (this->kwh_) { + buffer[4] |= 0x08; + } + if (this->humidity_) { + buffer[6] |= 0x08; + } + if (this->voltage_) { + buffer[7] |= 0x08; + } + + // AddLog(LOG_LEVEL_DEBUG, PSTR("TM1: Dump3 %8_H"), buffer); + + this->send_address_(0x10); // Sonoff only uses the upper 16 Segments + for (uint8_t i : buffer) { + this->send_common_(i); + } + this->stop_(); +} + +bool TM1621Display::send_command_(uint16_t command) { + uint16_t full_command = (0x0400 | command) << 5; // 0b100cccccccc00000 + this->cs_pin_->digital_write(false); // Start command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + for (uint32_t i = 0; i < 12; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (full_command & 0x8000) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + full_command <<= 1; + } + this->stop_(); + return true; +} + +bool TM1621Display::send_common_(uint8_t common) { + for (uint32_t i = 0; i < 8; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (common & 1) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + common >>= 1; + } + return true; +} + +bool TM1621Display::send_address_(uint16_t address) { + uint16_t full_address = (address | 0x0140) << 7; // 0b101aaaaaa0000000 + this->cs_pin_->digital_write(false); // Start command sequence + delayMicroseconds(TM1621_PULSE_WIDTH / 2); + for (uint32_t i = 0; i < 9; i++) { + this->write_pin_->digital_write(false); // Start write sequence + if (full_address & 0x8000) { + this->data_pin_->digital_write(true); // Set data + } else { + this->data_pin_->digital_write(false); // Set data + } + delayMicroseconds(TM1621_PULSE_WIDTH); + this->write_pin_->digital_write(true); // Read data + delayMicroseconds(TM1621_PULSE_WIDTH); + full_address <<= 1; + } + return true; +} + +uint8_t TM1621Display::print(uint8_t start_pos, const char *str) { + // ESP_LOGD(TAG, "Print at %d: %s", start_pos, str); + return snprintf(this->row_[start_pos], sizeof(this->row_[start_pos]), "%s", str); +} +uint8_t TM1621Display::print(const char *str) { return this->print(0, str); } +uint8_t TM1621Display::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 TM1621Display::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; +} + +int TM1621Display::get_command_code_(char *destination, size_t destination_size, const char *needle, + const char *haystack) { + // Returns -1 of not found + // Returns index and command if found + int result = -1; + const char *read = haystack; + char *write = destination; + + while (true) { + result++; + size_t size = destination_size - 1; + write = destination; + char ch = '.'; + while ((ch != '\0') && (ch != '|')) { + ch = *(read++); + if (size && (ch != '|')) { + *write++ = ch; + size--; + } + } + *write = '\0'; + if (!strcasecmp(needle, destination)) { + break; + } + if (0 == ch) { + result = -1; + break; + } + } + return result; +} +} // namespace tm1621 +} // namespace esphome diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h new file mode 100644 index 0000000000..b9f330e96e --- /dev/null +++ b/esphome/components/tm1621/tm1621.h @@ -0,0 +1,74 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tm1621 { + +class TM1621Display; + +using tm1621_writer_t = std::function; + +class TM1621Display : public PollingComponent { + public: + void set_writer(tm1621_writer_t &&writer) { this->writer_ = writer; } + + void setup() override; + + void dump_config() override; + + void set_cs_pin(GPIOPin *pin) { cs_pin_ = pin; } + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_read_pin(GPIOPin *pin) { read_pin_ = pin; } + void set_write_pin(GPIOPin *pin) { write_pin_ = pin; } + + void display_celsius(bool d) { celsius_ = d; } + void display_fahrenheit(bool d) { fahrenheit_ = d; } + void display_humidity(bool d) { humidity_ = d; } + void display_voltage(bool d) { voltage_ = d; } + void display_kwh(bool d) { kwh_ = d; } + + 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 display(); + + protected: + void bit_delay_(); + void setup_pins_(); + bool send_command_(uint16_t command); + bool send_common_(uint8_t common); + bool send_address_(uint16_t address); + void stop_(); + int get_command_code_(char *destination, size_t destination_size, const char *needle, const char *haystack); + + GPIOPin *data_pin_; + GPIOPin *cs_pin_; + GPIOPin *read_pin_; + GPIOPin *write_pin_; + optional writer_{}; + char row_[2][12]; + uint8_t state_; + uint8_t device_; + bool celsius_; + bool fahrenheit_; + bool humidity_; + bool voltage_; + bool kwh_; +}; + +} // namespace tm1621 +} // namespace esphome diff --git a/esphome/components/tm1637/binary_sensor.py b/esphome/components/tm1637/binary_sensor.py index 66b5172358..f14b9bd018 100644 --- a/esphome/components/tm1637/binary_sensor.py +++ b/esphome/components/tm1637/binary_sensor.py @@ -9,9 +9,8 @@ tm1637_ns = cg.esphome_ns.namespace("tm1637") TM1637Display = tm1637_ns.class_("TM1637Display", cg.PollingComponent) TM1637Key = tm1637_ns.class_("TM1637Key", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TM1637Key).extend( { - cv.GenerateID(): cv.declare_id(TM1637Key), cv.GenerateID(CONF_TM1637_ID): cv.use_id(TM1637Display), cv.Required(CONF_KEY): cv.int_range(min=0, max=15), } diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index be2192ea22..5b8cbc6004 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -168,7 +168,7 @@ uint8_t TM1637Display::get_keys() { // Bit | 7 6 5 4 3 2 1 0 // ------+------------------------ // To | 0 0 0 0 K2 S2 S1 S0 - key_code = (uint8_t)((key_code & 0x80) >> 7 | (key_code & 0x40) >> 5 | (key_code & 0x20) >> 3 | (key_code & 0x08)); + key_code = (uint8_t) ((key_code & 0x80) >> 7 | (key_code & 0x40) >> 5 | (key_code & 0x20) >> 3 | (key_code & 0x08)); } return key_code; } diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 0a77acaabe..2fb572bc55 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include + #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" #endif diff --git a/esphome/components/tm1638/__init__.py b/esphome/components/tm1638/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tm1638/binary_sensor/__init__.py b/esphome/components/tm1638/binary_sensor/__init__.py new file mode 100644 index 0000000000..6623228555 --- /dev/null +++ b/esphome/components/tm1638/binary_sensor/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_KEY +from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TM1638Key).extend( + { + cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), + cv.Required(CONF_KEY): cv.int_range(min=0, max=15), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + cg.add(var.set_keycode(config[CONF_KEY])) + hub = await cg.get_variable(config[CONF_TM1638_ID]) + cg.add(hub.register_listener(var)) diff --git a/esphome/components/tm1638/binary_sensor/tm1638_key.cpp b/esphome/components/tm1638/binary_sensor/tm1638_key.cpp new file mode 100644 index 0000000000..c143bafaea --- /dev/null +++ b/esphome/components/tm1638/binary_sensor/tm1638_key.cpp @@ -0,0 +1,13 @@ +#include "tm1638_key.h" + +namespace esphome { +namespace tm1638 { + +void TM1638Key::keys_update(uint8_t keys) { + bool pressed = keys & (1 << key_code_); + if (pressed != this->state) + this->publish_state(pressed); +} + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/binary_sensor/tm1638_key.h b/esphome/components/tm1638/binary_sensor/tm1638_key.h new file mode 100644 index 0000000000..0ea385f434 --- /dev/null +++ b/esphome/components/tm1638/binary_sensor/tm1638_key.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "../tm1638.h" + +namespace esphome { +namespace tm1638 { + +class TM1638Key : public binary_sensor::BinarySensor, public KeyListener { + public: + void set_keycode(uint8_t key_code) { key_code_ = key_code; }; + void keys_update(uint8_t keys) override; + + protected: + uint8_t key_code_{0}; +}; + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py new file mode 100644 index 0000000000..6339983674 --- /dev/null +++ b/esphome/components/tm1638/display.py @@ -0,0 +1,55 @@ +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_ID, + CONF_INTENSITY, + CONF_LAMBDA, + CONF_CLK_PIN, + CONF_DIO_PIN, + CONF_STB_PIN, +) + +CODEOWNERS = ["@skykingjwc"] + +CONF_TM1638_ID = "tm1638_id" + +tm1638_ns = cg.esphome_ns.namespace("tm1638") +TM1638Component = tm1638_ns.class_("TM1638Component", cg.PollingComponent) +TM1638ComponentRef = TM1638Component.operator("ref") + + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1638Component), + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_STB_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_INTENSITY, default=7): cv.int_range(min=0, max=8), + } +).extend(cv.polling_component_schema("1s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await display.register_display(var, config) + + clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk)) + + dio = await cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio)) + + stb = await cg.gpio_pin_expression(config[CONF_STB_PIN]) + cg.add(var.set_stb_pin(stb)) + + cg.add(var.set_intensity(config[CONF_INTENSITY])) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(TM1638ComponentRef, "it")], return_type=cg.void + ) + + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1638/output/__init__.py b/esphome/components/tm1638/output/__init__.py new file mode 100644 index 0000000000..2d982e409d --- /dev/null +++ b/esphome/components/tm1638/output/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_ID, CONF_LED +from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component) + + +CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1638OutputLed), + cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), + cv.Required(CONF_LED): cv.int_range(min=0, max=7), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + await cg.register_component(var, config) + cg.add(var.set_lednum(config[CONF_LED])) + hub = await cg.get_variable(config[CONF_TM1638_ID]) + cg.add(var.set_tm1638(hub)) diff --git a/esphome/components/tm1638/output/tm1638_output_led.cpp b/esphome/components/tm1638/output/tm1638_output_led.cpp new file mode 100644 index 0000000000..ea1c84e64b --- /dev/null +++ b/esphome/components/tm1638/output/tm1638_output_led.cpp @@ -0,0 +1,17 @@ +#include "tm1638_output_led.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tm1638 { + +static const char *const TAG = "tm1638.led"; + +void TM1638OutputLed::write_state(bool state) { tm1638_->set_led(led_, state); } + +void TM1638OutputLed::dump_config() { + LOG_BINARY_OUTPUT(this); + ESP_LOGCONFIG(TAG, " LED: %d", led_); +} + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/output/tm1638_output_led.h b/esphome/components/tm1638/output/tm1638_output_led.h new file mode 100644 index 0000000000..6aa1015aae --- /dev/null +++ b/esphome/components/tm1638/output/tm1638_output_led.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/binary_output.h" +#include "../tm1638.h" + +namespace esphome { +namespace tm1638 { + +class TM1638OutputLed : public output::BinaryOutput, public Component { + public: + void dump_config() override; + + void set_tm1638(TM1638Component *tm1638) { tm1638_ = tm1638; } + void set_lednum(int led) { led_ = led; } + + protected: + void write_state(bool state) override; + + TM1638Component *tm1638_; + int led_; +}; + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h new file mode 100644 index 0000000000..e20a55a69f --- /dev/null +++ b/esphome/components/tm1638/sevenseg.h @@ -0,0 +1,107 @@ +#pragma once + +namespace esphome { +namespace tm1638 { +namespace TM1638Translation { + +const unsigned char SEVEN_SEG[] PROGMEM = { + 0x00, /* (space) */ + 0x86, /* ! */ + 0x22, /* " */ + 0x7E, /* # */ + 0x6D, /* $ */ + 0xD2, /* % */ + 0x46, /* & */ + 0x20, /* ' */ + 0x29, /* ( */ + 0x0B, /* ) */ + 0x21, /* * */ + 0x70, /* + */ + 0x10, /* , */ + 0x40, /* - */ + 0x80, /* . */ + 0x52, /* / */ + 0x3F, /* 0 */ + 0x06, /* 1 */ + 0x5B, /* 2 */ + 0x4F, /* 3 */ + 0x66, /* 4 */ + 0x6D, /* 5 */ + 0x7D, /* 6 */ + 0x07, /* 7 */ + 0x7F, /* 8 */ + 0x6F, /* 9 */ + 0x09, /* : */ + 0x0D, /* ; */ + 0x61, /* < */ + 0x48, /* = */ + 0x43, /* > */ + 0xD3, /* ? */ + 0x5F, /* @ */ + 0x77, /* A */ + 0x7C, /* B */ + 0x39, /* C */ + 0x5E, /* D */ + 0x79, /* E */ + 0x71, /* F */ + 0x3D, /* G */ + 0x76, /* H */ + 0x30, /* I */ + 0x1E, /* J */ + 0x75, /* K */ + 0x38, /* L */ + 0x15, /* M */ + 0x37, /* N */ + 0x3F, /* O */ + 0x73, /* P */ + 0x6B, /* Q */ + 0x33, /* R */ + 0x6D, /* S */ + 0x78, /* T */ + 0x3E, /* U */ + 0x3E, /* V */ + 0x2A, /* W */ + 0x76, /* X */ + 0x6E, /* Y */ + 0x5B, /* Z */ + 0x39, /* [ */ + 0x64, /* \ */ + 0x0F, /* ] */ + 0x23, /* ^ */ + 0x08, /* _ */ + 0x02, /* ` */ + 0x5F, /* a */ + 0x7C, /* b */ + 0x58, /* c */ + 0x5E, /* d */ + 0x7B, /* e */ + 0x71, /* f */ + 0x6F, /* g */ + 0x74, /* h */ + 0x10, /* i */ + 0x0C, /* j */ + 0x75, /* k */ + 0x30, /* l */ + 0x14, /* m */ + 0x54, /* n */ + 0x5C, /* o */ + 0x73, /* p */ + 0x67, /* q */ + 0x50, /* r */ + 0x6D, /* s */ + 0x78, /* t */ + 0x1C, /* u */ + 0x1C, /* v */ + 0x14, /* w */ + 0x76, /* x */ + 0x6E, /* y */ + 0x5B, /* z */ + 0x46, /* { */ + 0x30, /* | */ + 0x70, /* } */ + 0x01, /* ~ */ +}; + +}; // namespace TM1638Translation +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/switch/__init__.py b/esphome/components/tm1638/switch/__init__.py new file mode 100644 index 0000000000..ed6aa91d03 --- /dev/null +++ b/esphome/components/tm1638/switch/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import CONF_LED +from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component) + + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1638SwitchLed), + cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), + cv.Required(CONF_LED): cv.int_range(min=0, max=7), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = await switch.new_switch(config) + await cg.register_component(var, config) + cg.add(var.set_lednum(config[CONF_LED])) + hub = await cg.get_variable(config[CONF_TM1638_ID]) + cg.add(var.set_tm1638(hub)) diff --git a/esphome/components/tm1638/switch/tm1638_switch_led.cpp b/esphome/components/tm1638/switch/tm1638_switch_led.cpp new file mode 100644 index 0000000000..60c9e8b4a9 --- /dev/null +++ b/esphome/components/tm1638/switch/tm1638_switch_led.cpp @@ -0,0 +1,20 @@ +#include "tm1638_switch_led.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tm1638 { + +static const char *const TAG = "tm1638.led"; + +void TM1638SwitchLed::write_state(bool state) { + tm1638_->set_led(led_, state); + publish_state(state); +} + +void TM1638SwitchLed::dump_config() { + LOG_SWITCH("", "TM1638 LED", this); + ESP_LOGCONFIG(TAG, " LED: %d", led_); +} + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/switch/tm1638_switch_led.h b/esphome/components/tm1638/switch/tm1638_switch_led.h new file mode 100644 index 0000000000..10516e0079 --- /dev/null +++ b/esphome/components/tm1638/switch/tm1638_switch_led.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "../tm1638.h" + +namespace esphome { +namespace tm1638 { + +class TM1638SwitchLed : public switch_::Switch, public Component { + public: + void dump_config() override; + + void set_tm1638(TM1638Component *tm1638) { tm1638_ = tm1638; } + void set_lednum(int led) { led_ = led; } + + protected: + void write_state(bool state) override; + TM1638Component *tm1638_; + int led_; +}; +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp new file mode 100644 index 0000000000..24cb4122bf --- /dev/null +++ b/esphome/components/tm1638/tm1638.cpp @@ -0,0 +1,288 @@ +#include "tm1638.h" +#include "sevenseg.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tm1638 { + +static const char *const TAG = "display.tm1638"; +static const uint8_t TM1638_REGISTER_FIXEDADDRESS = 0x44; +static const uint8_t TM1638_REGISTER_AUTOADDRESS = 0x40; +static const uint8_t TM1638_REGISTER_READBUTTONS = 0x42; +static const uint8_t TM1638_REGISTER_DISPLAYOFF = 0x80; +static const uint8_t TM1638_REGISTER_DISPLAYON = 0x88; +static const uint8_t TM1638_REGISTER_7SEG_0 = 0xC0; +static const uint8_t TM1638_REGISTER_LED_0 = 0xC1; +static const uint8_t TM1638_UNKNOWN_CHAR = 0b11111111; + +static const uint8_t TM1638_SHIFT_DELAY = 4; // clock pause between commands, default 4ms + +void TM1638Component::setup() { + ESP_LOGD(TAG, "Setting up TM1638..."); + + this->clk_pin_->setup(); // OUTPUT + this->dio_pin_->setup(); // OUTPUT + this->stb_pin_->setup(); // OUTPUT + + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->stb_pin_->pin_mode(gpio::FLAG_OUTPUT); + + this->clk_pin_->digital_write(false); + this->dio_pin_->digital_write(false); + this->stb_pin_->digital_write(false); + + this->set_intensity(intensity_); + + this->reset_(); // all LEDs off + + for (uint8_t i = 0; i < 8; i++) // zero fill print buffer + this->buffer_[i] = 0; +} + +void TM1638Component::dump_config() { + ESP_LOGCONFIG(TAG, "TM1638:"); + ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" DIO Pin: ", this->dio_pin_); + LOG_PIN(" STB Pin: ", this->stb_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1638Component::loop() { + if (this->listeners_.empty()) + return; + + uint8_t keys = this->get_keys(); + for (auto &listener : this->listeners_) + listener->keys_update(keys); +} + +uint8_t TM1638Component::get_keys() { + uint8_t buttons = 0; + + this->stb_pin_->digital_write(false); + + this->shift_out_(TM1638_REGISTER_READBUTTONS); + + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + + delayMicroseconds(10); + + for (uint8_t i = 0; i < 4; i++) { // read the 4 button registers + uint8_t v = this->shift_in_(); + buttons |= v << i; // shift bits to correct slots in the byte + } + + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + + this->stb_pin_->digital_write(true); + + return buttons; +} + +void TM1638Component::update() { // this is called at the interval specified in the config.yaml + if (this->writer_.has_value()) { + (*this->writer_)(*this); + } + + this->display(); +} + +float TM1638Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void TM1638Component::display() { + for (uint8_t i = 0; i < 8; i++) { + this->set_7seg_(i, buffer_[i]); + } +} + +void TM1638Component::reset_() { + uint8_t num_commands = 16; // 16 addresses, 8 for 7seg and 8 for LEDs + uint8_t commands[num_commands]; + + for (uint8_t i = 0; i < num_commands; i++) { + commands[i] = 0; + } + + this->send_command_sequence_(commands, num_commands, TM1638_REGISTER_7SEG_0); +} + +/////////////// LEDs ///////////////// + +void TM1638Component::set_led(int led_pos, bool led_on_off) { + this->send_command_(TM1638_REGISTER_FIXEDADDRESS); + + uint8_t commands[2]; + + commands[0] = TM1638_REGISTER_LED_0 + (led_pos << 1); + commands[1] = led_on_off; + + this->send_commands_(commands, 2); +} + +void TM1638Component::set_7seg_(int seg_pos, uint8_t seg_bits) { + this->send_command_(TM1638_REGISTER_FIXEDADDRESS); + + uint8_t commands[2] = {}; + + commands[0] = TM1638_REGISTER_7SEG_0 + (seg_pos << 1); + commands[1] = seg_bits; + + this->send_commands_(commands, 2); +} + +void TM1638Component::set_intensity(uint8_t brightness_level) { + this->intensity_ = brightness_level; + + this->send_command_(TM1638_REGISTER_FIXEDADDRESS); + + if (brightness_level > 0) { + this->send_command_((uint8_t) (TM1638_REGISTER_DISPLAYON | intensity_)); + } else { + this->send_command_(TM1638_REGISTER_DISPLAYOFF); + } +} + +/////////////// DISPLAY PRINT ///////////////// + +uint8_t TM1638Component::print(uint8_t start_pos, const char *str) { + uint8_t pos = start_pos; + + bool last_was_dot = false; + + for (; *str != '\0'; str++) { + uint8_t data = TM1638_UNKNOWN_CHAR; + + if (*str >= ' ' && *str <= '~') { + data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset + } else if (data == TM1638_UNKNOWN_CHAR) { + ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); + } + + if (*str == '.') // handle dots + { + if (pos != start_pos && + !last_was_dot) // if we are not at the first position, backup by one unless last char was a dot + { + pos--; + } + this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position + last_was_dot = true; // set a bit in case the next chracter is also a dot + } else // if not a dot, then just write the character to display + { + if (pos >= 8) { + ESP_LOGI(TAG, "TM1638 String is too long for the display!"); + break; + } + this->buffer_[pos] = data; + last_was_dot = false; // clear dot tracking bit + } + + pos++; + } + return pos - start_pos; +} + +/////////////// PRINT ///////////////// + +uint8_t TM1638Component::print(const char *str) { return this->print(0, str); } + +uint8_t TM1638Component::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 TM1638Component::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 TM1638Component::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 TM1638Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } +#endif + +//////////////// SPI //////////////// + +void TM1638Component::send_command_(uint8_t value) { + this->stb_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->stb_pin_->digital_write(false); + this->shift_out_(value); + this->stb_pin_->digital_write(true); +} + +void TM1638Component::send_commands_(uint8_t const commands[], uint8_t num_commands) { + this->stb_pin_->digital_write(false); + + for (uint8_t i = 0; i < num_commands; i++) { + uint8_t command = commands[i]; + this->shift_out_(command); + } + this->stb_pin_->digital_write(true); +} + +void TM1638Component::send_command_leave_open_(uint8_t value) { + this->stb_pin_->digital_write(false); + this->shift_out_(value); +} + +void TM1638Component::send_command_sequence_(uint8_t commands[], uint8_t num_commands, uint8_t starting_address) { + this->send_command_(TM1638_REGISTER_AUTOADDRESS); + this->send_command_leave_open_(starting_address); + + for (uint8_t i = 0; i < num_commands; i++) { + this->shift_out_(commands[i]); + } + + this->stb_pin_->digital_write(true); +} + +uint8_t TM1638Component::shift_in_() { + uint8_t value = 0; + + for (int i = 0; i < 8; ++i) { + value |= dio_pin_->digital_read() << i; + delayMicroseconds(TM1638_SHIFT_DELAY); + this->clk_pin_->digital_write(true); + delayMicroseconds(TM1638_SHIFT_DELAY); + this->clk_pin_->digital_write(false); + delayMicroseconds(TM1638_SHIFT_DELAY); + } + return value; +} + +void TM1638Component::shift_out_(uint8_t val) { + for (int i = 0; i < 8; i++) { + this->dio_pin_->digital_write((val & (1 << i))); + delayMicroseconds(TM1638_SHIFT_DELAY); + + this->clk_pin_->digital_write(true); + delayMicroseconds(TM1638_SHIFT_DELAY); + + this->clk_pin_->digital_write(false); + delayMicroseconds(TM1638_SHIFT_DELAY); + } +} + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h new file mode 100644 index 0000000000..2e1ac6fad3 --- /dev/null +++ b/esphome/components/tm1638/tm1638.h @@ -0,0 +1,83 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" +#include "esphome/core/hal.h" + +#include + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace tm1638 { + +class KeyListener { + public: + virtual void keys_update(uint8_t keys){}; +}; + +class TM1638Component; + +using tm1638_writer_t = std::function; + +class TM1638Component : public PollingComponent { + public: + void set_writer(tm1638_writer_t &&writer) { this->writer_ = writer; } + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override; + void set_intensity(uint8_t brightness_level); + void display(); + + void set_clk_pin(GPIOPin *pin) { this->clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { this->dio_pin_ = pin; } + void set_stb_pin(GPIOPin *pin) { this->stb_pin_ = pin; } + + void register_listener(KeyListener *listener) { this->listeners_.push_back(listener); } + + /// 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 loop() override; + uint8_t get_keys(); + +#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 + + void set_led(int led_pos, bool led_on_off); + + protected: + void set_7seg_(int seg_pos, uint8_t seg_bits); + void send_command_(uint8_t value); + void send_command_leave_open_(uint8_t value); + void send_commands_(uint8_t const commands[], uint8_t num_commands); + void send_command_sequence_(uint8_t commands[], uint8_t num_commands, uint8_t starting_address); + void shift_out_(uint8_t value); + void reset_(); + uint8_t shift_in_(); + uint8_t intensity_{}; /// brghtness of the display 0 through 7 + GPIOPin *clk_pin_; + GPIOPin *stb_pin_; + GPIOPin *dio_pin_; + uint8_t *buffer_ = new uint8_t[8]; + optional writer_{}; + std::vector listeners_{}; +}; + +} // namespace tm1638 +} // namespace esphome diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index c7a5b72852..33d36d6a69 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -1,5 +1,7 @@ #include "toshiba.h" +#include + namespace esphome { namespace toshiba { @@ -122,9 +124,6 @@ void ToshibaClimate::setup() { // Set supported modes & temperatures based on model this->minimum_temperature_ = this->temperature_min_(); this->maximum_temperature_ = this->temperature_max_(); - this->supports_dry_ = this->toshiba_supports_dry_(); - this->supports_fan_only_ = this->toshiba_supports_fan_only_(); - this->fan_modes_ = this->toshiba_fan_modes_(); this->swing_modes_ = this->toshiba_swing_modes_(); // Never send nan to HA if (std::isnan(this->target_temperature)) @@ -176,12 +175,43 @@ void ToshibaClimate::transmit_generic_() { mode = TOSHIBA_MODE_COOL; break; + case climate::CLIMATE_MODE_DRY: + mode = TOSHIBA_MODE_DRY; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + mode = TOSHIBA_MODE_FAN_ONLY; + break; + case climate::CLIMATE_MODE_HEAT_COOL: default: mode = TOSHIBA_MODE_AUTO; } - message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + uint8_t fan; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_QUIET: + fan = TOSHIBA_FAN_SPEED_QUIET; + break; + + case climate::CLIMATE_FAN_LOW: + fan = TOSHIBA_FAN_SPEED_1; + break; + + case climate::CLIMATE_FAN_MEDIUM: + fan = TOSHIBA_FAN_SPEED_3; + break; + + case climate::CLIMATE_FAN_HIGH: + fan = TOSHIBA_FAN_SPEED_5; + break; + + case climate::CLIMATE_FAN_AUTO: + default: + fan = TOSHIBA_FAN_SPEED_AUTO; + break; + } + message[6] = fan | mode; // Zero message[7] = 0x00; @@ -666,6 +696,30 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_HEAT_COOL; } + // Get the fan mode + switch (message[6] & 0xF0) { + case TOSHIBA_FAN_SPEED_QUIET: + this->fan_mode = climate::CLIMATE_FAN_QUIET; + break; + + case TOSHIBA_FAN_SPEED_1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + + case TOSHIBA_FAN_SPEED_3: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + + case TOSHIBA_FAN_SPEED_5: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + + case TOSHIBA_FAN_SPEED_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + // Get the target temperature this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN; } diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 36e8760169..83e85c34db 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -22,7 +22,10 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0; class ToshibaClimate : public climate_ir::ClimateIR { public: - ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f) {} + ToshibaClimate() + : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}) {} void setup() override; void set_model(Model model) { this->model_ = model; } @@ -46,18 +49,6 @@ class ToshibaClimate : public climate_ir::ClimateIR { float temperature_max_() { return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; } - bool toshiba_supports_dry_() { - return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); - } - bool toshiba_supports_fan_only_() { - return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); - } - std::set toshiba_fan_modes_() { - return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}; - } std::set toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) ? std::set{} diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index a40c56a7db..1a9d5d1a49 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -37,7 +37,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { TotalDailyEnergyMethod method_; uint16_t last_day_of_year_{}; uint32_t last_update_{0}; - uint32_t last_save_{0}; bool restore_; float total_energy_{0.0f}; float last_power_state_{0.0f}; diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index d7e53962e2..701468aa1e 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -23,6 +23,12 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, this->y_min_ = y_min; this->y_max_ = y_max; } + int16_t get_x_min() { return this->x_min_; } + int16_t get_x_max() { return this->x_max_; } + int16_t get_y_min() { return this->y_min_; } + int16_t get_y_max() { return this->y_max_; } + int16_t get_width() { return this->x_max_ - this->x_min_; } + int16_t get_height() { return this->y_max_ - this->y_min_; } void set_page(display::DisplayPage *page) { this->page_ = page; } diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py index 63a0733365..5435ed4b62 100644 --- a/esphome/components/tsl2591/sensor.py +++ b/esphome/components/tsl2591/sensor.py @@ -24,6 +24,7 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_GAIN, + CONF_ACTUAL_GAIN, CONF_ID, CONF_NAME, CONF_INTEGRATION_TIME, @@ -79,7 +80,6 @@ TSL2591Component = tsl2591_ns.class_( "TSL2591Component", cg.PollingComponent, i2c.I2CDevice ) - CONFIG_SCHEMA = ( cv.Schema( { @@ -106,6 +106,12 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ACTUAL_GAIN): sensor.sensor_schema( + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional( CONF_INTEGRATION_TIME, default="100ms" ): validate_integration_time, @@ -150,6 +156,11 @@ async def to_code(config): sens = await sensor.new_sensor(conf) cg.add(var.set_calculated_lux_sensor(sens)) + if CONF_ACTUAL_GAIN in config: + conf = config[CONF_ACTUAL_GAIN] + sens = await sensor.new_sensor(conf) + cg.add(var.set_actual_gain_sensor(sens)) + cg.add(var.set_name(config[CONF_NAME])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index f8c59a53c6..5086a38408 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -130,6 +130,7 @@ void TSL2591Component::dump_config() { LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_); LOG_SENSOR(" ", "Visible:", this->visible_sensor_); LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_); + LOG_SENSOR(" ", "Actual gain:", this->actual_gain_sensor_); LOG_UPDATE_INTERVAL(this); } @@ -140,8 +141,9 @@ void TSL2591Component::process_update_() { uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined); uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); float lux = this->get_calculated_lux(full, infrared); - ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared, - visible, lux); + uint16_t actual_gain = this->get_actual_gain(); + ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.", combined, + full, infrared, visible, lux, actual_gain); if (this->full_spectrum_sensor_ != nullptr) { this->full_spectrum_sensor_->publish_state(full); } @@ -154,9 +156,14 @@ void TSL2591Component::process_update_() { if (this->calculated_lux_sensor_ != nullptr) { this->calculated_lux_sensor_->publish_state(lux); } + if (this->component_gain_ == TSL2591_CGAIN_AUTO) { this->automatic_gain_update(full); } + + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(actual_gain); + } this->status_clear_warning(); } @@ -207,6 +214,10 @@ void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_ this->calculated_lux_sensor_ = calculated_lux_sensor; } +void TSL2591Component::set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor) { + this->actual_gain_sensor_ = actual_gain_sensor; +} + void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) { this->integration_time_ = integration_time; } @@ -347,8 +358,8 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535); if ((full_spectrum == max_count) || (infrared == max_count)) { // Signal an overflow - ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_); - return -1.0F; + ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain or integration time.", this->name_); + return NAN; } if ((full_spectrum == 0) && (infrared == 0)) { @@ -377,7 +388,6 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr again = 1.0F; break; } - // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly. // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14 // and that library code. @@ -448,5 +458,25 @@ void TSL2591Component::automatic_gain_update(uint16_t full_spectrum) { ESP_LOGD(TAG, "Gain setting: %d", this->gain_); } +/** Reads the actual gain used + * + * Useful for exposing the real gain used when configured in "auto" gain mode + */ +float TSL2591Component::get_actual_gain() { + switch (this->gain_) { + case TSL2591_GAIN_LOW: + return 1.0F; + case TSL2591_GAIN_MED: + return 25.0F; + case TSL2591_GAIN_HIGH: + return 400.0F; + case TSL2591_GAIN_MAX: + return 9500.0F; + default: + // Shouldn't get here, but just in case. + return NAN; + } +} + } // namespace tsl2591 } // namespace esphome diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index d82dbc395f..d7c5230276 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -217,14 +217,21 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { * * This gets called on update and tries to keep the ADC readings in the middle of the range */ - void automatic_gain_update(uint16_t full_spectrum); + /** Reads the actual gain used + * + * Useful for exposing the real gain used when configured in "auto" gain mode + */ + float get_actual_gain(); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these. They're for ESPHome integration use.) /** Used by ESPHome framework. */ void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor); /** Used by ESPHome framework. */ + void set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor); + /** Used by ESPHome framework. */ void set_infrared_sensor(sensor::Sensor *infrared_sensor); /** Used by ESPHome framework. */ void set_visible_sensor(sensor::Sensor *visible_sensor); @@ -245,10 +252,11 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { protected: const char *name_; - sensor::Sensor *full_spectrum_sensor_; - sensor::Sensor *infrared_sensor_; - sensor::Sensor *visible_sensor_; - sensor::Sensor *calculated_lux_sensor_; + sensor::Sensor *full_spectrum_sensor_{nullptr}; + sensor::Sensor *infrared_sensor_{nullptr}; + sensor::Sensor *visible_sensor_{nullptr}; + sensor::Sensor *calculated_lux_sensor_{nullptr}; + sensor::Sensor *actual_gain_sensor_{nullptr}; TSL2591IntegrationTime integration_time_; TSL2591ComponentGain component_gain_; TSL2591Gain gain_; diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 59749a4fa7..2663afcec9 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -4,6 +4,8 @@ #include "esphome/core/hal.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include + namespace esphome { namespace ttp229_bsf { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index 2064d9b654..f8775a17f0 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -4,6 +4,8 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/i2c/i2c.h" +#include + namespace esphome { namespace ttp229_lsf { diff --git a/esphome/components/tuya/automation.h b/esphome/components/tuya/automation.h index d7706e1d60..8d91cfdfbf 100644 --- a/esphome/components/tuya/automation.h +++ b/esphome/components/tuya/automation.h @@ -4,6 +4,8 @@ #include "esphome/core/automation.h" #include "tuya.h" +#include + namespace esphome { namespace tuya { diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 7d4b37ad22..199c2eabeb 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -25,6 +25,7 @@ CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier" CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier" CONF_ECO_DATAPOINT = "eco_datapoint" CONF_ECO_TEMPERATURE = "eco_temperature" +CONF_REPORTS_FAHRENHEIT = "reports_fahrenheit" TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) @@ -110,6 +111,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TARGET_TEMPERATURE_MULTIPLIER): cv.positive_float, cv.Optional(CONF_ECO_DATAPOINT): cv.uint8_t, cv.Optional(CONF_ECO_TEMPERATURE): cv.temperature, + cv.Optional(CONF_REPORTS_FAHRENHEIT, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), @@ -186,3 +188,6 @@ async def to_code(config): cg.add(var.set_eco_id(config[CONF_ECO_DATAPOINT])) if CONF_ECO_TEMPERATURE in config: cg.add(var.set_eco_temperature(config[CONF_ECO_TEMPERATURE])) + + if config[CONF_REPORTS_FAHRENHEIT]: + cg.add(var.set_reports_fahrenheit()) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 39d4203684..687764e30f 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -1,5 +1,5 @@ -#include "esphome/core/log.h" #include "tuya_climate.h" +#include "esphome/core/log.h" namespace esphome { namespace tuya { @@ -44,6 +44,10 @@ void TuyaClimate::setup() { if (this->target_temperature_id_.has_value()) { this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { this->manual_temperature_ = datapoint.value_int * this->target_temperature_multiplier_; + if (this->reports_fahrenheit_) { + this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9; + } + ESP_LOGV(TAG, "MCU reported manual target temperature is: %.1f", this->manual_temperature_); this->compute_target_temperature_(); this->compute_state_(); @@ -53,6 +57,10 @@ void TuyaClimate::setup() { if (this->current_temperature_id_.has_value()) { this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) { this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_; + if (this->reports_fahrenheit_) { + this->current_temperature = (this->current_temperature - 32) * 5 / 9; + } + ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature); this->compute_state_(); this->publish_state(); @@ -105,7 +113,10 @@ void TuyaClimate::control(const climate::ClimateCall &call) { } if (call.get_target_temperature().has_value()) { - const float target_temperature = *call.get_target_temperature(); + float target_temperature = *call.get_target_temperature(); + if (this->reports_fahrenheit_) + target_temperature = (target_temperature * 9 / 5) + 32; + ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature); this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, (int) (target_temperature / this->target_temperature_multiplier_)); @@ -138,18 +149,23 @@ climate::ClimateTraits TuyaClimate::traits() { void TuyaClimate::dump_config() { LOG_CLIMATE("", "Tuya Climate", this); - if (this->switch_id_.has_value()) + if (this->switch_id_.has_value()) { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); - if (this->active_state_id_.has_value()) + } + if (this->active_state_id_.has_value()) { ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_); - if (this->target_temperature_id_.has_value()) + } + if (this->target_temperature_id_.has_value()) { ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); - if (this->current_temperature_id_.has_value()) + } + if (this->current_temperature_id_.has_value()) { ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + } LOG_PIN(" Heating State Pin: ", this->heating_state_pin_); LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_); - if (this->eco_id_.has_value()) + if (this->eco_id_.has_value()) { ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); + } } void TuyaClimate::compute_preset_() { diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index ec19d05308..7c18625c4e 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -35,6 +35,8 @@ class TuyaClimate : public climate::Climate, public Component { void set_eco_id(uint8_t eco_id) { this->eco_id_ = eco_id; } void set_eco_temperature(float eco_temperature) { this->eco_temperature_ = eco_temperature; } + void set_reports_fahrenheit() { this->reports_fahrenheit_ = true; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } protected: @@ -77,6 +79,7 @@ class TuyaClimate : public climate::Climate, public Component { bool cooling_state_{false}; float manual_temperature_; bool eco_; + bool reports_fahrenheit_{false}; }; } // namespace tuya diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index b55873c3c1..fcb961f45e 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -112,18 +112,23 @@ void TuyaCover::dump_config() { ESP_LOGCONFIG(TAG, " Configured as Inverted, but direction_datapoint isn't configured"); } } - if (this->control_id_.has_value()) + if (this->control_id_.has_value()) { ESP_LOGCONFIG(TAG, " Control has datapoint ID %u", *this->control_id_); - if (this->direction_id_.has_value()) + } + if (this->direction_id_.has_value()) { ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); - if (this->position_id_.has_value()) + } + if (this->position_id_.has_value()) { ESP_LOGCONFIG(TAG, " Position has datapoint ID %u", *this->position_id_); - if (this->position_report_id_.has_value()) + } + if (this->position_report_id_.has_value()) { ESP_LOGCONFIG(TAG, " Position Report has datapoint ID %u", *this->position_report_id_); + } } cover::CoverTraits TuyaCover::get_traits() { auto traits = cover::CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); return traits; } diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 813aee4aa0..1b03ea50fa 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -49,14 +49,18 @@ void TuyaFan::setup() { void TuyaFan::dump_config() { LOG_FAN("", "Tuya Fan", this); - if (this->speed_id_.has_value()) + if (this->speed_id_.has_value()) { ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); - if (this->switch_id_.has_value()) + } + if (this->switch_id_.has_value()) { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); - if (this->oscillation_id_.has_value()) + } + if (this->oscillation_id_.has_value()) { ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); - if (this->direction_id_.has_value()) + } + if (this->direction_id_.has_value()) { ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); + } } fan::FanTraits TuyaFan::get_traits() { diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index b983e3f84e..d806060018 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -23,9 +23,23 @@ CONF_COLOR_TEMPERATURE_INVERT = "color_temperature_invert" CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value" CONF_RGB_DATAPOINT = "rgb_datapoint" CONF_HSV_DATAPOINT = "hsv_datapoint" +CONF_COLOR_DATAPOINT = "color_datapoint" +CONF_COLOR_TYPE = "color_type" + +TuyaColorType = tuya_ns.enum("TuyaColorType") + +COLOR_TYPES = { + "RGB": TuyaColorType.RGB, + "HSV": TuyaColorType.HSV, + "RGBHSV": TuyaColorType.RGBHSV, +} TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) +COLOR_CONFIG_ERROR = ( + "This option has been removed, use color_datapoint and color_type instead." +) + CONFIG_SCHEMA = cv.All( light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( { @@ -34,8 +48,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Exclusive(CONF_RGB_DATAPOINT, "color"): cv.uint8_t, - cv.Exclusive(CONF_HSV_DATAPOINT, "color"): cv.uint8_t, + cv.Optional(CONF_RGB_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), + cv.Optional(CONF_HSV_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), + cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t, + cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True), cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" @@ -61,8 +77,7 @@ CONFIG_SCHEMA = cv.All( cv.has_at_least_one_key( CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT, - CONF_RGB_DATAPOINT, - CONF_HSV_DATAPOINT, + CONF_COLOR_DATAPOINT, ), ) @@ -78,10 +93,9 @@ async def to_code(config): cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT])) if CONF_SWITCH_DATAPOINT in config: cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) - if CONF_RGB_DATAPOINT in config: - cg.add(var.set_rgb_id(config[CONF_RGB_DATAPOINT])) - elif CONF_HSV_DATAPOINT in config: - cg.add(var.set_hsv_id(config[CONF_HSV_DATAPOINT])) + if CONF_COLOR_DATAPOINT in config: + cg.add(var.set_color_id(config[CONF_COLOR_DATAPOINT])) + cg.add(var.set_color_type(config[CONF_COLOR_TYPE])) if CONF_COLOR_TEMPERATURE_DATAPOINT in config: cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT])) cg.add(var.set_color_temperature_invert(config[CONF_COLOR_TEMPERATURE_INVERT])) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 4d8bc9e37b..869e20871d 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -50,38 +50,39 @@ void TuyaLight::setup() { call.perform(); }); } - if (rgb_id_.has_value()) { - this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { - auto red = parse_hex(datapoint.value_string.substr(0, 2)); - auto green = parse_hex(datapoint.value_string.substr(2, 2)); - auto blue = parse_hex(datapoint.value_string.substr(4, 2)); - if (red.has_value() && green.has_value() && blue.has_value()) { - if (this->state_->current_values != this->state_->remote_values) { - ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); - return; - } - - auto call = this->state_->make_call(); - call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); - call.perform(); + if (color_id_.has_value()) { + this->parent_->register_listener(*this->color_id_, [this](const TuyaDatapoint &datapoint) { + if (this->state_->current_values != this->state_->remote_values) { + ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); + return; } - }); - } else if (hsv_id_.has_value()) { - this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { - auto hue = parse_hex(datapoint.value_string.substr(0, 4)); - auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); - auto value = parse_hex(datapoint.value_string.substr(8, 4)); - if (hue.has_value() && saturation.has_value() && value.has_value()) { - if (this->state_->current_values != this->state_->remote_values) { - ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); - return; - } - float red, green, blue; - hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); - auto call = this->state_->make_call(); - call.set_rgb(red, green, blue); - call.perform(); + switch (*this->color_type_) { + case TuyaColorType::RGBHSV: + case TuyaColorType::RGB: { + auto red = parse_hex(datapoint.value_string.substr(0, 2)); + auto green = parse_hex(datapoint.value_string.substr(2, 2)); + auto blue = parse_hex(datapoint.value_string.substr(4, 2)); + if (red.has_value() && green.has_value() && blue.has_value()) { + auto rgb_call = this->state_->make_call(); + rgb_call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); + rgb_call.perform(); + } + break; + } + case TuyaColorType::HSV: { + auto hue = parse_hex(datapoint.value_string.substr(0, 4)); + auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); + auto value = parse_hex(datapoint.value_string.substr(8, 4)); + if (hue.has_value() && saturation.has_value() && value.has_value()) { + float red, green, blue; + hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); + auto rgb_call = this->state_->make_call(); + rgb_call.set_rgb(red, green, blue); + rgb_call.perform(); + } + break; + } } }); } @@ -92,21 +93,21 @@ void TuyaLight::setup() { void TuyaLight::dump_config() { ESP_LOGCONFIG(TAG, "Tuya Dimmer:"); - if (this->dimmer_id_.has_value()) + if (this->dimmer_id_.has_value()) { ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); - if (this->switch_id_.has_value()) + } + if (this->switch_id_.has_value()) { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); - if (this->rgb_id_.has_value()) { - ESP_LOGCONFIG(TAG, " RGB has datapoint ID %u", *this->rgb_id_); - } else if (this->hsv_id_.has_value()) { - ESP_LOGCONFIG(TAG, " HSV has datapoint ID %u", *this->hsv_id_); + } + if (this->color_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Color has datapoint ID %u", *this->color_id_); } } light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { - if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_id_.has_value()) { if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); } else { @@ -117,7 +118,7 @@ light::LightTraits TuyaLight::get_traits() { traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); - } else if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + } else if (this->color_id_.has_value()) { if (this->dimmer_id_.has_value()) { if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); @@ -140,7 +141,7 @@ void TuyaLight::write_state(light::LightState *state) { float red = 0.0f, green = 0.0f, blue = 0.0f; float color_temperature = 0.0f, brightness = 0.0f; - if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_id_.has_value()) { if (this->color_temperature_id_.has_value()) { state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &brightness); } else if (this->dimmer_id_.has_value()) { @@ -176,21 +177,36 @@ void TuyaLight::write_state(light::LightState *state) { } } - if (brightness == 0.0f || !color_interlock_) { - if (this->rgb_id_.has_value()) { - char buffer[7]; - sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); - std::string rgb_value = buffer; - this->parent_->set_string_datapoint_value(*this->rgb_id_, rgb_value); - } else if (this->hsv_id_.has_value()) { - int hue; - float saturation, value; - rgb_to_hsv(red, green, blue, hue, saturation, value); - char buffer[13]; - sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); - std::string hsv_value = buffer; - this->parent_->set_string_datapoint_value(*this->hsv_id_, hsv_value); + if (this->color_id_.has_value() && (brightness == 0.0f || !color_interlock_)) { + std::string color_value; + switch (*this->color_type_) { + case TuyaColorType::RGB: { + char buffer[7]; + sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); + color_value = buffer; + break; + } + case TuyaColorType::HSV: { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[13]; + sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); + color_value = buffer; + break; + } + case TuyaColorType::RGBHSV: { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[15]; + sprintf(buffer, "%02X%02X%02X%04X%02X%02X", int(red * 255), int(green * 255), int(blue * 255), hue, + int(saturation * 255), int(value * 255)); + color_value = buffer; + break; + } } + this->parent_->set_string_datapoint_value(*this->color_id_, color_value); } if (this->switch_id_.has_value()) { diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index 3d9f25271c..bd9920f18f 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -7,6 +7,12 @@ namespace esphome { namespace tuya { +enum TuyaColorType { + RGB, + HSV, + RGBHSV, +}; + class TuyaLight : public Component, public light::LightOutput { public: void setup() override; @@ -16,8 +22,8 @@ class TuyaLight : public Component, public light::LightOutput { this->min_value_datapoint_id_ = min_value_datapoint_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } - void set_rgb_id(uint8_t rgb_id) { this->rgb_id_ = rgb_id; } - void set_hsv_id(uint8_t hsv_id) { this->hsv_id_ = hsv_id; } + void set_color_id(uint8_t color_id) { this->color_id_ = color_id; } + void set_color_type(TuyaColorType color_type) { this->color_type_ = color_type; } void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_id; } void set_color_temperature_invert(bool color_temperature_invert) { this->color_temperature_invert_ = color_temperature_invert; @@ -48,8 +54,8 @@ class TuyaLight : public Component, public light::LightOutput { optional dimmer_id_{}; optional min_value_datapoint_id_{}; optional switch_id_{}; - optional rgb_id_{}; - optional hsv_id_{}; + optional color_id_{}; + optional color_type_{}; optional color_temperature_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 12c0c0f6e5..42ac9fcfbe 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -23,16 +23,17 @@ def validate_min_max(config): CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend( + number.number_schema(TuyaNumber) + .extend( { - cv.GenerateID(): cv.declare_id(TuyaNumber), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Required(CONF_NUMBER_DATAPOINT): cv.uint8_t, cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_min_max, ) diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py index 3d65eda301..dc78b2c3db 100644 --- a/esphome/components/tuya/select/__init__.py +++ b/esphome/components/tuya/select/__init__.py @@ -25,15 +25,18 @@ def ensure_option_map(value): return value -CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaSelect), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, - cv.Required(CONF_OPTIONS): ensure_option_map, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + select.select_schema(TuyaSelect) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Required(CONF_OPTIONS): ensure_option_map, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index ab233dc501..6a7e5c7ed0 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -4,6 +4,8 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/select/select.h" +#include + namespace esphome { namespace tuya { diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py index 441400fa43..69711204a8 100644 --- a/esphome/components/tuya/sensor/__init__.py +++ b/esphome/components/tuya/sensor/__init__.py @@ -9,13 +9,16 @@ CODEOWNERS = ["@jesserockz"] TuyaSensor = tuya_ns.class_("TuyaSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaSensor), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema(TuyaSensor) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 7b580986e1..040b9b7ed5 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -5,6 +5,14 @@ #include "esphome/core/util.h" #include "esphome/core/gpio.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + namespace esphome { namespace tuya { @@ -66,7 +74,6 @@ void Tuya::dump_config() { LOG_PIN(" Status Pin: ", this->status_pin_.value()); } ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); - this->check_uart_settings(9600); } bool Tuya::validate_message_() { @@ -231,6 +238,10 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::WIFI_TEST: this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector{0x00, 0x00}}); break; + case TuyaCommandType::WIFI_RSSI: + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::WIFI_RSSI, .payload = std::vector{get_wifi_rssi_()}}); + break; case TuyaCommandType::LOCAL_TIME_QUERY: #ifdef USE_TIME if (this->time_id_.has_value()) { @@ -244,6 +255,19 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); #endif break; + case TuyaCommandType::VACUUM_MAP_UPLOAD: + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::VACUUM_MAP_UPLOAD, .payload = std::vector{0x01}}); + ESP_LOGW(TAG, "Vacuum map upload requested, responding that it is not enabled."); + break; + case TuyaCommandType::GET_NETWORK_STATUS: { + uint8_t wifi_status = this->get_wifi_status_code_(); + + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::GET_NETWORK_STATUS, .payload = std::vector{wifi_status}}); + ESP_LOGV(TAG, "Network status requested, reported as %i", wifi_status); + break; + } default: ESP_LOGE(TAG, "Invalid command (0x%02X) received", command); } @@ -357,8 +381,8 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { } void Tuya::send_raw_command_(TuyaCommand command) { - uint8_t len_hi = (uint8_t)(command.payload.size() >> 8); - uint8_t len_lo = (uint8_t)(command.payload.size() & 0xFF); + uint8_t len_hi = (uint8_t) (command.payload.size() >> 8); + uint8_t len_lo = (uint8_t) (command.payload.size() & 0xFF); uint8_t version = 0; this->last_command_timestamp_ = millis(); @@ -438,8 +462,9 @@ void Tuya::set_status_pin_() { this->status_pin_.value()->digital_write(is_network_ready); } -void Tuya::send_wifi_status_() { +uint8_t Tuya::get_wifi_status_code_() { uint8_t status = 0x02; + if (network::is_connected()) { status = 0x03; @@ -447,7 +472,28 @@ void Tuya::send_wifi_status_() { if (this->protocol_version_ >= 0x03 && remote_is_connected()) { status = 0x04; } - } + } else { +#ifdef USE_CAPTIVE_PORTAL + if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) { + status = 0x01; + } +#endif + }; + + return status; +} + +uint8_t Tuya::get_wifi_rssi_() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->wifi_rssi(); +#endif + + return 0; +} + +void Tuya::send_wifi_status_() { + uint8_t status = this->get_wifi_status_code_(); if (status == this->wifi_status_) { return; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 1f21b09c0c..8d6153482f 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -9,6 +9,8 @@ #include "esphome/components/time/real_time_clock.h" #endif +#include + namespace esphome { namespace tuya { @@ -53,6 +55,9 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + WIFI_RSSI = 0x24, + VACUUM_MAP_UPLOAD = 0x28, + GET_NETWORK_STATUS = 0x2B, }; enum class TuyaInitState : uint8_t { @@ -118,6 +123,8 @@ class Tuya : public Component, public uart::UARTDevice { void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); void set_status_pin_(); void send_wifi_status_(); + uint8_t get_wifi_status_code_(); + uint8_t get_wifi_rssi_(); #ifdef USE_TIME void send_local_time_(); diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index fefcc8f4d5..a2a3baa46e 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace tx20 { diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index 1c617d0674..95a9517227 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -43,8 +43,8 @@ class Tx20Component : public Component { std::string wind_cardinal_direction_; InternalGPIOPin *pin_; - sensor::Sensor *wind_speed_sensor_; - sensor::Sensor *wind_direction_degrees_sensor_; + sensor::Sensor *wind_speed_sensor_{nullptr}; + sensor::Sensor *wind_direction_degrees_sensor_{nullptr}; Tx20ComponentStore store_; }; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a63b220fc7..ed60a9f880 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -41,6 +41,7 @@ ESP32ArduinoUARTComponent = uart_ns.class_( ESP8266UartComponent = uart_ns.class_( "ESP8266UartComponent", UARTComponent, cg.Component ) +RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component) UARTDevice = uart_ns.class_("UARTDevice") UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) @@ -89,6 +90,8 @@ def _uart_declare_type(value): return cv.declare_id(ESP32ArduinoUARTComponent)(value) if CORE.using_esp_idf: return cv.declare_id(IDFUARTComponent)(value) + if CORE.is_rp2040: + return cv.declare_id(RP2040UartComponent)(value) raise NotImplementedError @@ -243,11 +246,13 @@ def final_validate_device_schema( baud_rate: Optional[int] = None, require_tx: bool = False, require_rx: bool = False, + parity: Optional[str] = None, + stop_bits: Optional[int] = None, ): def validate_baud_rate(value): if value != baud_rate: raise cv.Invalid( - f"Component {name} required baud rate {baud_rate} for the uart bus" + f"Component {name} requires baud rate {baud_rate} for the uart bus" ) return value @@ -263,6 +268,20 @@ def final_validate_device_schema( return validator + def validate_parity(value): + if value != parity: + raise cv.Invalid( + f"Component {name} requires parity {parity} for the uart bus" + ) + return value + + def validate_stop_bits(value): + if value != stop_bits: + raise cv.Invalid( + f"Component {name} requires stop bits {stop_bits} for the uart bus" + ) + return value + def validate_hub(hub_config): hub_schema = {} uart_id = hub_config[CONF_ID] @@ -285,6 +304,10 @@ def final_validate_device_schema( ] = validate_pin(CONF_RX_PIN, device) if baud_rate is not None: hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + if parity is not None: + hub_schema[cv.Required(CONF_PARITY)] = validate_parity + if stop_bits is not None: + hub_schema[cv.Required(CONF_STOP_BITS)] = validate_stop_bits return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) return cv.Schema( diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 9686f94413..b6a50ea22d 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -3,6 +3,8 @@ #include "uart.h" #include "esphome/core/automation.h" +#include + namespace esphome { namespace uart { diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 4c82d5680a..4f24d76d0c 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -4,6 +4,8 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/switch/switch.h" +#include + namespace esphome { namespace uart { diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index a67e5354fb..8bbbc1a650 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -90,6 +90,7 @@ void ESP32ArduinoUARTComponent::setup() { this->hw_serial_ = &Serial; } else { static uint8_t next_uart_num = 1; + this->number_ = next_uart_num; this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; @@ -99,12 +100,12 @@ void ESP32ArduinoUARTComponent::setup() { invert = true; if (rx_pin_ != nullptr && rx_pin_->is_inverted()) invert = true; - this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); } void ESP32ArduinoUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); + ESP_LOGCONFIG(TAG, "UART Bus %d:", this->number_); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); if (this->rx_pin_ != nullptr) { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index c6f445ff12..f85c709097 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -28,10 +28,14 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { uint32_t get_config(); + HardwareSerial *get_hw_serial() { return this->hw_serial_; } + uint8_t get_hw_serial_number() { return this->number_; } + protected: void check_logger_conflict() override; HardwareSerial *hw_serial_{nullptr}; + uint8_t number_{0}; }; } // namespace uart diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 80255ccddf..1560409772 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -79,7 +79,12 @@ void IDFUARTComponent::setup() { return; } - err = uart_driver_install(this->uart_num_, this->rx_buffer_size_, 0, 0, nullptr, 0); + err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, + /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will + block task until all data have been sent out.*/ + 0, + /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), + /* Flags used to allocate the interrupt. */ 0); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); this->mark_failed(); diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 27fb80d2cc..fdaa4da9a7 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -23,9 +23,13 @@ class IDFUARTComponent : public UARTComponent, public Component { int available() override; void flush() override; + uint8_t get_hw_serial_number() { return this->uart_num_; } + QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } + protected: void check_logger_conflict() override; uart_port_t uart_num_; + QueueHandle_t uart_event_queue_; uart_config_t get_config_(); SemaphoreHandle_t lock_; diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp new file mode 100644 index 0000000000..e2c47080ac --- /dev/null +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -0,0 +1,184 @@ +#ifdef USE_RP2040 +#include "uart_component_rp2040.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart.arduino_rp2040"; + +uint16_t RP2040UartComponent::get_config() { + uint16_t config = 0; + + if (this->parity_ == UART_CONFIG_PARITY_NONE) { + config |= UART_PARITY_NONE; + } else if (this->parity_ == UART_CONFIG_PARITY_EVEN) { + config |= UART_PARITY_EVEN; + } else if (this->parity_ == UART_CONFIG_PARITY_ODD) { + config |= UART_PARITY_ODD; + } + + switch (this->data_bits_) { + case 5: + config |= SERIAL_DATA_5; + break; + case 6: + config |= SERIAL_DATA_6; + break; + case 7: + config |= SERIAL_DATA_7; + break; + case 8: + config |= SERIAL_DATA_8; + break; + } + + if (this->stop_bits_ == 1) { + config |= SERIAL_STOP_BIT_1; + } else { + config |= SERIAL_STOP_BIT_2; + } + + return config; +} + +void RP2040UartComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up UART bus..."); + + uint16_t config = get_config(); + + constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); + constexpr uint32_t valid_tx_uart_1 = __bitset({4, 8, 20, 24}); + + constexpr uint32_t valid_rx_uart_0 = __bitset({1, 13, 17, 29}); + constexpr uint32_t valid_rx_uart_1 = __bitset({5, 9, 21, 25}); + + int8_t tx_hw = -1; + int8_t rx_hw = -1; + + if (this->tx_pin_ != nullptr) { + if (this->tx_pin_->is_inverted()) { + ESP_LOGD(TAG, "An inverted TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin()); + } else { + if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_0) != 0) { + tx_hw = 0; + } else if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_1) != 0) { + tx_hw = 1; + } else { + ESP_LOGD(TAG, "TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin()); + } + } + } + + if (this->rx_pin_ != nullptr) { + if (this->rx_pin_->is_inverted()) { + ESP_LOGD(TAG, "An inverted RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin()); + } else { + if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_0) != 0) { + rx_hw = 0; + } else if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_1) != 0) { + rx_hw = 1; + } else { + ESP_LOGD(TAG, "RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin()); + } + } + } + +#ifdef USE_LOGGER + if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) { + ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw); + tx_hw = -1; + rx_hw = -1; + } +#endif + + if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { + ESP_LOGV(TAG, "Using SerialPIO"); + pin_size_t tx = this->tx_pin_ == nullptr ? SerialPIO::NOPIN : this->tx_pin_->get_pin(); + pin_size_t rx = this->rx_pin_ == nullptr ? SerialPIO::NOPIN : this->rx_pin_->get_pin(); + auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory) + serial->begin(this->baud_rate_, config); + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + gpio_set_outover(tx, GPIO_OVERRIDE_INVERT); + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + gpio_set_inover(rx, GPIO_OVERRIDE_INVERT); + this->serial_ = serial; + } else { + ESP_LOGV(TAG, "Using Hardware Serial"); + SerialUART *serial; + if (tx_hw == 0) { + serial = &Serial1; + } else { + serial = &Serial2; + } + serial->setTX(this->tx_pin_->get_pin()); + serial->setRX(this->rx_pin_->get_pin()); + serial->setFIFOSize(this->rx_buffer_size_); + serial->begin(this->baud_rate_, config); + this->serial_ = serial; + this->hw_serial_ = true; + } +} + +void RP2040UartComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + LOG_PIN(" TX Pin: ", tx_pin_); + LOG_PIN(" RX Pin: ", rx_pin_); + if (this->rx_pin_ != nullptr) { + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); + } + ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_))); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); + if (this->hw_serial_) { + ESP_LOGCONFIG(TAG, " Using hardware serial"); + } else { + ESP_LOGCONFIG(TAG, " Using SerialPIO"); + } +} + +void RP2040UartComponent::write_array(const uint8_t *data, size_t len) { + this->serial_->write(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); + } +#endif +} +bool RP2040UartComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + *data = this->serial_->peek(); + return true; +} +bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + this->serial_->readBytes(data, len); +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); + } +#endif + return true; +} +int RP2040UartComponent::available() { return this->serial_->available(); } +void RP2040UartComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + this->serial_->flush(); +} + +} // namespace uart +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h new file mode 100644 index 0000000000..f26c913cff --- /dev/null +++ b/esphome/components/uart/uart_component_rp2040.h @@ -0,0 +1,46 @@ +#pragma once + +#ifdef USE_RP2040 + +#include +#include + +#include +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +class RP2040UartComponent : public UARTComponent, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void write_array(const uint8_t *data, size_t len) override; + + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + + int available() override; + void flush() override; + + uint16_t get_config(); + + bool is_hw_serial() { return this->hw_serial_; } + HardwareSerial *get_hw_serial() { return this->serial_; } + + protected: + void check_logger_conflict() override {} + bool hw_serial_{false}; + + HardwareSerial *serial_{nullptr}; +}; + +} // namespace uart +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/ufire_ec/__init__.py b/esphome/components/ufire_ec/__init__.py new file mode 100644 index 0000000000..08f36c7934 --- /dev/null +++ b/esphome/components/ufire_ec/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@pvizeli"] diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py new file mode 100644 index 0000000000..9602d0c2d0 --- /dev/null +++ b/esphome/components/ufire_ec/sensor.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_EC, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MILLISIEMENS_PER_CENTIMETER, +) + +DEPENDENCIES = ["i2c"] + +CONF_SOLUTION = "solution" +CONF_TEMPERATURE_SENSOR = "temperature_sensor" +CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" +CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" + +ufire_ec_ns = cg.esphome_ns.namespace("ufire_ec") +UFireECComponent = ufire_ec_ns.class_( + "UFireECComponent", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +UFireECCalibrateProbeAction = ufire_ec_ns.class_( + "UFireECCalibrateProbeAction", automation.Action +) +UFireECResetAction = ufire_ec_ns.class_("UFireECResetAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UFireECComponent), + cv.Exclusive(CONF_TEMPERATURE, "temperature"): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Optional(CONF_EC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISIEMENS_PER_CENTIMETER, + icon=ICON_EMPTY, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Exclusive(CONF_TEMPERATURE_SENSOR, "temperature"): cv.use_id( + sensor.Sensor + ), + cv.Optional(CONF_TEMPERATURE_COMPENSATION, default=21.0): cv.temperature, + cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0.019): cv.float_range( + min=0.01, max=0.04 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_temperature_compensation(config[CONF_TEMPERATURE_COMPENSATION])) + cg.add(var.set_temperature_coefficient(config[CONF_TEMPERATURE_COEFFICIENT])) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_EC in config: + sens = await sensor.new_sensor(config[CONF_EC]) + cg.add(var.set_ec_sensor(sens)) + + if CONF_TEMPERATURE_SENSOR in config: + sens = await cg.get_variable(config[CONF_TEMPERATURE_SENSOR]) + cg.add(var.set_temperature_sensor_external(sens)) + + await i2c.register_i2c_device(var, config) + + +UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireECComponent), + cv.Required(CONF_SOLUTION): cv.templatable(float), + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + } +) + + +@automation.register_action( + "ufire_ec.calibrate_probe", + UFireECCalibrateProbeAction, + UFIRE_EC_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + solution_ = await cg.templatable(config[CONF_SOLUTION], args, float) + temperature_ = await cg.templatable(config[CONF_TEMPERATURE], args, float) + cg.add(var.set_solution(solution_)) + cg.add(var.set_temperature(temperature_)) + return var + + +UFIRE_EC_RESET_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireECComponent), + } +) + + +@automation.register_action( + "ufire_ec.reset", + UFireECResetAction, + UFIRE_EC_RESET_SCHEMA, +) +async def ufire_ec_reset_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp new file mode 100644 index 0000000000..7af4fadf75 --- /dev/null +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -0,0 +1,118 @@ +#include "esphome/core/log.h" +#include "ufire_ec.h" + +namespace esphome { +namespace ufire_ec { + +static const char *const TAG = "ufire_ec"; + +void UFireECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up uFire_ec..."); + + uint8_t version; + if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + this->mark_failed(); + return; + } + ESP_LOGI(TAG, "Found ufire_ec board version 0x%02X", version); + + // Write option for temperature adjustments + uint8_t config; + this->read_byte(REGISTER_CONFIG, &config); + if (this->temperature_sensor_ == nullptr && this->temperature_sensor_external_ == nullptr) { + config &= ~CONFIG_TEMP_COMPENSATION; + } else { + config |= CONFIG_TEMP_COMPENSATION; + } + this->write_byte(REGISTER_CONFIG, config); + + // Update temperature compensation + this->set_compensation_(this->temperature_compensation_); + this->set_coefficient_(this->temperature_coefficient_); +} + +void UFireECComponent::update() { + int wait = 0; + + if (this->temperature_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_TEMP); + wait += 750; + } else if (this->temperature_sensor_external_ != nullptr) { + this->set_temperature_(this->temperature_sensor_external_->state); + } + + if (this->ec_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_EC); + wait += 750; + } + + if (wait > 0) { + this->set_timeout("data", wait, [this]() { this->update_internal_(); }); + } +} + +void UFireECComponent::update_internal_() { + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(this->measure_temperature_()); + if (this->ec_sensor_ != nullptr) + this->ec_sensor_->publish_state(this->measure_ms_()); +} + +float UFireECComponent::measure_temperature_() { return this->read_data_(REGISTER_TEMP); } + +float UFireECComponent::measure_ms_() { return this->read_data_(REGISTER_MS); } + +void UFireECComponent::set_solution_(float solution, float temperature) { + solution /= (1 - (this->temperature_coefficient_ * (temperature - 25))); + this->write_data_(REGISTER_SOLUTION, solution); +} + +void UFireECComponent::set_compensation_(float temperature) { this->write_data_(REGISTER_COMPENSATION, temperature); } + +void UFireECComponent::set_coefficient_(float coefficient) { this->write_data_(REGISTER_COEFFICENT, coefficient); } + +void UFireECComponent::set_temperature_(float temperature) { this->write_data_(REGISTER_TEMP, temperature); } + +void UFireECComponent::calibrate_probe(float solution, float temperature) { + this->set_solution_(solution, temperature); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_PROBE); +} + +void UFireECComponent::reset_board() { this->write_data_(REGISTER_CALIBRATE_OFFSET, NAN); } + +float UFireECComponent::read_data_(uint8_t reg) { + float f; + uint8_t temp[4]; + + this->write(®, 1); + delay(10); + + for (uint8_t i = 0; i < 4; i++) { + this->read_bytes_raw(temp + i, 1); + } + memcpy(&f, temp, sizeof(f)); + + return f; +} + +void UFireECComponent::write_data_(uint8_t reg, float data) { + uint8_t temp[4]; + + memcpy(temp, &data, sizeof(data)); + this->write_bytes(reg, temp, 4); + delay(10); +} + +void UFireECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "uFire-EC"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_) + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + ESP_LOGCONFIG(TAG, " Temperature Compensation: %f", this->temperature_compensation_); + ESP_LOGCONFIG(TAG, " Temperature Coefficient: %f", this->temperature_coefficient_); +} + +} // namespace ufire_ec +} // namespace esphome diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h new file mode 100644 index 0000000000..3d436555a2 --- /dev/null +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ufire_ec { + +static const uint8_t CONFIG_TEMP_COMPENSATION = 0x02; + +static const uint8_t REGISTER_VERSION = 0; +static const uint8_t REGISTER_MS = 1; +static const uint8_t REGISTER_TEMP = 5; +static const uint8_t REGISTER_SOLUTION = 9; +static const uint8_t REGISTER_COEFFICENT = 13; +static const uint8_t REGISTER_CALIBRATE_OFFSET = 33; +static const uint8_t REGISTER_COMPENSATION = 45; +static const uint8_t REGISTER_CONFIG = 54; +static const uint8_t REGISTER_TASK = 55; + +static const uint8_t COMMAND_CALIBRATE_PROBE = 20; +static const uint8_t COMMAND_MEASURE_TEMP = 40; +static const uint8_t COMMAND_MEASURE_EC = 80; + +class UFireECComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_temperature_sensor_external(sensor::Sensor *temperature_sensor) { + this->temperature_sensor_external_ = temperature_sensor; + } + void set_ec_sensor(sensor::Sensor *ec_sensor) { this->ec_sensor_ = ec_sensor; } + void set_temperature_compensation(float compensation) { this->temperature_compensation_ = compensation; } + void set_temperature_coefficient(float coefficient) { this->temperature_coefficient_ = coefficient; } + void calibrate_probe(float solution, float temperature); + void reset_board(); + + protected: + float measure_temperature_(); + float measure_ms_(); + void set_solution_(float solution, float temperature); + void set_compensation_(float temperature); + void set_coefficient_(float coefficient); + void set_temperature_(float temperature); + float read_data_(uint8_t reg); + void write_data_(uint8_t reg, float data); + void update_internal_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_external_{nullptr}; + sensor::Sensor *ec_sensor_{nullptr}; + float temperature_compensation_{0.0}; + float temperature_coefficient_{0.0}; +}; + +template class UFireECCalibrateProbeAction : public Action { + public: + UFireECCalibrateProbeAction(UFireECComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + TEMPLATABLE_VALUE(float, temperature) + + void play(Ts... x) override { + this->parent_->calibrate_probe(this->solution_.value(x...), this->temperature_.value(x...)); + } + + protected: + UFireECComponent *parent_; +}; + +template class UFireECResetAction : public Action { + public: + UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset_board(); } + + protected: + UFireECComponent *parent_; +}; + +} // namespace ufire_ec +} // namespace esphome diff --git a/esphome/components/ufire_ise/__init__.py b/esphome/components/ufire_ise/__init__.py new file mode 100644 index 0000000000..08f36c7934 --- /dev/null +++ b/esphome/components/ufire_ise/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@pvizeli"] diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py new file mode 100644 index 0000000000..8f4359d6af --- /dev/null +++ b/esphome/components/ufire_ise/sensor.py @@ -0,0 +1,127 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PH, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PH, +) + +DEPENDENCIES = ["i2c"] + +CONF_SOLUTION = "solution" +CONF_TEMPERATURE_SENSOR = "temperature_sensor" + +ufire_ise_ns = cg.esphome_ns.namespace("ufire_ise") +UFireISEComponent = ufire_ise_ns.class_( + "UFireISEComponent", cg.PollingComponent, i2c.I2CDevice +) + +# Actions +UFireISECalibrateProbeLowAction = ufire_ise_ns.class_( + "UFireISECalibrateProbeLowAction", automation.Action +) +UFireISECalibrateProbeHighAction = ufire_ise_ns.class_( + "UFireISECalibrateProbeHighAction", automation.Action +) +UFireISEResetAction = ufire_ise_ns.class_("UFireISEResetAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UFireISEComponent), + cv.Exclusive(CONF_TEMPERATURE, "temperature"): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Optional(CONF_PH): sensor.sensor_schema( + unit_of_measurement=UNIT_PH, + icon=ICON_EMPTY, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + cv.Exclusive(CONF_TEMPERATURE_SENSOR, "temperature"): cv.use_id( + sensor.Sensor + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3F)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_PH in config: + sens = await sensor.new_sensor(config[CONF_PH]) + cg.add(var.set_ph_sensor(sens)) + + if CONF_TEMPERATURE_SENSOR in config: + sens = await cg.get_variable(config[CONF_TEMPERATURE_SENSOR]) + cg.add(var.set_temperature_sensor_external(sens)) + + await i2c.register_i2c_device(var, config) + + +UFIRE_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(UFireISEComponent), + cv.Required(CONF_SOLUTION): cv.templatable(float), + } +) + + +@automation.register_action( + "ufire_ise.calibrate_probe_low", + UFireISECalibrateProbeLowAction, + UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + cg.add(var.set_solution(template_)) + return var + + +@automation.register_action( + "ufire_ise.calibrate_probe_high", + UFireISECalibrateProbeHighAction, + UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, +) +async def ufire_ise_calibrate_probe_high_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + cg.add(var.set_solution(template_)) + return var + + +UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent)}) + + +@automation.register_action( + "ufire_ise.reset", + UFireISEResetAction, + UFIRE_ISE_RESET_SCHEMA, +) +async def ufire_ise_reset_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp new file mode 100644 index 0000000000..957e6f3299 --- /dev/null +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -0,0 +1,153 @@ +#include "esphome/core/log.h" +#include "ufire_ise.h" + +#include + +namespace esphome { +namespace ufire_ise { + +static const char *const TAG = "ufire_ise"; + +void UFireISEComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up uFire_ise..."); + + uint8_t version; + if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + this->mark_failed(); + return; + } + ESP_LOGI(TAG, "Found uFire_ise board version 0x%02X", version); + + // Write option for temperature adjustments + uint8_t config; + this->read_byte(REGISTER_CONFIG, &config); + if (this->temperature_sensor_ == nullptr && this->temperature_sensor_external_ == nullptr) { + config &= ~CONFIG_TEMP_COMPENSATION; + } else { + config |= CONFIG_TEMP_COMPENSATION; + } + this->write_byte(REGISTER_CONFIG, config); +} + +void UFireISEComponent::update() { + int wait = 0; + if (this->temperature_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_TEMP); + wait += 750; + } + if (this->ph_sensor_ != nullptr) { + this->write_byte(REGISTER_TASK, COMMAND_MEASURE_MV); + wait += 750; + } + + // Wait until measurement are taken + this->set_timeout("data", wait, [this]() { this->update_internal_(); }); +} + +void UFireISEComponent::update_internal_() { + float temperature = 0; + + // Read temperature internal and populate it + if (this->temperature_sensor_ != nullptr) { + temperature = this->measure_temperature_(); + this->temperature_sensor_->publish_state(temperature); + } + // Get temperature from external only for adjustments + else if (this->temperature_sensor_external_ != nullptr) { + temperature = this->temperature_sensor_external_->state; + } + + if (this->ph_sensor_ != nullptr) { + this->ph_sensor_->publish_state(this->measure_ph_(temperature)); + } +} + +float UFireISEComponent::measure_temperature_() { return this->read_data_(REGISTER_TEMP); } + +float UFireISEComponent::measure_mv_() { return this->read_data_(REGISTER_MV); } + +float UFireISEComponent::measure_ph_(float temperature) { + float mv, ph; + + mv = this->measure_mv_(); + if (mv == -1) + return -1; + + ph = fabs(7.0 - (mv / PROBE_MV_TO_PH)); + + // Determine the temperature correction + float distance_from_7 = std::abs(7 - roundf(ph)); + float distance_from_25 = std::floor(std::abs(25 - roundf(temperature)) / 10); + float temp_multiplier = (distance_from_25 * distance_from_7) * PROBE_TMP_CORRECTION; + if ((ph >= 8.0) && (temperature >= 35)) + temp_multiplier *= -1; + if ((ph <= 6.0) && (temperature <= 15)) + temp_multiplier *= -1; + + ph += temp_multiplier; + if ((ph <= 0.0) || (ph > 14.0)) + ph = -1; + if (std::isinf(ph)) + ph = -1; + if (std::isnan(ph)) + ph = -1; + + return ph; +} + +void UFireISEComponent::set_solution_(float solution) { + solution = (7 - solution) * PROBE_MV_TO_PH; + this->write_data_(REGISTER_SOLUTION, solution); +} + +void UFireISEComponent::calibrate_probe_low(float solution) { + this->set_solution_(solution); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_LOW); +} + +void UFireISEComponent::calibrate_probe_high(float solution) { + this->set_solution_(solution); + this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_HIGH); +} + +void UFireISEComponent::reset_board() { + this->write_data_(REGISTER_REFHIGH, NAN); + this->write_data_(REGISTER_REFLOW, NAN); + this->write_data_(REGISTER_READHIGH, NAN); + this->write_data_(REGISTER_READLOW, NAN); +} + +float UFireISEComponent::read_data_(uint8_t reg) { + float f; + uint8_t temp[4]; + + this->write(®, 1); + delay(10); + + for (uint8_t i = 0; i < 4; i++) { + this->read_bytes_raw(temp + i, 1); + } + memcpy(&f, temp, sizeof(f)); + + return f; +} + +void UFireISEComponent::write_data_(uint8_t reg, float data) { + uint8_t temp[4]; + + memcpy(temp, &data, sizeof(data)); + this->write_bytes(reg, temp, 4); + delay(10); +} + +void UFireISEComponent::dump_config() { + ESP_LOGCONFIG(TAG, "uFire-ISE"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_) + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) +} + +} // namespace ufire_ise +} // namespace esphome diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h new file mode 100644 index 0000000000..01efdcdb55 --- /dev/null +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ufire_ise { + +static const float PROBE_MV_TO_PH = 59.2; +static const float PROBE_TMP_CORRECTION = 0.03; + +static const uint8_t CONFIG_TEMP_COMPENSATION = 0x02; + +static const uint8_t REGISTER_VERSION = 0; +static const uint8_t REGISTER_MV = 1; +static const uint8_t REGISTER_TEMP = 5; +static const uint8_t REGISTER_REFHIGH = 13; +static const uint8_t REGISTER_REFLOW = 17; +static const uint8_t REGISTER_READHIGH = 21; +static const uint8_t REGISTER_READLOW = 25; +static const uint8_t REGISTER_SOLUTION = 29; +static const uint8_t REGISTER_CONFIG = 38; +static const uint8_t REGISTER_TASK = 39; + +static const uint8_t COMMAND_CALIBRATE_HIGH = 8; +static const uint8_t COMMAND_CALIBRATE_LOW = 10; +static const uint8_t COMMAND_MEASURE_TEMP = 40; +static const uint8_t COMMAND_MEASURE_MV = 80; + +class UFireISEComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_temperature_sensor_external(sensor::Sensor *temperature_sensor) { + this->temperature_sensor_external_ = temperature_sensor; + } + void set_ph_sensor(sensor::Sensor *ph_sensor) { this->ph_sensor_ = ph_sensor; } + void calibrate_probe_low(float solution); + void calibrate_probe_high(float solution); + void reset_board(); + + protected: + float measure_temperature_(); + float measure_mv_(); + float measure_ph_(float temperature); + void set_solution_(float solution); + float read_data_(uint8_t reg); + void write_data_(uint8_t reg, float data); + void update_internal_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_external_{nullptr}; + sensor::Sensor *ph_sensor_{nullptr}; +}; + +template class UFireISECalibrateProbeLowAction : public Action { + public: + UFireISECalibrateProbeLowAction(UFireISEComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + + void play(Ts... x) override { this->parent_->calibrate_probe_low(this->solution_.value(x...)); } + + protected: + UFireISEComponent *parent_; +}; + +template class UFireISECalibrateProbeHighAction : public Action { + public: + UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, solution) + + void play(Ts... x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } + + protected: + UFireISEComponent *parent_; +}; + +template class UFireISEResetAction : public Action { + public: + UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset_board(); } + + protected: + UFireISEComponent *parent_; +}; + +} // namespace ufire_ise +} // namespace esphome diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 103bc3a666..07d7d8f2cf 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, + DEVICE_CLASS_DURATION, ) uptime_ns = cg.esphome_ns.namespace("uptime") @@ -17,6 +18,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( icon=ICON_TIMER, accuracy_decimals=0, state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py new file mode 100644 index 0000000000..70f130e23b --- /dev/null +++ b/esphome/components/vbus/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@ssieb"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +vbus_ns = cg.esphome_ns.namespace("vbus") +VBus = vbus_ns.class_("VBus", uart.UARTDevice, cg.Component) + +CONF_VBUS_ID = "vbus_id" + +CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" +CONF_DELTASOL_C = "deltasol_c" +CONF_DELTASOL_CS2 = "deltasol_cs2" +CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" + +CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBus), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py new file mode 100644 index 0000000000..9901fb2724 --- /dev/null +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -0,0 +1,296 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, + CONF_BINARY_SENSORS, + CONF_COMMAND, + CONF_CUSTOM, + CONF_DEST, + CONF_LAMBDA, + CONF_MODEL, + CONF_SOURCE, + DEVICE_CLASS_PROBLEM, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from .. import ( + vbus_ns, + VBus, + CONF_VBUS_ID, + CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_C, + CONF_DELTASOL_CS2, + CONF_DELTASOL_CS_PLUS, +) + +DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) +DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) +DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) +DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) +VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component) +VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component) + +CONF_RELAY1 = "relay1" +CONF_RELAY2 = "relay2" +CONF_SENSOR1_ERROR = "sensor1_error" +CONF_SENSOR2_ERROR = "sensor2_error" +CONF_SENSOR3_ERROR = "sensor3_error" +CONF_SENSOR4_ERROR = "sensor4_error" +CONF_COLLECTOR_MAX = "collector_max" +CONF_COLLECTOR_MIN = "collector_min" +CONF_COLLECTOR_FROST = "collector_frost" +CONF_TUBE_COLLECTOR = "tube_collector" +CONF_RECOOLING = "recooling" +CONF_HQM = "hqm" + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_RELAY1): binary_sensor.binary_sensor_schema(), + cv.Optional(CONF_RELAY2): binary_sensor.binary_sensor_schema(), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_MAX): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_MIN): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_FROST): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TUBE_COLLECTOR): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RECOOLING): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_HQM): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_C), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBusCustom), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_COMMAND): cv.uint16_t, + cv.Optional(CONF_SOURCE): cv.uint16_t, + cv.Optional(CONF_DEST): cv.uint16_t, + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + binary_sensor.binary_sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(VBusCustomSub), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ) + ), + } + ), + }, + key=CONF_MODEL, + lower=True, + space="_", +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4221)) + cg.add(var.set_dest(0x0010)) + if CONF_RELAY1 in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY1]) + cg.add(var.set_relay1_bsensor(sens)) + if CONF_RELAY2 in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY2]) + cg.add(var.set_relay2_bsensor(sens)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + if CONF_COLLECTOR_MAX in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MAX]) + cg.add(var.set_collector_max_bsensor(sens)) + if CONF_COLLECTOR_MIN in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MIN]) + cg.add(var.set_collector_min_bsensor(sens)) + if CONF_COLLECTOR_FROST in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_FROST]) + cg.add(var.set_collector_frost_bsensor(sens)) + if CONF_TUBE_COLLECTOR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_TUBE_COLLECTOR]) + cg.add(var.set_tube_collector_bsensor(sens)) + if CONF_RECOOLING in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RECOOLING]) + cg.add(var.set_recooling_bsensor(sens)) + if CONF_HQM in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HQM]) + cg.add(var.set_hqm_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_C: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4212)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1121)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x2211)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_CUSTOM: + if CONF_COMMAND in config: + cg.add(var.set_command(config[CONF_COMMAND])) + if CONF_SOURCE in config: + cg.add(var.set_source(config[CONF_SOURCE])) + if CONF_DEST in config: + cg.add(var.set_dest(config[CONF_DEST])) + bsensors = [] + for conf in config[CONF_BINARY_SENSORS]: + bsens = await binary_sensor.new_binary_sensor(conf) + lambda_ = await cg.process_lambda( + conf[CONF_LAMBDA], + [(cg.std_vector.template(cg.uint8), "x")], + return_type=cg.bool_, + ) + cg.add(bsens.set_message_parser(lambda_)) + bsensors.append(bsens) + cg.add(var.set_bsensors(bsensors)) + + vbus = await cg.get_variable(config[CONF_VBUS_ID]) + cg.add(vbus.register_listener(var)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp new file mode 100644 index 0000000000..6edbae22ba --- /dev/null +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -0,0 +1,142 @@ +#include "vbus_binary_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus.binary_sensor"; + +void DeltaSolBSPlusBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS Plus:"); + LOG_BINARY_SENSOR(" ", "Relay 1 On", this->relay1_bsensor_); + LOG_BINARY_SENSOR(" ", "Relay 2 On", this->relay2_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Max", this->collector_max_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Min", this->collector_min_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Frost", this->collector_frost_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Tube Collector", this->tube_collector_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Recooling", this->recooling_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Heat Quantity Measurement", this->hqm_bsensor_); +} + +void DeltaSolBSPlusBSensor::handle_message(std::vector &message) { + if (this->relay1_bsensor_ != nullptr) + this->relay1_bsensor_->publish_state(message[10] & 1); + if (this->relay2_bsensor_ != nullptr) + this->relay2_bsensor_->publish_state(message[10] & 2); + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[11] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[11] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[11] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[11] & 8); + if (this->collector_max_bsensor_ != nullptr) + this->collector_max_bsensor_->publish_state(message[15] & 1); + if (this->collector_min_bsensor_ != nullptr) + this->collector_min_bsensor_->publish_state(message[15] & 2); + if (this->collector_frost_bsensor_ != nullptr) + this->collector_frost_bsensor_->publish_state(message[15] & 4); + if (this->tube_collector_bsensor_ != nullptr) + this->tube_collector_bsensor_->publish_state(message[15] & 8); + if (this->recooling_bsensor_ != nullptr) + this->recooling_bsensor_->publish_state(message[15] & 0x10); + if (this->hqm_bsensor_ != nullptr) + this->hqm_bsensor_->publish_state(message[15] & 0x20); +} + +void DeltaSolCBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol C:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCBSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + +void DeltaSolCS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS2:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCS2BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[18] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[18] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[18] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[18] & 8); +} + +void DeltaSolCSPlusBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCSPlusBSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[20] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[20] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[20] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[20] & 8); +} + +void VBusCustomBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); + if (this->source_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Source address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Source address: 0x%04x", this->source_); + } + if (this->dest_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Dest address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Dest address: 0x%04x", this->dest_); + } + if (this->command_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Command: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); + } + ESP_LOGCONFIG(TAG, " Binary Sensors:"); + for (VBusCustomSubBSensor *bsensor : this->bsensors_) + LOG_BINARY_SENSOR(" ", "-", bsensor); +} + +void VBusCustomBSensor::handle_message(std::vector &message) { + for (VBusCustomSubBSensor *bsensor : this->bsensors_) + bsensor->parse_message(message); +} + +void VBusCustomSubBSensor::parse_message(std::vector &message) { + this->publish_state(this->message_parser_(message)); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h new file mode 100644 index 0000000000..c0a823a0ab --- /dev/null +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -0,0 +1,115 @@ +#pragma once + +#include "../vbus.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace vbus { + +class DeltaSolBSPlusBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_relay1_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay1_bsensor_ = bsensor; } + void set_relay2_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay2_bsensor_ = bsensor; } + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + void set_collector_max_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_max_bsensor_ = bsensor; } + void set_collector_min_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_min_bsensor_ = bsensor; } + void set_collector_frost_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_frost_bsensor_ = bsensor; } + void set_tube_collector_bsensor(binary_sensor::BinarySensor *bsensor) { this->tube_collector_bsensor_ = bsensor; } + void set_recooling_bsensor(binary_sensor::BinarySensor *bsensor) { this->recooling_bsensor_ = bsensor; } + void set_hqm_bsensor(binary_sensor::BinarySensor *bsensor) { this->hqm_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *relay1_bsensor_{nullptr}; + binary_sensor::BinarySensor *relay2_bsensor_{nullptr}; + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_max_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_min_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_frost_bsensor_{nullptr}; + binary_sensor::BinarySensor *tube_collector_bsensor_{nullptr}; + binary_sensor::BinarySensor *recooling_bsensor_{nullptr}; + binary_sensor::BinarySensor *hqm_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSPlusBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubBSensor; + +class VBusCustomBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_bsensors(std::vector bsensors) { this->bsensors_ = std::move(bsensors); }; + + protected: + std::vector bsensors_; + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubBSensor : public binary_sensor::BinarySensor, public Component { + public: + void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); }; + void parse_message(std::vector &message); + + protected: + message_parser_t message_parser_; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py new file mode 100644 index 0000000000..bce28758ce --- /dev/null +++ b/esphome/components/vbus/sensor/__init__.py @@ -0,0 +1,568 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_COMMAND, + CONF_CUSTOM, + CONF_DEST, + CONF_LAMBDA, + CONF_MODEL, + CONF_SENSORS, + CONF_SOURCE, + CONF_TIME, + CONF_VERSION, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_PERCENT, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HOUR, + UNIT_MINUTE, + UNIT_PERCENT, + UNIT_WATT_HOURS, +) +from .. import ( + vbus_ns, + VBus, + CONF_VBUS_ID, + CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_C, + CONF_DELTASOL_CS2, + CONF_DELTASOL_CS_PLUS, +) + +DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) +DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) +DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) +DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) +VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component) +VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component) + +CONF_FLOW_RATE = "flow_rate" +CONF_HEAT_QUANTITY = "heat_quantity" +CONF_OPERATING_HOURS = "operating_hours" +CONF_OPERATING_HOURS_1 = "operating_hours_1" +CONF_OPERATING_HOURS_2 = "operating_hours_2" +CONF_PUMP_SPEED = "pump_speed" +CONF_PUMP_SPEED_1 = "pump_speed_1" +CONF_PUMP_SPEED_2 = "pump_speed_2" +CONF_TEMPERATURE_1 = "temperature_1" +CONF_TEMPERATURE_2 = "temperature_2" +CONF_TEMPERATURE_3 = "temperature_3" +CONF_TEMPERATURE_4 = "temperature_4" +CONF_TEMPERATURE_5 = "temperature_5" + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_C), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema( + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), + CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBusCustom), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_COMMAND): cv.uint16_t, + cv.Optional(CONF_SOURCE): cv.uint16_t, + cv.Optional(CONF_DEST): cv.uint16_t, + cv.Optional(CONF_SENSORS): cv.ensure_list( + sensor.sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(VBusCustomSub), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ) + ), + } + ), + }, + key=CONF_MODEL, + lower=True, + space="_", +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4221)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_C: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4212)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1121)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED]) + cg.add(var.set_pump_speed_sensor(sens)) + if CONF_OPERATING_HOURS in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS]) + cg.add(var.set_operating_hours_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + + if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x2211)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_TEMPERATURE_5 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5]) + cg.add(var.set_temperature5_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + if CONF_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_FLOW_RATE]) + cg.add(var.set_flow_rate_sensor(sens)) + + elif config[CONF_MODEL] == CONF_CUSTOM: + if CONF_COMMAND in config: + cg.add(var.set_command(config[CONF_COMMAND])) + if CONF_SOURCE in config: + cg.add(var.set_source(config[CONF_SOURCE])) + if CONF_DEST in config: + cg.add(var.set_dest(config[CONF_DEST])) + sensors = [] + for conf in config[CONF_SENSORS]: + sens = await sensor.new_sensor(conf) + lambda_ = await cg.process_lambda( + conf[CONF_LAMBDA], + [(cg.std_vector.template(cg.uint8), "x")], + return_type=cg.float_, + ) + cg.add(sens.set_message_parser(lambda_)) + sensors.append(sens) + cg.add(var.set_sensors(sensors)) + + vbus = await cg.get_variable(config[CONF_VBUS_ID]) + cg.add(vbus.register_listener(var)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp new file mode 100644 index 0000000000..8261773431 --- /dev/null +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -0,0 +1,208 @@ +#include "vbus_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus.sensor"; + +static inline uint16_t get_u16(std::vector &message, int start) { + return (message[start + 1] << 8) + message[start]; +} + +static inline int16_t get_i16(std::vector &message, int start) { + return (int16_t) ((message[start + 1] << 8) + message[start]); +} + +void DeltaSolBSPlusSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS Plus:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBSPlusSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 16)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 18)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 20) + get_u16(message, 22) * 1000 + + get_u16(message, 24) * 1000000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 12)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); +} + +void DeltaSolCSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol C:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); +} + +void DeltaSolCSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 16) + get_u16(message, 18) * 1000 + + get_u16(message, 20) * 1000000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 22)); +} + +void DeltaSolCS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS2:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed", this->pump_speed_sensor_); + LOG_SENSOR(" ", "Operating Hours", this->operating_hours_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolCS2Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed_sensor_ != nullptr) + this->pump_speed_sensor_->publish_state(message[12]); + if (this->operating_hours_sensor_ != nullptr) + this->operating_hours_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) + this->heat_quantity_sensor_->publish_state((get_u16(message, 26) << 16) + get_u16(message, 24)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f); +} + +void DeltaSolCSPlusSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Temperature 5", this->temperature5_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); + LOG_SENSOR(" ", "Flow Rate", this->flow_rate_sensor_); +} + +void DeltaSolCSPlusSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->temperature5_sensor_ != nullptr) + this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[12]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) + this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 12)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); + if (this->flow_rate_sensor_ != nullptr) + this->flow_rate_sensor_->publish_state(get_u16(message, 38)); +} + +void VBusCustomSensor::dump_config() { + ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); + if (this->source_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Source address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Source address: 0x%04x", this->source_); + } + if (this->dest_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Dest address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Dest address: 0x%04x", this->dest_); + } + if (this->command_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Command: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); + } + ESP_LOGCONFIG(TAG, " Sensors:"); + for (VBusCustomSubSensor *sensor : this->sensors_) + LOG_SENSOR(" ", "-", sensor); +} + +void VBusCustomSensor::handle_message(std::vector &message) { + for (VBusCustomSubSensor *sensor : this->sensors_) + sensor->parse_message(message); +} + +void VBusCustomSubSensor::parse_message(std::vector &message) { + this->publish_state(this->message_parser_(message)); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h new file mode 100644 index 0000000000..6ba752b68c --- /dev/null +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -0,0 +1,151 @@ +#pragma once + +#include "../vbus.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace vbus { + +class DeltaSolBSPlusSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed_sensor(sensor::Sensor *sensor) { this->pump_speed_sensor_ = sensor; } + void set_operating_hours_sensor(sensor::Sensor *sensor) { this->operating_hours_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed_sensor_{nullptr}; + sensor::Sensor *operating_hours_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSPlusSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *temperature5_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + sensor::Sensor *flow_rate_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubSensor; + +class VBusCustomSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_sensors(std::vector sensors) { this->sensors_ = std::move(sensors); }; + + protected: + std::vector sensors_; + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubSensor : public sensor::Sensor, public Component { + public: + void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); }; + void parse_message(std::vector &message); + + protected: + message_parser_t message_parser_; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp new file mode 100644 index 0000000000..c9758891cc --- /dev/null +++ b/esphome/components/vbus/vbus.cpp @@ -0,0 +1,124 @@ +#include "vbus.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus"; + +void VBus::dump_config() { + ESP_LOGCONFIG(TAG, "VBus:"); + check_uart_settings(9600); +} + +static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) { + for (int i = 0; i < count; i++, septet >>= 1) { + if (septet & 1) + data[start + i] |= 0x80; + } +} + +static bool checksum(const uint8_t *data, int start, int count) { + uint8_t csum = 0x7f; + for (int i = 0; i < count; i++) + csum = (csum - data[start + i]) & 0x7f; + return csum == 0; +} + +void VBus::loop() { + if (!available()) + return; + + while (available()) { + uint8_t c; + read_byte(&c); + + if (c == 0xaa) { + this->state_ = 1; + this->buffer_.clear(); + continue; + } + if (c & 0x80) { + this->state_ = 0; + continue; + } + if (this->state_ == 0) + continue; + + if (this->state_ == 1) { + this->buffer_.push_back(c); + if (this->buffer_.size() == 7) { + this->protocol_ = this->buffer_[4]; + this->source_ = (this->buffer_[3] << 8) + this->buffer_[2]; + this->dest_ = (this->buffer_[1] << 8) + this->buffer_[0]; + this->command_ = (this->buffer_[6] << 8) + this->buffer_[5]; + } + if ((this->protocol_ == 0x20) && (this->buffer_.size() == 15)) { + this->state_ = 0; + if (!checksum(this->buffer_.data(), 0, 15)) { + ESP_LOGE(TAG, "P2 checksum failed"); + continue; + } + septet_spread(this->buffer_.data(), 7, 6, this->buffer_[13]); + uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7]; + uint32_t value = + (this->buffer_[12] << 24) + (this->buffer_[11] << 16) + (this->buffer_[10] << 8) + this->buffer_[9]; + ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04x (%d)", this->command_, this->source_, this->dest_, id, value, + value); + } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) { + if (!checksum(this->buffer_.data(), 0, 9)) { + ESP_LOGE(TAG, "P1 checksum failed"); + this->state_ = 0; + continue; + } + this->frames_ = this->buffer_[7]; + if (this->frames_) { + this->state_ = 2; + this->cframe_ = 0; + this->fbcount_ = 0; + this->buffer_.clear(); + } else { + this->state_ = 0; + ESP_LOGD(TAG, "P1 empty message"); + } + } + continue; + } + + if (this->state_ == 2) { + this->fbytes_[this->fbcount_++] = c; + if (this->fbcount_ < 6) + continue; + this->fbcount_ = 0; + if (!checksum(this->fbytes_, 0, 6)) { + ESP_LOGE(TAG, "frame checksum failed"); + continue; + } + septet_spread(this->fbytes_, 0, 4, this->fbytes_[4]); + for (int i = 0; i < 4; i++) + this->buffer_.push_back(this->fbytes_[i]); + if (++this->cframe_ < this->frames_) + continue; + ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, + format_hex(this->buffer_).c_str()); + for (auto &listener : this->listeners_) + listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); + this->state_ = 0; + continue; + } + } +} + +void VBusListener::on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector &message) { + if ((this->command_ != 0xffff) && (this->command_ != command)) + return; + if ((this->source_ != 0xffff) && (this->source_ != source)) + return; + if ((this->dest_ != 0xffff) && (this->dest_ != dest)) + return; + this->handle_message(message); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h new file mode 100644 index 0000000000..7e97b5049a --- /dev/null +++ b/esphome/components/vbus/vbus.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace vbus { + +using message_parser_t = std::function &)>; + +class VBus; + +class VBusListener { + public: + void set_command(uint16_t command) { this->command_ = command; } + void set_source(uint16_t source) { this->source_ = source; } + void set_dest(uint16_t dest) { this->dest_ = dest; } + + void on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector &message); + + protected: + uint16_t command_{0xffff}; + uint16_t source_{0xffff}; + uint16_t dest_{0xffff}; + + virtual void handle_message(std::vector &message) = 0; +}; + +class VBus : public uart::UARTDevice, public Component { + public: + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } + + protected: + int state_{0}; + std::vector buffer_; + uint8_t protocol_; + uint16_t source_; + uint16_t dest_; + uint16_t command_; + uint8_t frames_; + uint8_t cframe_; + uint8_t fbytes_[6]; + int fbcount_; + std::vector listeners_{}; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index f851cf6d73..b07779a653 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -88,7 +88,7 @@ void VL53L0XSensor::setup() { this->timeout_start_us_ = micros(); while (reg(0x83).get() == 0x00) { - if (this->timeout_us_ > 0 && ((uint16_t)(micros() - this->timeout_start_us_) > this->timeout_us_)) { + if (this->timeout_us_ > 0 && ((uint16_t) (micros() - this->timeout_start_us_) > this->timeout_us_)) { ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str()); this->mark_failed(); return; diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py new file mode 100644 index 0000000000..20698a1b82 --- /dev/null +++ b/esphome/components/voice_assistant/__init__.py @@ -0,0 +1,105 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ID, CONF_MICROPHONE +from esphome import automation +from esphome.automation import register_action +from esphome.components import microphone + +AUTO_LOAD = ["socket"] +DEPENDENCIES = ["api", "microphone"] + +CODEOWNERS = ["@jesserockz"] + +CONF_ON_START = "on_start" +CONF_ON_STT_END = "on_stt_end" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_END = "on_tts_end" +CONF_ON_END = "on_end" +CONF_ON_ERROR = "on_error" + + +voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant") +VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) + +StartAction = voice_assistant_ns.class_( + "StartAction", automation.Action, cg.Parented.template(VoiceAssistant) +) +StopAction = voice_assistant_ns.class_( + "StopAction", automation.Action, cg.Parented.template(VoiceAssistant) +) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(VoiceAssistant), + cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Optional(CONF_ON_START): automation.validate_automation(single=True), + cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TTS_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_END): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + mic = await cg.get_variable(config[CONF_MICROPHONE]) + cg.add(var.set_microphone(mic)) + + if CONF_ON_START in config: + await automation.build_automation( + var.get_start_trigger(), [], config[CONF_ON_START] + ) + + if CONF_ON_STT_END in config: + await automation.build_automation( + var.get_stt_end_trigger(), [(cg.std_string, "x")], config[CONF_ON_STT_END] + ) + + if CONF_ON_TTS_START in config: + await automation.build_automation( + var.get_tts_start_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_TTS_START], + ) + + if CONF_ON_TTS_END in config: + await automation.build_automation( + var.get_tts_end_trigger(), [(cg.std_string, "x")], config[CONF_ON_TTS_END] + ) + + if CONF_ON_END in config: + await automation.build_automation( + var.get_end_trigger(), [], config[CONF_ON_END] + ) + + if CONF_ON_ERROR in config: + await automation.build_automation( + var.get_error_trigger(), + [(cg.std_string, "code"), (cg.std_string, "message")], + config[CONF_ON_ERROR], + ) + + cg.add_define("USE_VOICE_ASSISTANT") + + +VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssistant)}) + + +@register_action("voice_assistant.start", StartAction, VOICE_ASSISTANT_ACTION_SCHEMA) +async def voice_assistant_listen_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@register_action("voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA) +async def voice_assistant_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp new file mode 100644 index 0000000000..e2d5bea90a --- /dev/null +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -0,0 +1,156 @@ +#include "voice_assistant.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace voice_assistant { + +static const char *const TAG = "voice_assistant"; + +float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void VoiceAssistant::setup() { + ESP_LOGCONFIG(TAG, "Setting up Voice Assistant..."); + + global_voice_assistant = this; + + this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (socket_ == nullptr) { + ESP_LOGW(TAG, "Could not create socket."); + this->mark_failed(); + return; + } + int enable = 1; + int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = socket_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + this->mic_->add_data_callback([this](const std::vector &data) { + if (!this->running_) { + return; + } + this->socket_->sendto(data.data(), data.size(), 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); + }); +} + +void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { + ESP_LOGD(TAG, "Starting..."); + + memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_)); + if (this->dest_addr_.ss_family == AF_INET) { + ((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port); + } +#if LWIP_IPV6 + else if (this->dest_addr_.ss_family == AF_INET6) { + ((struct sockaddr_in6 *) &this->dest_addr_)->sin6_port = htons(port); + } +#endif + else { + ESP_LOGW(TAG, "Unknown address family: %d", this->dest_addr_.ss_family); + return; + } + this->running_ = true; + this->mic_->start(); +} + +void VoiceAssistant::request_start() { + ESP_LOGD(TAG, "Requesting start..."); + if (!api::global_api_server->start_voice_assistant()) { + ESP_LOGW(TAG, "Could not request start."); + this->error_trigger_->trigger("not-connected", "Could not request start."); + } +} + +void VoiceAssistant::signal_stop() { + ESP_LOGD(TAG, "Signaling stop..."); + this->mic_->stop(); + this->running_ = false; + api::global_api_server->stop_voice_assistant(); + memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); +} + +void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { + switch (msg.event_type) { + case api::enums::VOICE_ASSISTANT_RUN_START: + ESP_LOGD(TAG, "Assist Pipeline running"); + this->start_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_STT_END: { + std::string text; + for (auto arg : msg.data) { + if (arg.name == "text") { + text = std::move(arg.value); + } + } + if (text.empty()) { + ESP_LOGW(TAG, "No text in STT_END event."); + return; + } + ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); + this->stt_end_trigger_->trigger(text); + break; + } + case api::enums::VOICE_ASSISTANT_TTS_START: { + std::string text; + for (auto arg : msg.data) { + if (arg.name == "text") { + text = std::move(arg.value); + } + } + if (text.empty()) { + ESP_LOGW(TAG, "No text in TTS_START event."); + return; + } + ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); + this->tts_start_trigger_->trigger(text); + break; + } + case api::enums::VOICE_ASSISTANT_TTS_END: { + std::string url; + for (auto arg : msg.data) { + if (arg.name == "url") { + url = std::move(arg.value); + } + } + if (url.empty()) { + ESP_LOGW(TAG, "No url in TTS_END event."); + return; + } + ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); + this->tts_end_trigger_->trigger(url); + break; + } + case api::enums::VOICE_ASSISTANT_RUN_END: + ESP_LOGD(TAG, "Assist Pipeline ended"); + this->end_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_ERROR: { + std::string code = ""; + std::string message = ""; + for (auto arg : msg.data) { + if (arg.name == "code") { + code = std::move(arg.value); + } else if (arg.name == "message") { + message = std::move(arg.value); + } + } + ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); + this->error_trigger_->trigger(code, message); + } + default: + break; + } +} + +VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace voice_assistant +} // namespace esphome diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h new file mode 100644 index 0000000000..813c006e98 --- /dev/null +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_server.h" +#include "esphome/components/microphone/microphone.h" +#include "esphome/components/socket/socket.h" + +namespace esphome { +namespace voice_assistant { + +class VoiceAssistant : public Component { + public: + void setup() override; + float get_setup_priority() const override; + void start(struct sockaddr_storage *addr, uint16_t port); + + void set_microphone(microphone::Microphone *mic) { this->mic_ = mic; } + + void request_start(); + void signal_stop(); + + void on_event(const api::VoiceAssistantEventResponse &msg); + + Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } + Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } + Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_error_trigger() const { return this->error_trigger_; } + + protected: + std::unique_ptr socket_ = nullptr; + struct sockaddr_storage dest_addr_; + + Trigger<> *start_trigger_ = new Trigger<>(); + Trigger *stt_end_trigger_ = new Trigger(); + Trigger *tts_start_trigger_ = new Trigger(); + Trigger *tts_end_trigger_ = new Trigger(); + Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *error_trigger_ = new Trigger(); + + microphone::Microphone *mic_{nullptr}; + + bool running_{false}; +}; + +template class StartAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_start(); } +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->signal_stop(); } +}; + +extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace voice_assistant +} // namespace esphome diff --git a/esphome/components/wake_on_lan/button.py b/esphome/components/wake_on_lan/button.py index 2710eb3df9..778ea60cfa 100644 --- a/esphome/components/wake_on_lan/button.py +++ b/esphome/components/wake_on_lan/button.py @@ -12,11 +12,12 @@ WakeOnLanButton = wake_on_lan_ns.class_("WakeOnLanButton", button.Button, cg.Com DEPENDENCIES = ["network"] CONFIG_SCHEMA = cv.All( - button.BUTTON_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + button.button_schema(WakeOnLanButton) + .extend(cv.COMPONENT_SCHEMA) + .extend( cv.Schema( { cv.Required(CONF_TARGET_MAC_ADDRESS): cv.mac_address, - cv.GenerateID(): cv.declare_id(WakeOnLanButton), } ), ), diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a9d8350404..747794b631 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -29,6 +29,7 @@ WaveshareEPaper2P7In = waveshare_epaper_ns.class_( WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( "WaveshareEPaper2P9InB", WaveshareEPaper ) +GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper) WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) @@ -73,6 +74,7 @@ MODELS = { "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), + "gdey029t94": ("c", GDEY029T94), "2.70in": ("b", WaveshareEPaper2P7In), "2.90in-b": ("b", WaveshareEPaper2P9InB), "4.20in": ("b", WaveshareEPaper4P2In), @@ -88,13 +90,17 @@ MODELS = { } -def validate_full_update_every_only_type_a(value): +def validate_full_update_every_only_types_ac(value): if CONF_FULL_UPDATE_EVERY not in value: return value if MODELS[value[CONF_MODEL]][0] == "b": + full_models = [] + for key, val in sorted(MODELS.items()): + if val[0] != "b": + full_models.append(key) raise cv.Invalid( "The 'full_update_every' option is only available for models " - "'1.54in', '1.54inV2', '2.13in', '2.90in', and '2.90inV2'." + + ", ".join(full_models) ) return value @@ -116,7 +122,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("1s")) .extend(spi.spi_device_schema()), - validate_full_update_every_only_type_a, + validate_full_update_every_only_types_ac, cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 5580674c34..8c4b137514 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -137,7 +137,7 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; - const uint32_t pos = (x + y * this->get_width_internal()) / 8u; + const uint32_t pos = (x + y * this->get_width_controller()) / 8u; const uint8_t subpos = x & 0x07; // flip logic if (!color.is_on()) { @@ -146,7 +146,9 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color this->buffer_[pos] &= ~(0x80 >> subpos); } } -uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal() / 8u; } +uint32_t WaveshareEPaper::get_buffer_length_() { + return this->get_width_controller() * this->get_height_internal() / 8u; +} void WaveshareEPaper::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -291,7 +293,7 @@ void HOT WaveshareEPaperTypeA::display() { // COMMAND SET RAM X ADDRESS START END POSITION this->command(0x44); this->data(0x00); - this->data((this->get_width_internal() - 1) >> 3); + this->data((this->get_width_controller() - 1) >> 3); // COMMAND SET RAM Y ADDRESS START END POSITION this->command(0x45); this->data(this->get_height_internal() - 1); @@ -392,12 +394,26 @@ int WaveshareEPaperTypeA::get_width_internal() { case TTGO_EPAPER_2_13_IN_B73: case TTGO_EPAPER_2_13_IN_B74: case TTGO_EPAPER_2_13_IN_B1: + return 122; case WAVESHARE_EPAPER_2_9_IN: case WAVESHARE_EPAPER_2_9_IN_V2: return 128; } return 0; } +// The controller of the 2.13" displays has a buffer larger than screen size +int WaveshareEPaperTypeA::get_width_controller() { + switch (this->model_) { + case WAVESHARE_EPAPER_2_13_IN: + case TTGO_EPAPER_2_13_IN: + case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: + case TTGO_EPAPER_2_13_IN_B1: + return 128; + default: + return this->get_width_internal(); + } +} int WaveshareEPaperTypeA::get_height_internal() { switch (this->model_) { case WAVESHARE_EPAPER_1_54_IN: @@ -663,6 +679,90 @@ void WaveshareEPaper2P9InB::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// Good Display 2.9in black/white/grey +// Datasheet: +// - https://v4.cecdn.yun300.cn/100001_1909185148/SSD1680.pdf +// - https://github.com/adafruit/Adafruit_EPD/blob/master/src/panels/ThinkInk_290_Grayscale4_T5.h +// ======================================================== + +void GDEY029T94::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 POWER SETTINGS + this->command(0x00); + this->data(0x03); + this->data(0x00); + this->data(0x2b); + this->data(0x2b); + this->data(0x03); /* for b/w */ + + // 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_(); + + // Not sure what this does but it's in the Adafruit EPD library + this->command(0xFF); + 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(0b10011111); + + // COMMAND RESOLUTION SETTING + // set to 128x296 by COMMAND PANEL SETTING + + // COMMAND VCOM AND DATA INTERVAL SETTING + // use defaults for white border and ESPHome image polarity + + // EPD hardware init end +} +void HOT GDEY029T94::display() { + // COMMAND DATA START TRANSMISSION 2 (B/W only) + this->command(0x13); + delay(2); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) { + this->write_byte(this->buffer_[i]); + } + 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 GDEY029T94::get_width_internal() { return 128; } +int GDEY029T94::get_height_internal() { return 296; } +void GDEY029T94::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper (Good Display)", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in Greyscale GDEY029T94"); + 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 e613899149..a674d3af0c 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -54,6 +54,8 @@ class WaveshareEPaper : public PollingComponent, } } + virtual int get_width_controller() { return this->get_width_internal(); }; + uint32_t get_buffer_length_(); uint32_t reset_duration_{200}; @@ -111,6 +113,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { int get_height_internal() override; + int get_width_controller() override; + uint32_t full_update_every_{30}; uint32_t at_update_{0}; WaveshareEPaperTypeAModel model_; @@ -146,6 +150,26 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { int get_height_internal() override; }; +class GDEY029T94 : 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 WaveshareEPaper2P9InB : public WaveshareEPaper { public: void initialize() override; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 42683c8d77..d8343c6c39 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -75,6 +75,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_arduino, + cv.only_on(["esp32", "esp8266"]), default_url, validate_local, ) diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h index 75c7130151..8eaaaf4581 100644 --- a/esphome/components/web_server/server_index.h +++ b/esphome/components/web_server/server_index.h @@ -6,580 +6,585 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, - 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x25, 0x10, 0x22, 0x29, 0xcb, 0x76, 0x81, 0x02, 0x79, 0xe5, - 0xa5, 0xae, 0x5d, 0xe5, 0xad, 0x2c, 0xd9, 0x75, 0xab, 0x54, 0x2c, 0x0b, 0x22, 0x93, 0x22, 0xca, 0x20, 0xc0, 0x02, - 0x92, 0x5a, 0x8a, 0x42, 0x9f, 0x7e, 0xea, 0xa7, 0x39, 0x67, 0xd6, 0x87, 0x7e, 0x99, 0xd3, 0xfd, 0x30, 0x1f, 0x31, - 0xcf, 0xfd, 0x29, 0xf7, 0x07, 0xa6, 0x3f, 0x61, 0x22, 0x22, 0x17, 0x24, 0x40, 0x6a, 0x71, 0x75, 0xcd, 0x3d, 0x5e, - 0x04, 0xe4, 0x1a, 0x11, 0x19, 0x19, 0x5b, 0x46, 0x42, 0xbb, 0x77, 0xc6, 0xd9, 0x88, 0x5f, 0xcc, 0x99, 0x35, 0xe5, - 0xb3, 0xa4, 0xbf, 0x2b, 0xff, 0x67, 0xd1, 0xb8, 0xbf, 0x9b, 0xc4, 0xe9, 0x27, 0x2b, 0x67, 0x49, 0x18, 0x8f, 0xb2, - 0xd4, 0x9a, 0xe6, 0x6c, 0x12, 0x8e, 0x23, 0x1e, 0x05, 0xf1, 0x2c, 0x3a, 0x61, 0xd6, 0x56, 0x7f, 0x77, 0xc6, 0x78, - 0x64, 0x8d, 0xa6, 0x51, 0x5e, 0x30, 0x1e, 0xbe, 0x3f, 0xf8, 0xba, 0xf5, 0xa8, 0xbf, 0x5b, 0x8c, 0xf2, 0x78, 0xce, - 0x2d, 0x1c, 0x32, 0x9c, 0x65, 0xe3, 0x45, 0xc2, 0xfa, 0xa7, 0x51, 0x6e, 0x9d, 0xb3, 0xf0, 0xcd, 0xf1, 0x2f, 0x6c, - 0xc4, 0xfd, 0x31, 0x9b, 0xc4, 0x29, 0x7b, 0x9b, 0x67, 0x73, 0x96, 0xf3, 0x0b, 0xef, 0xd9, 0xfa, 0x8a, 0x98, 0x15, - 0xde, 0xbe, 0xae, 0x3a, 0x61, 0xfc, 0xcd, 0x59, 0xaa, 0xfa, 0x3c, 0x65, 0x62, 0x92, 0x2c, 0x2f, 0x3c, 0x7e, 0x45, - 0x9b, 0xfd, 0x8b, 0xd9, 0x71, 0x96, 0x14, 0xde, 0x27, 0x5d, 0x3f, 0xcf, 0x33, 0x9e, 0x21, 0x58, 0xfe, 0x34, 0x2a, - 0x8c, 0x96, 0xde, 0x93, 0x35, 0x4d, 0xe6, 0xb2, 0xf2, 0x45, 0xf1, 0x2c, 0x5d, 0xcc, 0x58, 0x1e, 0x1d, 0x27, 0xcc, - 0x2b, 0x58, 0xe8, 0x30, 0x8f, 0x7b, 0xb1, 0x1b, 0xf6, 0xb9, 0x15, 0xa7, 0x16, 0x1b, 0x9c, 0x33, 0x2a, 0x59, 0x32, - 0xdd, 0x2a, 0xb8, 0xd3, 0xf6, 0x80, 0x5c, 0x93, 0xf8, 0x64, 0xa1, 0xdf, 0xcf, 0xf2, 0x98, 0xab, 0xe7, 0xd3, 0x28, - 0x59, 0xb0, 0x20, 0x2e, 0xdd, 0x80, 0x1d, 0xf2, 0x61, 0x18, 0x7b, 0x4f, 0x68, 0x50, 0x18, 0x72, 0x39, 0xc9, 0x72, - 0x07, 0x69, 0x15, 0xe3, 0xd8, 0xfc, 0xf2, 0xd2, 0xe1, 0xe1, 0xb2, 0x74, 0xdd, 0x4f, 0xcc, 0x1f, 0x45, 0x49, 0xe2, - 0xe0, 0xc4, 0x77, 0xef, 0x16, 0x38, 0x63, 0xec, 0xf1, 0xc3, 0x78, 0xe8, 0xf6, 0xe2, 0x89, 0xc3, 0x99, 0x5b, 0xf5, - 0xcb, 0x26, 0x16, 0x67, 0x0e, 0x77, 0xdd, 0x27, 0x57, 0xf7, 0xc9, 0x19, 0x5f, 0xe4, 0x00, 0x7b, 0xe9, 0xbd, 0x51, - 0x33, 0x3f, 0xc3, 0xfa, 0x7d, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x08, 0xcf, 0xe2, 0x74, 0x9c, 0x9d, 0xf9, - 0xfb, 0xd3, 0x08, 0x7e, 0xbc, 0xcb, 0x32, 0x7e, 0xf7, 0xae, 0x73, 0x9a, 0xc5, 0x63, 0xab, 0x1d, 0x86, 0x66, 0xe5, - 0xc5, 0x93, 0xfd, 0xfd, 0xcb, 0xcb, 0x46, 0x81, 0x9f, 0x46, 0x3c, 0x3e, 0x65, 0xa2, 0x33, 0x00, 0x60, 0xc3, 0xcf, - 0x39, 0x67, 0xe3, 0x7d, 0x7e, 0x91, 0x40, 0x29, 0x63, 0xbc, 0xb0, 0x01, 0xc7, 0xa7, 0xd9, 0x08, 0xc8, 0x96, 0x1a, - 0x84, 0x87, 0xa6, 0x39, 0x9b, 0x27, 0xd1, 0x88, 0x61, 0x3d, 0x8c, 0x54, 0xf5, 0xa8, 0x1a, 0x79, 0x5f, 0x87, 0x62, - 0x79, 0x1d, 0xd7, 0x8b, 0x59, 0x98, 0xb2, 0x33, 0xeb, 0x55, 0x34, 0xef, 0x8d, 0x92, 0xa8, 0x28, 0x80, 0x5f, 0x97, - 0x84, 0x42, 0xbe, 0x18, 0x01, 0x83, 0x10, 0x82, 0x4b, 0x24, 0xd3, 0x34, 0x2e, 0xfc, 0x8f, 0x1b, 0xa3, 0xa2, 0x78, - 0xc7, 0x8a, 0x45, 0xc2, 0x37, 0x42, 0x58, 0x0b, 0x7e, 0x27, 0x0c, 0xbf, 0x76, 0xf9, 0x34, 0xcf, 0xce, 0xac, 0x67, - 0x79, 0x0e, 0xcd, 0x6d, 0x98, 0x52, 0x34, 0xb0, 0xe2, 0xc2, 0x4a, 0x33, 0x6e, 0xe9, 0xc1, 0x70, 0x01, 0x7d, 0xeb, - 0x7d, 0xc1, 0xac, 0xa3, 0x45, 0x5a, 0x44, 0x13, 0x06, 0x4d, 0x8f, 0xac, 0x2c, 0xb7, 0x8e, 0x60, 0xd0, 0x23, 0x58, - 0xb2, 0x82, 0xc3, 0xae, 0xf1, 0x6d, 0xb7, 0x47, 0x73, 0x41, 0xe1, 0x01, 0x3b, 0xe7, 0x21, 0x2b, 0x81, 0x31, 0xad, - 0x42, 0xa3, 0xe1, 0xb8, 0xcb, 0x04, 0x0a, 0x58, 0x18, 0x33, 0x64, 0x59, 0xc7, 0x6c, 0xac, 0x17, 0xe7, 0xc3, 0xdd, - 0xbb, 0x9a, 0xd6, 0x40, 0x13, 0x07, 0xda, 0x16, 0x8d, 0xb6, 0x9e, 0x40, 0xbc, 0x46, 0x22, 0xd7, 0x63, 0xbe, 0x24, - 0xdf, 0xfe, 0x45, 0x3a, 0xaa, 0x8f, 0x0d, 0x95, 0x25, 0xcf, 0xf6, 0x79, 0x1e, 0xa7, 0x27, 0x00, 0x84, 0x9c, 0xc9, - 0x6c, 0x52, 0x96, 0x62, 0xf1, 0xdf, 0xb0, 0x90, 0x85, 0x7d, 0x1c, 0x3d, 0x67, 0x8e, 0x5d, 0x50, 0x0f, 0x3b, 0x0c, - 0x91, 0xf4, 0xc0, 0x60, 0x6c, 0xc0, 0x02, 0xb6, 0x69, 0xdb, 0xde, 0xd7, 0xae, 0x77, 0x81, 0x1c, 0xe4, 0xfb, 0x3e, - 0xb1, 0xaf, 0xe8, 0x1c, 0x87, 0x1d, 0x04, 0xda, 0x4f, 0x58, 0x7a, 0xc2, 0xa7, 0x03, 0x76, 0xd8, 0x1e, 0x06, 0x1c, - 0xa0, 0x1a, 0x2f, 0x46, 0xcc, 0x41, 0x7e, 0xf4, 0x72, 0xdc, 0x3e, 0x9b, 0x0e, 0x4c, 0x81, 0x0b, 0x73, 0x87, 0x70, - 0xac, 0x2d, 0x8d, 0xab, 0x58, 0x54, 0x01, 0x86, 0x7c, 0x6e, 0xc3, 0x0e, 0x3b, 0x66, 0xb9, 0x01, 0x87, 0x6e, 0xd6, - 0xab, 0xad, 0xe0, 0x02, 0x56, 0x08, 0xfa, 0x59, 0x93, 0x45, 0x3a, 0xe2, 0x31, 0x08, 0x2e, 0x7b, 0x13, 0xc0, 0x15, - 0x2b, 0xa7, 0x17, 0xce, 0x76, 0x4b, 0xd7, 0x89, 0xdd, 0x4d, 0x76, 0x98, 0x6f, 0x76, 0x86, 0x1e, 0x42, 0xa9, 0x89, - 0x2f, 0x11, 0x8f, 0x01, 0xc1, 0xd2, 0x7b, 0xcb, 0xf4, 0xf6, 0xfc, 0x30, 0x60, 0xfe, 0x2a, 0x1f, 0x87, 0xdc, 0x9f, - 0x45, 0x73, 0xc4, 0x86, 0x11, 0x0f, 0x44, 0xe9, 0x08, 0xa1, 0xab, 0xad, 0x0b, 0x52, 0xcc, 0xaf, 0x58, 0xc0, 0x05, - 0x82, 0xc0, 0x9e, 0x7d, 0x16, 0x8d, 0xa6, 0xb0, 0xc5, 0x2b, 0xc2, 0x8d, 0xd5, 0x76, 0x18, 0xe5, 0x2c, 0xe2, 0xec, - 0x59, 0xc2, 0xf0, 0x0d, 0x57, 0x00, 0x7a, 0xda, 0xae, 0x97, 0xab, 0x7d, 0x97, 0xc4, 0xfc, 0x75, 0x06, 0xf3, 0xf4, - 0x04, 0x93, 0x00, 0x17, 0xe7, 0x77, 0xef, 0xc6, 0xc8, 0x22, 0x7b, 0x1c, 0x56, 0xeb, 0x78, 0x01, 0x42, 0xc0, 0x4e, - 0xb1, 0x85, 0x0d, 0xd4, 0xf6, 0x62, 0x9f, 0x03, 0x11, 0x9f, 0x64, 0x29, 0x87, 0xe1, 0x00, 0x5e, 0xcd, 0x41, 0x7e, - 0x34, 0x9f, 0xb3, 0x74, 0xfc, 0x64, 0x1a, 0x27, 0x63, 0xa0, 0x46, 0x09, 0xf8, 0x66, 0x2c, 0x04, 0x3c, 0x01, 0x99, - 0xe0, 0x7a, 0x8c, 0x68, 0xf9, 0x90, 0x91, 0x79, 0x68, 0xdb, 0x3d, 0x94, 0x40, 0x12, 0x0b, 0x94, 0x41, 0xb4, 0x70, - 0xef, 0x40, 0xf4, 0x17, 0x2e, 0xdf, 0x0c, 0x63, 0xbd, 0x8c, 0x92, 0xc0, 0x6f, 0x50, 0xd2, 0x00, 0xfd, 0x19, 0xc8, - 0xc0, 0x1e, 0x0a, 0xae, 0xef, 0xa5, 0xd4, 0x49, 0x99, 0xc2, 0x10, 0x08, 0x30, 0x42, 0x09, 0x22, 0x69, 0xf0, 0x36, - 0x4b, 0x2e, 0x26, 0x71, 0x92, 0xec, 0x2f, 0xe6, 0xf3, 0x2c, 0xe7, 0xde, 0x37, 0xe1, 0x92, 0x67, 0x15, 0xae, 0xb4, - 0xc9, 0x8b, 0xb3, 0x98, 0x23, 0x41, 0xdd, 0xe5, 0x28, 0x82, 0xa5, 0x7e, 0x9c, 0x65, 0x09, 0x8b, 0x52, 0x40, 0x83, - 0x0d, 0x6c, 0x3b, 0x48, 0x17, 0x49, 0xd2, 0x3b, 0x86, 0x61, 0x3f, 0xf5, 0xa8, 0x5a, 0x48, 0xfc, 0x80, 0x9e, 0xf7, - 0xf2, 0x3c, 0xba, 0x80, 0x86, 0xd8, 0x06, 0x78, 0x11, 0x56, 0xeb, 0x9b, 0xfd, 0x37, 0xaf, 0x7d, 0xc1, 0xf8, 0xf1, - 0xe4, 0x02, 0x00, 0x2d, 0x2b, 0xa9, 0x39, 0xc9, 0xb3, 0x59, 0x63, 0x6a, 0xa4, 0x43, 0x1c, 0xb2, 0xde, 0x15, 0x20, - 0xc4, 0x34, 0x32, 0xac, 0x12, 0x33, 0x21, 0x78, 0x4d, 0xfc, 0x2c, 0x2b, 0x71, 0x0f, 0x0c, 0xf0, 0x21, 0x10, 0xc5, - 0x30, 0xe5, 0xf5, 0xd0, 0xf2, 0xfc, 0x62, 0x19, 0x87, 0x04, 0xe7, 0x1c, 0xf5, 0x2f, 0xc2, 0x38, 0x8a, 0x60, 0xf6, - 0xa5, 0x18, 0xb0, 0x54, 0x10, 0xc7, 0x65, 0xe9, 0x25, 0x9a, 0x89, 0x51, 0xe2, 0xa1, 0x40, 0xe1, 0xb0, 0x8d, 0x2e, - 0x2f, 0x19, 0xbc, 0xb8, 0xde, 0xb7, 0xe1, 0x32, 0x52, 0xf8, 0xa0, 0x86, 0xc2, 0xfd, 0x15, 0x08, 0x39, 0x81, 0x9a, - 0xec, 0x14, 0xf4, 0x20, 0xc0, 0xf9, 0x8d, 0x07, 0xfa, 0x3f, 0x41, 0x28, 0xee, 0x74, 0x3c, 0xd0, 0xa0, 0x4f, 0xa6, - 0x51, 0x7a, 0xc2, 0xc6, 0x41, 0xc2, 0x4a, 0x29, 0x79, 0xf7, 0x2c, 0x58, 0x63, 0x60, 0xa7, 0xc2, 0x7a, 0x7e, 0xf0, - 0xea, 0xa5, 0x5c, 0xb9, 0x9a, 0x30, 0x86, 0x45, 0x5a, 0x80, 0x5a, 0x05, 0xb1, 0x2d, 0xc5, 0xf1, 0x33, 0xae, 0xa4, - 0xb7, 0x28, 0x89, 0x8b, 0xf7, 0x73, 0x30, 0x31, 0xd8, 0x5b, 0x18, 0x06, 0xa6, 0x0f, 0x61, 0x2a, 0x2a, 0x87, 0xf9, - 0x44, 0xc5, 0x58, 0x17, 0x41, 0x67, 0x81, 0xa9, 0x78, 0xcd, 0x1c, 0xb7, 0x04, 0x56, 0xe5, 0xf1, 0xc8, 0x8a, 0xc6, - 0xe3, 0x17, 0x69, 0xcc, 0xe3, 0x28, 0x89, 0x7f, 0x23, 0x4a, 0x2e, 0x91, 0xc7, 0x78, 0x4f, 0x2e, 0x02, 0xe0, 0x4e, - 0x3d, 0x12, 0x57, 0x09, 0xd9, 0x3b, 0x44, 0x0c, 0x21, 0x2d, 0x93, 0xf0, 0x70, 0x28, 0xc1, 0x4b, 0xfc, 0xf9, 0xa2, - 0x98, 0x22, 0x61, 0xe5, 0xc0, 0x28, 0xc8, 0xb3, 0xe3, 0x82, 0xe5, 0xa7, 0x6c, 0xac, 0x39, 0xa0, 0x00, 0xac, 0xa8, - 0x39, 0x18, 0x2f, 0x34, 0xa3, 0xa3, 0x74, 0x28, 0x83, 0xa1, 0x7a, 0xa6, 0x98, 0x65, 0x92, 0x99, 0xb5, 0x85, 0xa3, - 0xa5, 0x80, 0x23, 0x8c, 0x0a, 0x29, 0x09, 0xf2, 0x50, 0x61, 0x38, 0x05, 0x29, 0x04, 0x5a, 0xc1, 0xdc, 0xe6, 0x4a, - 0x93, 0x3d, 0x5b, 0x90, 0x4a, 0xc8, 0xa1, 0x23, 0x6c, 0x64, 0x82, 0x34, 0x77, 0x61, 0x57, 0x81, 0x94, 0x97, 0xe0, - 0x0a, 0x29, 0xa2, 0xcc, 0x1c, 0x64, 0x80, 0xf0, 0x5b, 0xa1, 0x0b, 0x7d, 0x6c, 0x41, 0x6c, 0xe0, 0xeb, 0x95, 0x07, - 0xc2, 0x4a, 0xbc, 0x2b, 0x44, 0xbc, 0x2b, 0xc0, 0xc6, 0x89, 0x91, 0x9f, 0xbc, 0x3b, 0xdc, 0x4f, 0xb3, 0xbd, 0xd1, - 0x88, 0x15, 0x45, 0x06, 0xb0, 0xdd, 0xa1, 0xf6, 0x57, 0x19, 0x5a, 0x40, 0x49, 0x57, 0xcb, 0x3a, 0xbb, 0x20, 0x0d, - 0x6e, 0xaa, 0x15, 0xa5, 0xd3, 0x03, 0xfb, 0xe3, 0x47, 0x90, 0xd9, 0x9e, 0x24, 0x03, 0x50, 0x7d, 0xd5, 0xf0, 0x13, - 0xf6, 0x4c, 0x9d, 0x32, 0x6b, 0xed, 0x4b, 0xa7, 0x0e, 0x92, 0x07, 0xc3, 0xba, 0xa5, 0xb1, 0xa0, 0x6b, 0x87, 0xc6, - 0xd5, 0x90, 0x0a, 0x72, 0x79, 0x42, 0x2a, 0xdb, 0x58, 0x46, 0xb0, 0xda, 0x4a, 0x8f, 0x48, 0xaf, 0xb0, 0x29, 0x08, - 0xd0, 0x43, 0x36, 0xec, 0xc9, 0xfa, 0x30, 0x17, 0x94, 0xcb, 0xd9, 0xaf, 0x0b, 0x56, 0x70, 0xc1, 0xba, 0x30, 0x6e, - 0x01, 0xe3, 0x96, 0x2b, 0xd6, 0x61, 0xcd, 0x76, 0x5c, 0x07, 0xdb, 0x9b, 0x39, 0xea, 0xb1, 0x02, 0x39, 0xf9, 0x7a, - 0x76, 0x42, 0x58, 0x99, 0x7b, 0x79, 0xf9, 0xad, 0x1a, 0xa4, 0x5a, 0x4a, 0x6d, 0x03, 0x35, 0xd6, 0xc4, 0x56, 0x4d, - 0xc6, 0xb6, 0x2b, 0x15, 0xea, 0x9d, 0x4e, 0xaf, 0xc6, 0x07, 0xb0, 0xe7, 0xda, 0x9a, 0xa5, 0x2b, 0x63, 0xfb, 0xad, - 0xa2, 0xe9, 0x1b, 0x31, 0x32, 0x59, 0xa3, 0xec, 0x66, 0xee, 0x51, 0x3b, 0x1e, 0xda, 0xae, 0xd4, 0x55, 0x82, 0x61, - 0x51, 0x17, 0x0c, 0x4d, 0xa8, 0xe7, 0xba, 0x8b, 0xad, 0x99, 0x8a, 0x85, 0x6a, 0xad, 0x95, 0x03, 0xc1, 0xc3, 0x43, - 0x30, 0x4e, 0xd6, 0xfa, 0x07, 0xaf, 0xa3, 0x19, 0x43, 0x8a, 0x7a, 0x57, 0x35, 0x90, 0x0e, 0x04, 0x34, 0x19, 0x36, - 0xd5, 0x1b, 0x77, 0x85, 0xd5, 0x54, 0xdf, 0x5f, 0x31, 0x58, 0x11, 0x60, 0x5f, 0x97, 0x6b, 0x96, 0x88, 0xf4, 0xa6, - 0xe0, 0x12, 0x4d, 0x1f, 0x51, 0x26, 0xd6, 0x84, 0x14, 0x3c, 0x20, 0x0f, 0xcb, 0xdf, 0x58, 0x38, 0xd9, 0x8a, 0x29, - 0x1c, 0x39, 0xca, 0x14, 0xa0, 0x33, 0x29, 0x01, 0x10, 0x97, 0xf4, 0xb3, 0xb6, 0xb1, 0x90, 0x6c, 0xfb, 0xc8, 0x07, - 0xfe, 0x24, 0x89, 0xb8, 0xd3, 0xd9, 0x6a, 0xbb, 0xc0, 0x87, 0x20, 0xc4, 0x41, 0x47, 0x80, 0x79, 0x5f, 0xa1, 0xc2, - 0x10, 0x95, 0xd8, 0xe5, 0x3e, 0x18, 0x45, 0xd3, 0x78, 0xc2, 0x9d, 0x0c, 0x95, 0x88, 0x5b, 0xb2, 0x04, 0x94, 0x8c, - 0xde, 0x57, 0x20, 0x25, 0xb8, 0x90, 0x2e, 0xa2, 0x5a, 0x0b, 0x34, 0x05, 0x29, 0x49, 0x29, 0xd2, 0x82, 0x0a, 0x02, - 0x43, 0xa8, 0xf4, 0x14, 0x47, 0x81, 0x7e, 0x8b, 0x07, 0x62, 0xd0, 0x60, 0xc5, 0xa2, 0x8c, 0x07, 0xf1, 0x6a, 0x21, - 0xa8, 0x61, 0x9f, 0x67, 0x2f, 0xb3, 0x33, 0x96, 0x3f, 0x89, 0x10, 0xf6, 0x40, 0x74, 0x2f, 0x41, 0xd2, 0x93, 0x40, - 0x67, 0x3d, 0xc5, 0x2b, 0xa7, 0x84, 0x34, 0x2c, 0xc4, 0x2c, 0x46, 0x45, 0x08, 0x5a, 0x8e, 0x68, 0x9f, 0xe2, 0x96, - 0xa2, 0xbd, 0x87, 0xaa, 0x84, 0x69, 0xde, 0xda, 0x7b, 0x59, 0xe7, 0x2d, 0x18, 0x61, 0xae, 0xb8, 0xb5, 0xbe, 0x63, - 0x5d, 0x4f, 0xea, 0x66, 0x47, 0xf2, 0x96, 0xa1, 0xcc, 0x40, 0x7f, 0x5c, 0x5e, 0x56, 0x46, 0x3a, 0x28, 0x53, 0x2d, - 0xcd, 0xd1, 0x72, 0x12, 0x5b, 0xc2, 0x2d, 0x41, 0x19, 0xa1, 0xe1, 0x95, 0x67, 0x49, 0x62, 0xe8, 0x22, 0x2f, 0xee, - 0x39, 0x0d, 0x75, 0x04, 0x50, 0xcc, 0x6a, 0x1a, 0x69, 0xc0, 0x03, 0x5d, 0x81, 0x4a, 0x49, 0x69, 0x23, 0xaf, 0x6a, - 0x22, 0x20, 0x4e, 0xc7, 0x2c, 0x17, 0x0e, 0x9a, 0xd4, 0xa1, 0x30, 0x61, 0x0a, 0x0c, 0xcd, 0xc6, 0x20, 0xe1, 0x15, - 0x02, 0x60, 0x9e, 0xf8, 0xd3, 0xac, 0xe0, 0xba, 0xce, 0x84, 0x3e, 0xbe, 0xbc, 0x8c, 0x85, 0xbf, 0x88, 0x0c, 0x90, - 0xb3, 0x59, 0x76, 0xca, 0xd6, 0x40, 0xdd, 0x53, 0x83, 0x99, 0x20, 0x1b, 0xc3, 0x80, 0x12, 0x05, 0xd5, 0x32, 0x4f, - 0xe2, 0x11, 0xd3, 0x5a, 0x6a, 0xe6, 0x83, 0x41, 0xc7, 0xce, 0x41, 0x46, 0x30, 0xb7, 0xdf, 0xef, 0xb7, 0xbd, 0x8e, - 0x5b, 0x0a, 0x82, 0x2f, 0x57, 0x28, 0x7a, 0x8d, 0x7e, 0x94, 0x26, 0xf8, 0x3a, 0x59, 0xc0, 0x5d, 0x43, 0x29, 0x72, - 0xe1, 0x27, 0x79, 0x52, 0x10, 0xbb, 0xde, 0x18, 0x06, 0xe5, 0x4c, 0x09, 0x6e, 0x34, 0x71, 0xc5, 0xb6, 0x7d, 0xa7, - 0xc9, 0xa6, 0xd9, 0x49, 0xed, 0x30, 0xb5, 0x30, 0x72, 0xcd, 0x0b, 0xed, 0x01, 0x9b, 0xcb, 0x83, 0x56, 0x22, 0x55, - 0x03, 0xaf, 0x03, 0x84, 0xc2, 0xd3, 0x75, 0x56, 0x50, 0xaa, 0x3a, 0x4b, 0x21, 0xae, 0x37, 0xd0, 0x5b, 0x26, 0xc1, - 0x5c, 0x47, 0x82, 0x7d, 0x29, 0x10, 0x38, 0x7a, 0x64, 0x62, 0xbd, 0x9e, 0xc0, 0xf2, 0x1c, 0x47, 0xa3, 0x4f, 0x1a, - 0xdc, 0x8a, 0xec, 0x4d, 0x36, 0x70, 0x1a, 0x25, 0xa1, 0x21, 0xae, 0x4c, 0xbc, 0x95, 0x84, 0xae, 0x6d, 0x14, 0x70, - 0xc8, 0x56, 0xd8, 0xbe, 0xb9, 0xd0, 0x4d, 0x6e, 0x97, 0xec, 0xa1, 0xfc, 0x27, 0xcd, 0x25, 0xd7, 0xb0, 0x1c, 0x57, - 0xd2, 0x80, 0x2b, 0xc6, 0x83, 0xa5, 0x69, 0x40, 0x02, 0x7c, 0x57, 0x8e, 0xe3, 0xe2, 0x6a, 0x12, 0xfc, 0xa1, 0x60, - 0x3e, 0x35, 0x66, 0xba, 0x11, 0x52, 0x2d, 0xe1, 0xa4, 0x19, 0xac, 0x41, 0x93, 0xc6, 0x83, 0x12, 0x35, 0xdf, 0xa2, - 0xa1, 0x42, 0x1c, 0x7f, 0x22, 0xaa, 0xd0, 0x04, 0x43, 0x30, 0x72, 0xaf, 0x90, 0x0c, 0x97, 0xad, 0x8a, 0x16, 0x29, - 0x53, 0x63, 0x52, 0xa9, 0x9a, 0xe5, 0x32, 0x30, 0xb0, 0x68, 0xb7, 0xfa, 0xd2, 0x12, 0x57, 0x22, 0x37, 0x0d, 0xb5, - 0x30, 0x29, 0x94, 0x37, 0xe1, 0xe4, 0xe8, 0x77, 0x29, 0xeb, 0xdd, 0xc4, 0x27, 0x57, 0xf8, 0xe4, 0xbe, 0xe1, 0x43, - 0x99, 0xbc, 0x5d, 0x0c, 0x8a, 0xe0, 0x9b, 0x5a, 0x25, 0xda, 0xa7, 0x3e, 0x0a, 0x66, 0x57, 0x0b, 0x5d, 0x10, 0x28, - 0x92, 0x4d, 0xd2, 0x81, 0xe4, 0x37, 0x14, 0x1b, 0x95, 0x67, 0x94, 0xb9, 0x62, 0x83, 0xd4, 0xbc, 0xd2, 0xcc, 0x4b, - 0xdd, 0x86, 0xfd, 0x5e, 0x96, 0x92, 0x4e, 0x5c, 0x50, 0x26, 0xf6, 0xae, 0xa3, 0x8d, 0x97, 0x86, 0x99, 0xb0, 0x7e, - 0x85, 0xb1, 0x53, 0xa3, 0x50, 0x2a, 0x45, 0x20, 0x8e, 0x8d, 0xaf, 0x95, 0x65, 0x90, 0xf9, 0x6b, 0xec, 0x29, 0x00, - 0x25, 0x81, 0xc5, 0xd7, 0x54, 0xf2, 0xa2, 0xb0, 0x4e, 0xc7, 0x3b, 0x44, 0xc7, 0x4a, 0x84, 0xd6, 0x44, 0xbe, 0xd6, - 0x67, 0xb1, 0x5f, 0x73, 0x09, 0x4d, 0x4a, 0xe6, 0x83, 0x3c, 0xb0, 0x55, 0x20, 0xa2, 0xd2, 0x6d, 0xc9, 0x20, 0x21, - 0x87, 0x74, 0x95, 0xe8, 0xb5, 0x91, 0x0c, 0x5a, 0xa7, 0x42, 0xa2, 0xa5, 0xc3, 0x30, 0x72, 0xd0, 0x71, 0xa7, 0xb5, - 0x58, 0x21, 0x64, 0xd3, 0xde, 0x24, 0x56, 0x44, 0xe7, 0x34, 0x47, 0x13, 0xce, 0xd4, 0xe9, 0x8e, 0x03, 0xe8, 0x80, - 0xd8, 0x5f, 0x61, 0xbd, 0xb5, 0x66, 0xa7, 0xeb, 0x57, 0x0e, 0xdf, 0xe5, 0x65, 0x82, 0xfc, 0x20, 0x0c, 0x5e, 0x58, - 0xb3, 0x81, 0x92, 0xbd, 0x7b, 0x2f, 0xb1, 0x15, 0xd9, 0x9f, 0x55, 0x49, 0xe5, 0x29, 0xd4, 0x38, 0xb7, 0xbe, 0x4e, - 0xcc, 0x0c, 0x2d, 0xaa, 0x8a, 0x7d, 0x43, 0xaa, 0xef, 0x2b, 0x85, 0x5d, 0xa1, 0xbc, 0x2f, 0x87, 0x8e, 0x5d, 0xd7, - 0x0d, 0x72, 0x72, 0x5e, 0xee, 0xac, 0x73, 0x21, 0xef, 0xde, 0x35, 0x7d, 0xa6, 0x53, 0x3d, 0xfc, 0x13, 0x07, 0x95, - 0x73, 0x71, 0x91, 0x92, 0x05, 0xf3, 0x44, 0xa9, 0xa3, 0x15, 0x07, 0xb4, 0xdd, 0x43, 0x4f, 0x3b, 0x3a, 0x8b, 0x62, - 0x6e, 0xe9, 0x51, 0x84, 0xa7, 0x8d, 0xf2, 0x49, 0x1a, 0x1d, 0x80, 0x17, 0x9a, 0x90, 0xe4, 0x84, 0x9b, 0xb6, 0x68, - 0x31, 0x9a, 0x32, 0x0c, 0x81, 0x2b, 0x7b, 0xc2, 0x94, 0x3d, 0x77, 0x10, 0x6f, 0x31, 0x30, 0x5b, 0x0f, 0x7b, 0xd9, - 0xec, 0x5e, 0x33, 0xff, 0x61, 0x8d, 0x40, 0xb6, 0xcd, 0x54, 0x5d, 0xd9, 0x78, 0x97, 0x22, 0x12, 0x23, 0x6c, 0xeb, - 0xc6, 0x96, 0xb6, 0x7e, 0xaf, 0xe1, 0x5e, 0x57, 0x8e, 0x79, 0x4d, 0xa9, 0x36, 0xf4, 0xb0, 0x72, 0x73, 0x98, 0xe9, - 0xc8, 0x8b, 0x15, 0x74, 0x7b, 0x22, 0x28, 0x04, 0x4e, 0x84, 0xb6, 0x07, 0x15, 0x37, 0x10, 0x29, 0xb9, 0xd2, 0xaa, - 0xd9, 0x22, 0x19, 0x4b, 0x60, 0xc1, 0x85, 0xe5, 0x92, 0x8f, 0xce, 0xe2, 0x24, 0xa9, 0x4a, 0xff, 0x50, 0x01, 0x2f, - 0x86, 0xbd, 0x49, 0xb4, 0x0b, 0x8c, 0x16, 0x0a, 0x04, 0x57, 0x1b, 0x61, 0xef, 0x1d, 0xb7, 0x5a, 0x77, 0x11, 0x71, - 0xe4, 0x66, 0x34, 0x02, 0xea, 0x31, 0xc2, 0xaa, 0x59, 0x7b, 0xef, 0x19, 0x86, 0xd4, 0x0c, 0x7c, 0x50, 0x9d, 0x51, - 0xf1, 0x67, 0xd9, 0x53, 0x9f, 0x89, 0xde, 0x8d, 0xaa, 0xab, 0x19, 0x50, 0x51, 0x81, 0x0f, 0x33, 0xc4, 0xd2, 0x56, - 0x81, 0x80, 0x5c, 0x0f, 0x8b, 0x52, 0xc0, 0x24, 0x0d, 0x16, 0x94, 0x02, 0x6b, 0xad, 0xec, 0x5e, 0xde, 0x14, 0xcc, - 0xa1, 0x50, 0xb8, 0xe8, 0xff, 0x24, 0x9b, 0xcd, 0xd1, 0x32, 0x6b, 0x30, 0x35, 0x34, 0x78, 0xdf, 0xa8, 0x2f, 0xd7, - 0x94, 0xd5, 0xfa, 0xd0, 0x8e, 0xac, 0xf1, 0x93, 0x76, 0x94, 0xc1, 0xa1, 0x5a, 0xe8, 0xa2, 0xba, 0xdd, 0xdc, 0x14, - 0x31, 0xeb, 0x78, 0xdc, 0x27, 0xbd, 0xad, 0xad, 0x49, 0x4f, 0xd3, 0x80, 0x64, 0x92, 0x64, 0x78, 0x93, 0x01, 0xca, - 0x8a, 0x38, 0xcb, 0xb2, 0x41, 0xbe, 0x65, 0x59, 0xe2, 0xfa, 0x7d, 0xd7, 0xdb, 0xab, 0x79, 0xd6, 0xde, 0xde, 0xd5, - 0x2e, 0x72, 0x55, 0x27, 0x3d, 0xc8, 0xc3, 0x21, 0x14, 0xad, 0xd8, 0x94, 0xe1, 0x72, 0x96, 0x8d, 0x59, 0x60, 0x43, - 0xf7, 0xd4, 0x2e, 0x95, 0x56, 0x86, 0xcd, 0x91, 0x32, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4, - 0xb9, 0xf0, 0x7d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62, - 0x35, 0x94, 0xd4, 0xf7, 0x83, 0xef, 0x83, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11, - 0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x8b, 0x58, 0xf8, 0x72, 0xf0, 0x52, - 0xb9, 0xd4, 0x19, 0xb8, 0x10, 0xe0, 0xe4, 0x27, 0x31, 0x6f, 0xe1, 0x79, 0xa4, 0xed, 0x2d, 0x45, 0x05, 0xc6, 0x15, - 0x29, 0x2e, 0x5d, 0x2a, 0x6f, 0xd0, 0xfb, 0x18, 0x1e, 0x41, 0xb3, 0x8d, 0x8d, 0xa5, 0xf3, 0x2a, 0xe2, 0x53, 0x3f, - 0x8f, 0xd2, 0x71, 0x36, 0x73, 0xdc, 0x4d, 0xdb, 0x76, 0xfd, 0x82, 0x3c, 0x91, 0xaf, 0xdc, 0x72, 0xe3, 0xc8, 0x9b, - 0xb2, 0xd0, 0x1e, 0xd8, 0x9b, 0x1f, 0xbd, 0xf7, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x4e, 0x59, 0xd9, 0x3f, 0xf2, 0xce, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, + 0x9c, 0xd3, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x57, 0x20, 0x44, 0x52, 0x96, 0xed, 0x02, 0x05, 0xf2, 0xca, + 0x4b, 0x5d, 0xbb, 0xca, 0x5b, 0x59, 0xb2, 0x6b, 0x51, 0xb1, 0x2c, 0x88, 0x4c, 0x8a, 0x28, 0x83, 0x00, 0x0b, 0x48, + 0x6a, 0x29, 0x0a, 0x7d, 0xfa, 0xa9, 0x9f, 0xe6, 0x9c, 0x59, 0x1f, 0xfa, 0x65, 0x4e, 0xf7, 0xc3, 0x7c, 0xc4, 0x3c, + 0xf7, 0xa7, 0xdc, 0x1f, 0x98, 0xfe, 0x84, 0x89, 0x88, 0x5c, 0x90, 0x00, 0xa9, 0xc5, 0xd5, 0xd5, 0x7d, 0xbc, 0x08, + 0xc8, 0x35, 0x22, 0x32, 0x32, 0xb6, 0x8c, 0x84, 0x76, 0xef, 0x8c, 0xb3, 0x11, 0xbf, 0x98, 0x33, 0x6b, 0xca, 0x67, + 0x49, 0x7f, 0x57, 0xfe, 0xcf, 0xa2, 0x71, 0x7f, 0x37, 0x89, 0xd3, 0x4f, 0x56, 0xce, 0x92, 0x30, 0x1e, 0x65, 0xa9, + 0x35, 0xcd, 0xd9, 0x24, 0x1c, 0x47, 0x3c, 0x0a, 0xe2, 0x59, 0x74, 0xc2, 0xac, 0xad, 0xfe, 0xee, 0x8c, 0xf1, 0xc8, + 0x1a, 0x4d, 0xa3, 0xbc, 0x60, 0x3c, 0x7c, 0x7f, 0xf0, 0x55, 0xeb, 0x51, 0x7f, 0xb7, 0x18, 0xe5, 0xf1, 0x9c, 0x5b, + 0x38, 0x64, 0x38, 0xcb, 0xc6, 0x8b, 0x84, 0xf5, 0x4f, 0xa3, 0xdc, 0x7a, 0xc6, 0xc2, 0x37, 0xc7, 0xbf, 0xb0, 0x11, + 0xf7, 0xc7, 0x6c, 0x12, 0xa7, 0xec, 0x6d, 0x9e, 0xcd, 0x59, 0xce, 0x2f, 0xbc, 0xfd, 0xf5, 0x15, 0x31, 0x2b, 0xbc, + 0x4f, 0xba, 0xea, 0x84, 0xf1, 0x37, 0x67, 0xa9, 0xea, 0xf3, 0x94, 0x89, 0x49, 0xb2, 0xbc, 0xf0, 0x8a, 0x2b, 0xda, + 0xec, 0x5f, 0xcc, 0x8e, 0xb3, 0xa4, 0xf0, 0x9e, 0xe8, 0xfa, 0x79, 0x9e, 0xf1, 0x0c, 0xc1, 0xf2, 0xa7, 0x51, 0x61, + 0xb4, 0xf4, 0xde, 0xac, 0x69, 0x32, 0x97, 0x95, 0x2f, 0x8a, 0x67, 0xe9, 0x62, 0xc6, 0xf2, 0xe8, 0x38, 0x61, 0x5e, + 0xcc, 0x42, 0x87, 0x79, 0xdc, 0x8b, 0xdd, 0xb0, 0xcf, 0xad, 0x38, 0xb5, 0xd8, 0xe0, 0x19, 0xa3, 0x92, 0x25, 0xd3, + 0xad, 0x82, 0x3b, 0x6d, 0x0f, 0xc8, 0x35, 0x89, 0x4f, 0x16, 0xfa, 0xfd, 0x2c, 0x8f, 0xb9, 0x7a, 0x3e, 0x8d, 0x92, + 0x05, 0x0b, 0xe2, 0xd2, 0x0d, 0xd8, 0x21, 0x1f, 0x86, 0xb1, 0xf7, 0x84, 0x06, 0x85, 0x21, 0x97, 0x93, 0x2c, 0x77, + 0x90, 0x56, 0x31, 0x8e, 0xcd, 0x2f, 0x2f, 0x1d, 0x1e, 0x2e, 0x4b, 0xd7, 0x7d, 0xc2, 0xfc, 0x51, 0x94, 0x24, 0x0e, + 0x4e, 0x7c, 0xf7, 0x6e, 0x8c, 0x33, 0xc6, 0x1e, 0x3f, 0x8c, 0x87, 0x6e, 0x2f, 0x9e, 0x38, 0x05, 0x73, 0xab, 0x7e, + 0xd9, 0xc4, 0x2a, 0x98, 0xc3, 0x5d, 0xf7, 0xcd, 0xd5, 0x7d, 0x72, 0xc6, 0x17, 0x39, 0xc0, 0x5e, 0x7a, 0x6f, 0xd4, + 0xcc, 0xfb, 0x58, 0xff, 0x89, 0x3a, 0xf6, 0x00, 0xf6, 0x82, 0x5b, 0x1f, 0xc2, 0xb3, 0x38, 0x1d, 0x67, 0x67, 0xfe, + 0xfe, 0x34, 0x82, 0x1f, 0xef, 0xb2, 0x8c, 0xdf, 0xbd, 0xeb, 0x9c, 0x66, 0xf1, 0xd8, 0x6a, 0x87, 0xa1, 0x59, 0x79, + 0xf1, 0x64, 0x7f, 0xff, 0xf2, 0xb2, 0x51, 0xe0, 0xa7, 0x11, 0x8f, 0x4f, 0x99, 0xe8, 0x0c, 0x00, 0xd8, 0xf0, 0x73, + 0xce, 0xd9, 0x78, 0x9f, 0x5f, 0x24, 0x50, 0xca, 0x18, 0x2f, 0x6c, 0xc0, 0xf1, 0x69, 0x36, 0x02, 0xb2, 0xa5, 0x06, + 0xe1, 0xa1, 0x69, 0xce, 0xe6, 0x49, 0x34, 0x62, 0x58, 0x0f, 0x23, 0x55, 0x3d, 0xaa, 0x46, 0xde, 0x57, 0xa1, 0x58, + 0x5e, 0xc7, 0xf5, 0x72, 0x16, 0xa6, 0xec, 0xcc, 0x7a, 0x15, 0xcd, 0x7b, 0xa3, 0x24, 0x2a, 0x0a, 0x2b, 0x63, 0x4b, + 0x42, 0x21, 0x5f, 0x8c, 0x80, 0x41, 0x08, 0xc1, 0x25, 0x90, 0x89, 0x4f, 0xe3, 0xc2, 0xff, 0xb8, 0x31, 0x2a, 0x8a, + 0x77, 0xac, 0x58, 0x24, 0x7c, 0x23, 0x84, 0xb5, 0xe0, 0x77, 0xc2, 0xf0, 0x2b, 0x97, 0x4f, 0xf3, 0xec, 0xcc, 0x7a, + 0x96, 0xe7, 0xd0, 0xdc, 0x86, 0x29, 0x45, 0x03, 0x2b, 0x2e, 0xac, 0x34, 0xe3, 0x96, 0x1e, 0x0c, 0x17, 0xd0, 0xb7, + 0xde, 0x17, 0xcc, 0x3a, 0x5a, 0xa4, 0x45, 0x34, 0x61, 0xd0, 0xf4, 0xc8, 0xca, 0x72, 0xeb, 0x08, 0x06, 0x3d, 0x82, + 0x25, 0x2b, 0x38, 0xec, 0x1a, 0xdf, 0x76, 0x7b, 0x34, 0x17, 0x14, 0x1e, 0xb0, 0x73, 0x1e, 0xb2, 0x12, 0x18, 0xd3, + 0x2a, 0x34, 0x1a, 0x8e, 0xbb, 0x4c, 0xa0, 0x80, 0x85, 0x39, 0x43, 0x96, 0x75, 0xcc, 0xc6, 0x7a, 0x71, 0x3e, 0xdc, + 0xbd, 0xab, 0x69, 0x0d, 0x34, 0x71, 0xa0, 0x6d, 0xd1, 0x68, 0xeb, 0x09, 0xc4, 0x6b, 0x24, 0x72, 0x3d, 0xe6, 0x4b, + 0xf2, 0xed, 0x5f, 0xa4, 0xa3, 0xfa, 0xd8, 0x50, 0x59, 0xf2, 0x6c, 0x9f, 0xe7, 0x71, 0x7a, 0x02, 0x40, 0xc8, 0x99, + 0xcc, 0x26, 0x65, 0x29, 0x16, 0xff, 0x2d, 0x0b, 0x59, 0xd8, 0xc7, 0xd1, 0x33, 0xe6, 0xd8, 0x05, 0xf5, 0xb0, 0xc3, + 0x10, 0x49, 0x0f, 0x0c, 0xc6, 0x06, 0x2c, 0x60, 0x9b, 0xb6, 0xed, 0x7d, 0xe5, 0x7a, 0x17, 0xc8, 0x41, 0xbe, 0xef, + 0x13, 0xfb, 0x8a, 0xce, 0x71, 0xd8, 0x41, 0xa0, 0xfd, 0x84, 0xa5, 0x27, 0x7c, 0x3a, 0x60, 0x87, 0xed, 0x61, 0xc0, + 0x01, 0xaa, 0xf1, 0x62, 0xc4, 0x1c, 0xe4, 0x47, 0x2f, 0xc7, 0xed, 0xb3, 0xe9, 0xc0, 0x14, 0xb8, 0x30, 0x77, 0x08, + 0xc7, 0xda, 0xd2, 0xb8, 0x8a, 0x45, 0x15, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x63, 0x96, 0x1b, 0x70, 0xe8, 0x66, + 0xbd, 0xda, 0x0a, 0x2e, 0x60, 0x85, 0xa0, 0x9f, 0x35, 0x59, 0xa4, 0x23, 0x1e, 0x83, 0xe0, 0xb2, 0x37, 0x01, 0x5c, + 0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0x64, 0x87, 0xf9, 0x66, 0x67, 0xe8, 0x21, 0x94, 0x9a, + 0xf8, 0x12, 0xf1, 0x18, 0x10, 0x2c, 0xbd, 0xf7, 0x4c, 0x6f, 0xcf, 0x0f, 0x03, 0xe6, 0xaf, 0xf2, 0x71, 0xc8, 0xfd, + 0x59, 0x34, 0x47, 0x6c, 0x18, 0xf1, 0x40, 0x94, 0x8e, 0x10, 0xba, 0xda, 0xba, 0x20, 0xc5, 0xfc, 0x8a, 0x05, 0x5c, + 0x20, 0x08, 0xec, 0xd9, 0x67, 0xd1, 0x68, 0x0a, 0x5b, 0xbc, 0x22, 0xdc, 0x58, 0x6d, 0x87, 0x51, 0xce, 0x22, 0xce, + 0x9e, 0x25, 0x0c, 0xdf, 0x70, 0x05, 0xa0, 0xa7, 0x0d, 0xbc, 0xae, 0xf6, 0x5d, 0x12, 0xf3, 0xd7, 0x19, 0xcc, 0xd3, + 0x13, 0x4c, 0x02, 0x5c, 0x9c, 0xc3, 0x26, 0x47, 0x16, 0xd9, 0xe3, 0xb0, 0x5a, 0xc7, 0x0b, 0x0e, 0xeb, 0x96, 0x62, + 0x0b, 0x1b, 0xa8, 0xed, 0xc5, 0x3e, 0x07, 0x22, 0x3e, 0xc9, 0x52, 0x0e, 0xc3, 0x01, 0xbc, 0x9a, 0x83, 0xfc, 0x68, + 0x3e, 0x67, 0xe9, 0xf8, 0xc9, 0x34, 0x4e, 0xc6, 0x40, 0x8d, 0x12, 0xf0, 0x4d, 0x59, 0x08, 0x78, 0x02, 0x32, 0xc1, + 0xf5, 0x18, 0xd1, 0xf2, 0x21, 0x23, 0xf3, 0xd0, 0xb6, 0x7b, 0x28, 0x81, 0x24, 0x16, 0x28, 0x83, 0x68, 0xe1, 0xde, + 0x81, 0xe8, 0x2f, 0x5c, 0xbe, 0x19, 0xc6, 0x7a, 0x19, 0x25, 0x81, 0xdf, 0xa2, 0xa4, 0x01, 0xfa, 0x33, 0x90, 0x81, + 0x3d, 0x14, 0x5c, 0xdf, 0x49, 0xa9, 0x93, 0x30, 0x85, 0x21, 0x10, 0x60, 0x84, 0x12, 0x44, 0xd2, 0xe0, 0x6d, 0x96, + 0x5c, 0x4c, 0xe2, 0x24, 0xd9, 0x5f, 0xcc, 0xe7, 0x59, 0xce, 0xbd, 0xaf, 0xc3, 0x25, 0xcf, 0x2a, 0x5c, 0x69, 0x93, + 0x17, 0x67, 0x31, 0x47, 0x82, 0xba, 0xcb, 0x51, 0x04, 0x4b, 0xfd, 0x38, 0xcb, 0x12, 0x16, 0xa5, 0x80, 0x06, 0x1b, + 0xd8, 0x76, 0x90, 0x2e, 0x92, 0xa4, 0x77, 0x0c, 0xc3, 0x7e, 0xea, 0x51, 0xb5, 0x90, 0xf8, 0x01, 0x3d, 0xef, 0xe5, + 0x79, 0x74, 0x01, 0x0d, 0xb1, 0x0d, 0xf0, 0x22, 0xac, 0xd6, 0xd7, 0xfb, 0x6f, 0x5e, 0xfb, 0x82, 0xf1, 0xe3, 0xc9, + 0x05, 0x00, 0x5a, 0x56, 0x52, 0x73, 0x92, 0x67, 0xb3, 0xc6, 0xd4, 0x48, 0x87, 0x38, 0x64, 0xbd, 0x2b, 0x40, 0x88, + 0x69, 0x64, 0x58, 0x25, 0x66, 0x42, 0xf0, 0x9a, 0xf8, 0x59, 0x56, 0xe2, 0x1e, 0x18, 0xe0, 0x43, 0x20, 0x8a, 0x61, + 0xca, 0xeb, 0xa1, 0xe5, 0xf9, 0xc5, 0x32, 0x0e, 0x09, 0xce, 0x39, 0xea, 0x5f, 0x84, 0x71, 0x14, 0xc1, 0xec, 0x4b, + 0x31, 0x60, 0xa9, 0x20, 0x8e, 0xcb, 0xd2, 0x8b, 0x34, 0x13, 0xa3, 0xc4, 0x43, 0x81, 0xc2, 0x61, 0x1b, 0x5d, 0x5e, + 0x32, 0x78, 0x71, 0xbd, 0x6f, 0xc2, 0x65, 0xa4, 0xf0, 0x41, 0x0d, 0x85, 0xfb, 0x2b, 0x10, 0x72, 0x02, 0x35, 0xd9, + 0x29, 0xe8, 0x41, 0x80, 0xf3, 0x6b, 0x10, 0xb5, 0x93, 0x04, 0xa1, 0xb8, 0xd3, 0xf1, 0x40, 0x83, 0x3e, 0x99, 0x46, + 0xe9, 0x09, 0x1b, 0x07, 0x11, 0x2b, 0xa5, 0xe4, 0xdd, 0xb3, 0x60, 0x8d, 0x81, 0x9d, 0x0a, 0xeb, 0xf9, 0xc1, 0xab, + 0x97, 0x72, 0xe5, 0x6a, 0xc2, 0x18, 0x16, 0x69, 0x01, 0x6a, 0x15, 0xc4, 0xb6, 0x14, 0xc7, 0xcf, 0xb8, 0x92, 0xde, + 0xa2, 0x24, 0x2e, 0xde, 0xcf, 0xc1, 0xc4, 0x60, 0x6f, 0x61, 0x18, 0x98, 0x3e, 0x84, 0xa9, 0xa8, 0x1c, 0xe6, 0x13, + 0x15, 0x63, 0x5d, 0x04, 0x9d, 0x05, 0xa6, 0xe2, 0x35, 0x73, 0xdc, 0x12, 0x58, 0x95, 0xc7, 0x23, 0x2b, 0x1a, 0x8f, + 0x5f, 0xa4, 0x31, 0x8f, 0xa3, 0x24, 0xfe, 0x8d, 0x28, 0xb9, 0x44, 0x1e, 0xe3, 0x3d, 0xb9, 0x08, 0x80, 0x3b, 0xf5, + 0x48, 0x5c, 0x25, 0x64, 0xef, 0x10, 0x31, 0x84, 0xb4, 0x4c, 0xc2, 0xc3, 0xa1, 0x04, 0x2f, 0xf1, 0xe7, 0x8b, 0x62, + 0x8a, 0x84, 0x95, 0x03, 0xa3, 0x20, 0xcf, 0x8e, 0x0b, 0x96, 0x9f, 0xb2, 0xb1, 0xe6, 0x80, 0x02, 0xb0, 0xa2, 0xe6, + 0x60, 0xbc, 0xd0, 0x8c, 0x8e, 0xd2, 0xa1, 0x0c, 0x86, 0xea, 0x99, 0x62, 0x96, 0x49, 0x66, 0xd6, 0x16, 0x8e, 0x96, + 0x02, 0x8e, 0x30, 0x2a, 0xa4, 0x24, 0xc8, 0x43, 0x85, 0xe1, 0x14, 0xa4, 0x10, 0x68, 0x05, 0x73, 0x9b, 0x2b, 0x4d, + 0xf6, 0x6c, 0x41, 0x2a, 0x21, 0x87, 0x8e, 0xb0, 0x91, 0x09, 0xd2, 0xdc, 0x85, 0x5d, 0x05, 0x52, 0x5e, 0x82, 0x2b, + 0xa4, 0x88, 0x32, 0x73, 0x90, 0x01, 0xc2, 0x6f, 0x84, 0x2e, 0xf4, 0xb1, 0x05, 0xb1, 0x81, 0xaf, 0x57, 0x1e, 0x08, + 0x2b, 0xf1, 0xae, 0x10, 0xf1, 0xae, 0x00, 0x1b, 0x27, 0x46, 0x7e, 0xf2, 0xee, 0x70, 0x3f, 0xcd, 0xf6, 0x46, 0x23, + 0x56, 0x14, 0x19, 0xc0, 0x76, 0x87, 0xda, 0x5f, 0x65, 0x68, 0x01, 0x25, 0x5d, 0x2d, 0xeb, 0xec, 0x82, 0x34, 0xb8, + 0xa9, 0x56, 0x94, 0x4e, 0x0f, 0xec, 0x8f, 0x1f, 0x41, 0x66, 0x7b, 0x92, 0x0c, 0x40, 0xf5, 0x55, 0xc3, 0x4f, 0xd8, + 0x33, 0x75, 0xca, 0xac, 0xb5, 0x2f, 0x9d, 0x3a, 0x48, 0x1e, 0x0c, 0xeb, 0x96, 0xc6, 0x82, 0xae, 0x1d, 0x1a, 0x57, + 0x43, 0x2a, 0xc8, 0xe5, 0x09, 0xa9, 0x6c, 0x63, 0x19, 0xc1, 0x6a, 0x2b, 0x3d, 0x22, 0xbd, 0xc2, 0xa6, 0x20, 0x40, + 0x0f, 0xd9, 0xb0, 0x27, 0xeb, 0xc3, 0x5c, 0x50, 0x2e, 0x67, 0xbf, 0x2e, 0x58, 0xc1, 0x05, 0xeb, 0xc2, 0xb8, 0x05, + 0x8c, 0x5b, 0xae, 0x58, 0x87, 0x35, 0xdb, 0x71, 0x1d, 0x6c, 0x6f, 0xe6, 0xa8, 0xc7, 0x0a, 0xe4, 0xe4, 0xeb, 0xd9, + 0x09, 0x61, 0x65, 0xee, 0xe5, 0xe5, 0x37, 0x6a, 0x90, 0x6a, 0x29, 0xb5, 0x0d, 0xd4, 0x58, 0x13, 0x5b, 0x35, 0x19, + 0xdb, 0xae, 0x54, 0xa8, 0x77, 0x3a, 0xbd, 0x1a, 0x1f, 0xc0, 0x9e, 0x6b, 0x6b, 0x96, 0xae, 0x8c, 0xed, 0xb7, 0x8a, + 0xa6, 0x6f, 0xc4, 0xc8, 0x64, 0x8d, 0xb2, 0x9b, 0xb9, 0x47, 0xed, 0x78, 0x68, 0xbb, 0x52, 0x57, 0x09, 0x86, 0x45, + 0x5d, 0x30, 0x34, 0xa1, 0x9e, 0xeb, 0x2e, 0xb6, 0x66, 0x2a, 0x16, 0xaa, 0xb5, 0x56, 0x0e, 0x04, 0x0f, 0x0f, 0xc1, + 0x38, 0x59, 0xeb, 0x1f, 0xbc, 0x8e, 0x66, 0x0c, 0x29, 0xea, 0x5d, 0xd5, 0x40, 0x3a, 0x10, 0xd0, 0x64, 0xd8, 0x54, + 0x6f, 0xdc, 0x15, 0x56, 0x53, 0x7d, 0x7f, 0xc5, 0x60, 0x45, 0x80, 0x7d, 0x5d, 0xae, 0x59, 0x22, 0xd2, 0x9b, 0x82, + 0x4b, 0x34, 0x7d, 0x44, 0x99, 0x58, 0x13, 0x52, 0xf0, 0x80, 0x3c, 0x2c, 0x7f, 0x63, 0xe1, 0x64, 0x2b, 0xa6, 0x70, + 0xe4, 0x28, 0x53, 0x80, 0xce, 0xa4, 0x04, 0x40, 0x5c, 0xd2, 0xcf, 0xda, 0xc6, 0x42, 0xb2, 0xed, 0x23, 0x1f, 0xf8, + 0x93, 0x24, 0xe2, 0x4e, 0x67, 0xab, 0xed, 0x02, 0x1f, 0x82, 0x10, 0x07, 0x1d, 0x01, 0xe6, 0x7d, 0x85, 0x0a, 0x43, + 0x54, 0x62, 0x97, 0xfb, 0x60, 0x14, 0x4d, 0xe3, 0x09, 0x77, 0x52, 0x54, 0x22, 0x6e, 0xc9, 0x12, 0x50, 0x32, 0x7a, + 0x5f, 0x81, 0x94, 0xe0, 0x42, 0xba, 0x88, 0x6a, 0x2d, 0xd0, 0x14, 0xa4, 0x24, 0xa5, 0x48, 0x0b, 0x2a, 0x08, 0x0c, + 0xa1, 0xd2, 0x53, 0x1c, 0x05, 0xfa, 0x2d, 0x1e, 0x88, 0x41, 0x83, 0x15, 0x8b, 0x32, 0x1e, 0xc4, 0xab, 0x85, 0xa0, + 0x86, 0x7d, 0x9e, 0xbd, 0xcc, 0xce, 0x58, 0xfe, 0x24, 0x42, 0xd8, 0x03, 0xd1, 0xbd, 0x04, 0x49, 0x4f, 0x02, 0x9d, + 0xf5, 0x14, 0xaf, 0x9c, 0x12, 0xd2, 0xb0, 0x10, 0xb3, 0x18, 0x15, 0x21, 0x68, 0x39, 0xa2, 0x7d, 0x8a, 0x5b, 0x8a, + 0xf6, 0x1e, 0xaa, 0x12, 0xa6, 0x79, 0x6b, 0xef, 0x65, 0x9d, 0xb7, 0x60, 0x84, 0xb9, 0xe2, 0xd6, 0xfa, 0x8e, 0x75, + 0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x79, 0x59, 0x19, 0xe9, 0xa0, 0x4c, 0xb5, 0x34, + 0x47, 0xcb, 0x49, 0x6c, 0x09, 0xb7, 0x04, 0x65, 0x84, 0x86, 0x57, 0x9e, 0x25, 0x89, 0xa1, 0x8b, 0xbc, 0xb8, 0xe7, + 0x34, 0xd4, 0x11, 0x40, 0x31, 0xab, 0x69, 0xa4, 0x01, 0x0f, 0x74, 0x05, 0x2a, 0x25, 0xa5, 0x8d, 0xbc, 0xaa, 0x89, + 0x80, 0x38, 0x1d, 0xb3, 0x5c, 0x38, 0x68, 0x52, 0x87, 0xc2, 0x84, 0x29, 0x30, 0x34, 0x1b, 0x83, 0x84, 0x57, 0x08, + 0x80, 0x79, 0xe2, 0x4f, 0xb3, 0x82, 0xeb, 0x3a, 0x13, 0xfa, 0xf8, 0xf2, 0x32, 0x16, 0xfe, 0x22, 0x32, 0x40, 0xce, + 0x66, 0xd9, 0x29, 0x5b, 0x03, 0x75, 0x4f, 0x0d, 0x66, 0x82, 0x6c, 0x0c, 0x03, 0x4a, 0x14, 0x54, 0xcb, 0x3c, 0x89, + 0x47, 0x4c, 0x6b, 0xa9, 0x99, 0x0f, 0x06, 0x1d, 0x3b, 0x07, 0x19, 0xc1, 0xdc, 0x7e, 0xbf, 0xdf, 0xf6, 0x3a, 0x6e, + 0x29, 0x08, 0xbe, 0x5c, 0xa1, 0xe8, 0x35, 0xfa, 0x51, 0x9a, 0xe0, 0xeb, 0x64, 0x01, 0x77, 0x0d, 0xa5, 0xc8, 0x85, + 0x9f, 0xe4, 0x49, 0x41, 0xec, 0x7a, 0x63, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, 0xdb, 0xf6, 0x9d, 0x26, + 0x9b, 0x66, 0x27, 0xb5, 0xc3, 0xd4, 0xc2, 0xc8, 0x35, 0x2f, 0xb4, 0x07, 0x6c, 0x2e, 0x0f, 0x5a, 0x89, 0x54, 0x0d, + 0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x41, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, 0xef, 0x99, 0x04, 0x73, + 0x1d, 0x09, 0xf6, 0xa5, 0x40, 0xe0, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x02, 0xcb, 0x73, 0x1c, 0x8d, 0x3e, 0x69, 0x70, + 0x2b, 0xb2, 0x37, 0xd9, 0xc0, 0x69, 0x94, 0x84, 0x86, 0xb8, 0x32, 0xf1, 0x56, 0x12, 0xba, 0xb6, 0x51, 0xc0, 0x21, + 0x5b, 0x61, 0xfb, 0xe6, 0x42, 0x37, 0xb9, 0x5d, 0xb2, 0x87, 0xf2, 0x9f, 0x34, 0x97, 0x5c, 0xc3, 0x72, 0x5c, 0x49, + 0x03, 0xae, 0x18, 0x0f, 0x96, 0xa6, 0x01, 0x09, 0xf0, 0x5d, 0x39, 0x8e, 0x8b, 0xab, 0x49, 0xf0, 0x87, 0x82, 0xf9, + 0xd4, 0x98, 0xe9, 0x46, 0x48, 0xb5, 0x84, 0x93, 0x66, 0xb0, 0x06, 0x4d, 0x1a, 0x0f, 0x4a, 0xd4, 0x7c, 0x83, 0x86, + 0x0a, 0x71, 0xfc, 0x89, 0xa8, 0x42, 0x13, 0x0c, 0xc1, 0xc8, 0xbd, 0x42, 0x32, 0x5c, 0xb6, 0x2a, 0x5a, 0xa4, 0x4c, + 0x8d, 0x49, 0xa5, 0x6a, 0x96, 0xcb, 0xc0, 0xc0, 0xa2, 0xdd, 0xea, 0x4b, 0x4b, 0x5c, 0x89, 0xdc, 0x34, 0xd4, 0xc2, + 0xa4, 0x50, 0xde, 0x84, 0x93, 0xa3, 0xdf, 0xa5, 0xac, 0x77, 0x13, 0x9f, 0x5c, 0xe1, 0x93, 0xfb, 0x86, 0x0f, 0x65, + 0xf2, 0x76, 0x31, 0x28, 0x82, 0xaf, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, 0x74, 0x41, 0xa0, 0x48, + 0x36, 0x49, 0x07, 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xe6, 0x8a, 0x0d, 0x52, 0xf3, 0x4a, 0x33, 0x2f, 0x75, + 0x1b, 0xf6, 0x7b, 0x59, 0x4a, 0x3a, 0x71, 0x41, 0x99, 0xd8, 0xbb, 0x8e, 0x36, 0x5e, 0x1a, 0x66, 0xc2, 0xfa, 0x15, + 0xc6, 0x4e, 0x8d, 0x42, 0xa9, 0x14, 0x81, 0x38, 0x36, 0xbe, 0x56, 0x96, 0x41, 0xe6, 0xaf, 0xb1, 0xa7, 0x00, 0x94, + 0x04, 0x16, 0x5f, 0x53, 0xc9, 0x8b, 0xc2, 0x3a, 0x1d, 0xef, 0x10, 0x1d, 0x2b, 0x11, 0x5a, 0x13, 0xf9, 0x5a, 0x9f, + 0xc5, 0x7e, 0xcd, 0x25, 0x34, 0x29, 0x99, 0x0f, 0xf2, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, 0x25, 0x83, 0x84, 0x1c, + 0xd2, 0x55, 0xa2, 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x0a, 0x89, 0x96, 0x0e, 0xc3, 0xc8, 0x41, 0xc7, 0x9d, 0xd6, 0x62, + 0x85, 0x90, 0x4d, 0x7b, 0x93, 0x58, 0x11, 0x9d, 0xd3, 0x1c, 0x4d, 0x38, 0x53, 0xa7, 0x3b, 0x0e, 0xa0, 0x03, 0x62, + 0x7f, 0x85, 0xf5, 0xd6, 0x9a, 0x9d, 0xae, 0x5f, 0x39, 0x7c, 0x97, 0x97, 0x11, 0xf2, 0x83, 0x30, 0x78, 0x61, 0xcd, + 0x06, 0x4a, 0xf6, 0xee, 0xbd, 0xc4, 0x56, 0x64, 0x7f, 0x56, 0x25, 0x95, 0xa7, 0x50, 0xe3, 0xdc, 0xfa, 0x3a, 0x31, + 0x33, 0xb4, 0xa8, 0x2a, 0xf6, 0x0d, 0xa9, 0xbe, 0xaf, 0x14, 0x76, 0x85, 0xf2, 0xbe, 0x1c, 0x3a, 0x76, 0x5d, 0x37, + 0xc8, 0xc9, 0x79, 0xb9, 0xb3, 0xce, 0x85, 0xbc, 0x7b, 0xd7, 0xf4, 0x99, 0x4e, 0xf5, 0xf0, 0x4f, 0x1c, 0x54, 0xce, + 0xc5, 0x45, 0x4a, 0x16, 0xcc, 0x13, 0xa5, 0x8e, 0x56, 0x1c, 0xd0, 0x76, 0x0f, 0x3d, 0xed, 0xe8, 0x2c, 0x8a, 0xb9, + 0xa5, 0x47, 0x11, 0x9e, 0x36, 0xca, 0x27, 0x69, 0x74, 0x00, 0x5e, 0x68, 0x42, 0x92, 0x13, 0x6e, 0xda, 0xa2, 0xc5, + 0x68, 0xca, 0x30, 0x04, 0xae, 0xec, 0x09, 0x53, 0xf6, 0xdc, 0x41, 0xbc, 0xc5, 0xc0, 0x6c, 0x3d, 0xec, 0x65, 0xb3, + 0x7b, 0xcd, 0xfc, 0x87, 0x35, 0x02, 0xd9, 0x36, 0x53, 0x75, 0x65, 0xe3, 0x5d, 0x8a, 0x48, 0x8c, 0xb0, 0xad, 0x1b, + 0x5b, 0xda, 0xfa, 0xbd, 0x86, 0x7b, 0x5d, 0x39, 0xe6, 0x35, 0xa5, 0xda, 0xd0, 0xc3, 0xca, 0xcd, 0x61, 0xa6, 0x23, + 0x2f, 0x56, 0xd0, 0xed, 0x89, 0xa0, 0x10, 0x38, 0x11, 0xda, 0x1e, 0x54, 0xdc, 0x40, 0xa4, 0xe4, 0x4a, 0xab, 0x66, + 0x8b, 0x64, 0x2c, 0x81, 0x05, 0x17, 0x96, 0x4b, 0x3e, 0x3a, 0x8b, 0x93, 0xa4, 0x2a, 0xfd, 0x43, 0x05, 0xbc, 0x18, + 0xf6, 0x26, 0xd1, 0x2e, 0x30, 0x5a, 0x28, 0x10, 0x5c, 0x6d, 0x84, 0xbd, 0x77, 0xdc, 0x6a, 0xdd, 0x45, 0xc4, 0x91, + 0x9b, 0xd1, 0x08, 0xa8, 0xc7, 0x08, 0xab, 0x66, 0xed, 0xbd, 0x67, 0x18, 0x52, 0x33, 0xf0, 0x41, 0x75, 0x46, 0xc5, + 0x9f, 0x65, 0x4f, 0x7d, 0x26, 0x7a, 0x37, 0xaa, 0xae, 0x66, 0x40, 0x45, 0x05, 0x3e, 0xcc, 0x10, 0x4b, 0x5b, 0x05, + 0x02, 0x72, 0x3d, 0x2c, 0x4a, 0x01, 0x93, 0x34, 0x58, 0x50, 0x0a, 0xac, 0xb5, 0xb2, 0x7b, 0x79, 0x53, 0x30, 0x87, + 0x42, 0xe1, 0xa2, 0xff, 0x93, 0x6c, 0x36, 0x47, 0xcb, 0xac, 0xc1, 0xd4, 0xd0, 0xe0, 0x7d, 0xa3, 0xbe, 0x5c, 0x53, + 0x56, 0xeb, 0x43, 0x3b, 0xb2, 0xc6, 0x4f, 0xda, 0x51, 0x06, 0x87, 0x6a, 0xa1, 0x8b, 0xea, 0x76, 0x73, 0x53, 0xc4, + 0xac, 0xe3, 0x71, 0x9f, 0xf4, 0xb6, 0xb6, 0x26, 0x3d, 0x4d, 0x03, 0x92, 0x49, 0x92, 0xe1, 0x4d, 0x06, 0x28, 0x2b, + 0xe2, 0x2c, 0xcb, 0x06, 0xf9, 0x96, 0x65, 0x89, 0xeb, 0xf7, 0x6d, 0x6f, 0xaf, 0xe6, 0x59, 0x7b, 0x7b, 0x57, 0xbb, + 0xc8, 0x55, 0x9d, 0xf4, 0x20, 0x0f, 0x87, 0x50, 0xb4, 0x62, 0x53, 0x86, 0xcb, 0x59, 0x36, 0x66, 0x81, 0x0d, 0xdd, + 0x53, 0xbb, 0x94, 0x9b, 0x26, 0x81, 0xcd, 0x91, 0x30, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4, + 0xb9, 0xf0, 0x5d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62, + 0x35, 0x94, 0xd4, 0x77, 0x83, 0xef, 0x82, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11, + 0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x9b, 0xb2, 0xf0, 0xe5, 0xe0, 0xa5, + 0x72, 0xa9, 0x33, 0x70, 0x21, 0xc0, 0xc9, 0x4f, 0x62, 0xde, 0xc2, 0xf3, 0x48, 0xdb, 0x5b, 0x8a, 0x0a, 0x8c, 0x2b, + 0x52, 0x5c, 0xba, 0x54, 0xde, 0xa0, 0xf7, 0x31, 0x3c, 0x82, 0x66, 0x1b, 0x1b, 0x4b, 0xe7, 0x55, 0xc4, 0xa7, 0x7e, + 0x1e, 0xa5, 0xe3, 0x6c, 0xe6, 0xb8, 0x9b, 0xb6, 0xed, 0xfa, 0x05, 0x79, 0x22, 0x5f, 0xba, 0xe5, 0xc6, 0x91, 0x37, + 0x62, 0xa1, 0x3d, 0xb0, 0x37, 0x3f, 0x7a, 0x07, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x8e, 0x58, 0xd9, 0x3f, 0xf2, 0xce, 0x75, 0xcc, 0xdd, 0x7b, 0x8b, 0x52, 0x06, 0x7a, 0x85, 0xfd, 0x73, 0x09, 0x06, 0xb0, 0x1b, 0xc5, 0xdf, 0x41, 0xca, 0xbd, 0xa7, 0x03, 0x11, 0x19, 0xa7, 0xbd, 0xbc, 0xb4, 0x33, 0x8a, 0x18, 0xd8, 0x77, 0xb4, 0xb3, 0x7a, 0xf7, 0x6e, - 0xa5, 0xe6, 0xab, 0x52, 0x6f, 0xc4, 0xc2, 0x9a, 0xa7, 0xee, 0x1d, 0xd0, 0xd1, 0x4a, 0x7d, 0x23, 0x8f, 0x18, 0x29, - 0xcd, 0x55, 0x3b, 0xc1, 0x31, 0xb6, 0xf8, 0xfa, 0x6d, 0x7d, 0x28, 0xa2, 0x14, 0x7e, 0x0c, 0xd6, 0x4b, 0x04, 0xea, - 0x1b, 0x1c, 0x1c, 0xef, 0x20, 0xdc, 0xda, 0x75, 0x06, 0x81, 0x73, 0xa7, 0xd5, 0xba, 0xfc, 0x69, 0xeb, 0xf0, 0xe7, - 0xa8, 0xf5, 0xdb, 0x5e, 0xeb, 0xc7, 0xa1, 0x7b, 0xe9, 0xfc, 0xb4, 0x35, 0x38, 0x94, 0x6f, 0x87, 0x3f, 0xf7, 0x7f, - 0x2a, 0x86, 0x5f, 0x8a, 0xc2, 0x0d, 0xd7, 0xdd, 0x3a, 0x01, 0x4f, 0x29, 0xdc, 0x6a, 0xb5, 0xfa, 0xf0, 0xb4, 0x80, - 0x27, 0xfc, 0x79, 0x06, 0x3f, 0x2e, 0x0f, 0xad, 0xff, 0xf0, 0x53, 0xfa, 0x1f, 0x7f, 0xca, 0x87, 0x38, 0xe6, 0xe1, - 0xcf, 0x3f, 0x15, 0xf6, 0xbd, 0x7e, 0xb8, 0x35, 0xdc, 0x74, 0x1d, 0x5d, 0xf3, 0x65, 0x58, 0x3d, 0x42, 0xab, 0xc3, - 0x9f, 0xe5, 0x9b, 0x7d, 0xef, 0x68, 0xb7, 0x1f, 0x0e, 0x2f, 0x1d, 0xfb, 0xf2, 0x9e, 0x7b, 0xe9, 0xba, 0x97, 0x1b, - 0x38, 0xcf, 0x1c, 0x46, 0xbf, 0x07, 0x3f, 0x4f, 0xe0, 0xa7, 0x0d, 0x3f, 0x4f, 0xe1, 0xe7, 0xcf, 0xd0, 0x4d, 0xc4, - 0xdf, 0x2e, 0x29, 0x16, 0x72, 0x89, 0x07, 0x16, 0x11, 0xac, 0x82, 0xbb, 0xb1, 0x15, 0x7b, 0x13, 0x22, 0x1a, 0xec, - 0x43, 0xdf, 0xf7, 0x31, 0x4c, 0xea, 0x2c, 0x3f, 0x6e, 0xc0, 0xa2, 0x23, 0xe7, 0x6c, 0x04, 0xcc, 0x13, 0x91, 0x83, - 0x22, 0xe0, 0xe2, 0x6c, 0xb5, 0xc0, 0xc3, 0x55, 0x6f, 0x1c, 0x4e, 0x98, 0x03, 0x46, 0xc1, 0x73, 0x86, 0x0f, 0x5d, - 0xd7, 0x7b, 0x26, 0xcf, 0x0c, 0x71, 0x9f, 0x0b, 0xd6, 0x4a, 0x33, 0x61, 0xd2, 0xd8, 0xae, 0x37, 0x5f, 0x53, 0x09, - 0xdb, 0x3a, 0x3d, 0x81, 0xba, 0x0d, 0x71, 0xd0, 0xf6, 0x3d, 0x8b, 0x3e, 0xe1, 0x96, 0x7c, 0x6d, 0x1c, 0x02, 0xaf, - 0x58, 0xf2, 0x4d, 0xa3, 0xd1, 0xb0, 0x11, 0x85, 0x3b, 0xf6, 0x98, 0xc1, 0x0c, 0x2b, 0x26, 0x22, 0x27, 0xa5, 0x29, - 0x2c, 0x5b, 0x98, 0xfc, 0x6d, 0x94, 0xf3, 0x8d, 0xca, 0xb0, 0x0d, 0x6b, 0x96, 0x6c, 0xd3, 0xd2, 0xbf, 0xc5, 0x14, - 0x68, 0x5a, 0xd2, 0xf9, 0x87, 0x39, 0x7e, 0x98, 0x12, 0x5a, 0xaf, 0x1d, 0x0e, 0x1e, 0x7a, 0x01, 0x72, 0x47, 0xf4, - 0x73, 0xde, 0xa2, 0x1a, 0x83, 0xbf, 0x32, 0xcc, 0xe0, 0x89, 0xf9, 0x30, 0x44, 0xb3, 0x2c, 0x75, 0x70, 0x2b, 0x45, - 0x71, 0xff, 0x02, 0x77, 0x46, 0x5a, 0x7a, 0xfb, 0xa1, 0xda, 0x31, 0x07, 0x39, 0x63, 0xdf, 0x47, 0xc9, 0x27, 0x96, - 0x3b, 0xe7, 0x5e, 0xa7, 0xfb, 0x15, 0x75, 0xf6, 0xd0, 0x36, 0x7b, 0x55, 0x1d, 0xa3, 0x29, 0xb3, 0x40, 0x1d, 0x11, - 0xb6, 0x3a, 0x5e, 0x8e, 0x51, 0x2d, 0x24, 0x41, 0xe1, 0x65, 0x61, 0x97, 0x38, 0xdc, 0xde, 0x2d, 0x4e, 0x4f, 0xfa, - 0x76, 0x60, 0xdb, 0x60, 0xf1, 0x1f, 0x50, 0xd8, 0x4a, 0x18, 0x16, 0x60, 0x90, 0xed, 0xc6, 0x3d, 0xbe, 0xb9, 0x59, - 0x05, 0x9c, 0xf0, 0x20, 0x9d, 0xba, 0x27, 0x5e, 0xe4, 0x4d, 0x43, 0x18, 0x70, 0x04, 0xcd, 0xb0, 0x4b, 0x6f, 0xb4, - 0x1b, 0xcb, 0x69, 0x30, 0x16, 0xe2, 0x27, 0x51, 0xc1, 0x5f, 0x60, 0x3c, 0x22, 0x1c, 0xa1, 0xb1, 0xef, 0xb3, 0x73, - 0x36, 0x52, 0x76, 0x06, 0x10, 0x2a, 0x72, 0x7b, 0xee, 0x28, 0x34, 0x9a, 0xc1, 0xdc, 0x61, 0x78, 0x30, 0xb0, 0x61, - 0x2f, 0xc1, 0xae, 0x0c, 0xa3, 0xc3, 0xce, 0x70, 0x90, 0x86, 0x20, 0x6b, 0x35, 0x6d, 0x65, 0xd1, 0xa2, 0x56, 0xd4, - 0x1d, 0x0e, 0x9c, 0x53, 0x30, 0xd2, 0xc1, 0x16, 0x77, 0xf0, 0x0d, 0x23, 0x14, 0x45, 0xf8, 0x8e, 0x9d, 0x3c, 0x3b, - 0x9f, 0x3b, 0xf6, 0xee, 0x96, 0xbd, 0x89, 0xa5, 0x9e, 0x0d, 0xec, 0x05, 0x73, 0x87, 0x67, 0xae, 0xd9, 0x79, 0x7b, - 0x88, 0xa0, 0x62, 0x21, 0x4e, 0x7e, 0x36, 0xb0, 0xfb, 0x62, 0xea, 0x36, 0x0c, 0x9a, 0xca, 0xe5, 0xc7, 0x15, 0x3d, - 0x20, 0x54, 0x55, 0x57, 0x05, 0x1d, 0x94, 0x75, 0x03, 0x67, 0x6a, 0x22, 0xd1, 0xc2, 0xc9, 0x24, 0x15, 0xc0, 0xe1, - 0xc1, 0x66, 0x30, 0xa9, 0xd1, 0x6d, 0x7b, 0x38, 0x38, 0x0b, 0xee, 0xd9, 0xf7, 0xd4, 0xcb, 0x09, 0x0b, 0xc0, 0xbb, - 0xa0, 0xe9, 0x4f, 0x50, 0x8b, 0xc0, 0xcf, 0x39, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x2c, 0x8b, 0x16, 0x58, 0x74, 0x10, + 0xa5, 0xe6, 0xab, 0x52, 0xf0, 0x3e, 0xc2, 0x9a, 0xa7, 0xee, 0x3d, 0xa7, 0xa3, 0x95, 0xfa, 0x46, 0x1e, 0x33, 0x52, + 0x9a, 0xab, 0x76, 0x82, 0x63, 0x6c, 0xf1, 0xf5, 0xdb, 0xfa, 0x50, 0x44, 0x29, 0xfc, 0x18, 0xac, 0x97, 0x08, 0xd4, + 0x37, 0x38, 0x38, 0xde, 0x41, 0xb8, 0xb5, 0xeb, 0x0c, 0x02, 0xe7, 0x4e, 0xab, 0x75, 0xf9, 0xd3, 0xd6, 0xe1, 0xcf, + 0x51, 0xeb, 0xb7, 0xbd, 0xd6, 0x8f, 0x43, 0xf7, 0xd2, 0xf9, 0x69, 0x6b, 0x70, 0x28, 0xdf, 0x0e, 0x7f, 0xee, 0xff, + 0x54, 0x0c, 0xff, 0x24, 0x0a, 0x37, 0x5c, 0x77, 0xeb, 0xc4, 0x5b, 0xb0, 0x70, 0xab, 0xd5, 0xea, 0xc3, 0xd3, 0x1c, + 0x9e, 0xf0, 0xe7, 0x19, 0xfc, 0xb8, 0x3c, 0xb4, 0xfe, 0xd3, 0x4f, 0xe9, 0xdf, 0xfc, 0x94, 0x0f, 0x71, 0xcc, 0xc3, + 0x9f, 0x7f, 0x2a, 0xec, 0x7b, 0xfd, 0x70, 0x6b, 0xb8, 0xe9, 0x3a, 0xba, 0xe6, 0x4f, 0x61, 0xf5, 0x08, 0xad, 0x0e, + 0x7f, 0x96, 0x6f, 0xf6, 0xbd, 0xa3, 0xdd, 0x7e, 0x38, 0xbc, 0x74, 0xec, 0xcb, 0x7b, 0xee, 0xa5, 0xeb, 0x5e, 0x6e, + 0xe0, 0x3c, 0x27, 0x30, 0xfa, 0x3d, 0xf8, 0x79, 0x0a, 0x3f, 0x6d, 0xf8, 0x39, 0x81, 0x9f, 0x3f, 0x43, 0x37, 0x11, + 0x7f, 0xbb, 0xa4, 0x58, 0xc8, 0x25, 0x1e, 0x58, 0x44, 0xb0, 0x0a, 0xee, 0xc6, 0x56, 0xec, 0x6d, 0x10, 0xd1, 0x60, + 0x1f, 0xfa, 0xbe, 0x8f, 0x61, 0x52, 0x67, 0xf9, 0x71, 0x03, 0x16, 0x1d, 0x39, 0x67, 0x23, 0x60, 0x9e, 0x88, 0x1c, + 0x14, 0x01, 0x17, 0x67, 0xab, 0x05, 0x1e, 0xae, 0x7a, 0xe3, 0x70, 0x83, 0x39, 0x60, 0x14, 0xbc, 0x66, 0xf8, 0xd0, + 0x75, 0xbd, 0x67, 0xf2, 0xcc, 0x10, 0xf7, 0xb9, 0x60, 0xad, 0x34, 0x13, 0x26, 0x8d, 0xed, 0x7a, 0xf3, 0x35, 0x95, + 0xb0, 0xad, 0xd3, 0x13, 0xa8, 0x9b, 0x89, 0x83, 0xb6, 0xef, 0x58, 0xf4, 0x09, 0xb7, 0xe4, 0x2b, 0xe3, 0x10, 0x78, + 0xc5, 0x92, 0x6f, 0x1a, 0x8d, 0x86, 0x8d, 0x28, 0xdc, 0xb1, 0xc7, 0x0c, 0x66, 0x58, 0x31, 0x11, 0x39, 0x29, 0x4d, + 0x61, 0xd9, 0xc2, 0xe4, 0x6f, 0xa3, 0x9c, 0x6f, 0x54, 0x86, 0x6d, 0x58, 0xb3, 0x64, 0x9b, 0x96, 0xfe, 0x2d, 0xa6, + 0x40, 0xd3, 0x92, 0xce, 0x3f, 0xcc, 0xf1, 0xc3, 0x94, 0xd0, 0x7a, 0xed, 0x70, 0xf0, 0xd0, 0x0b, 0x90, 0x3b, 0xa2, + 0x9f, 0xf3, 0x16, 0xd5, 0x18, 0xfc, 0x95, 0x61, 0x06, 0x4f, 0xcc, 0x87, 0x21, 0x9a, 0x65, 0xa9, 0x83, 0x5b, 0x29, + 0x8a, 0xfb, 0x17, 0xb8, 0x33, 0xd2, 0xd2, 0xdb, 0x0f, 0xd5, 0x8e, 0x39, 0xc8, 0x19, 0xfb, 0x2e, 0x4a, 0x3e, 0xb1, + 0xdc, 0x39, 0xf7, 0x3a, 0xdd, 0x2f, 0xa9, 0xb3, 0x87, 0xb6, 0xd9, 0xbb, 0xea, 0x18, 0x4d, 0x99, 0x05, 0xea, 0x88, + 0xb0, 0xd5, 0xf1, 0x72, 0x8c, 0x6a, 0x21, 0x09, 0x0a, 0x2f, 0x0b, 0xbb, 0xc4, 0xe1, 0xf6, 0x6e, 0x71, 0x7a, 0xd2, + 0xb7, 0x03, 0xdb, 0x06, 0x8b, 0xff, 0x80, 0xc2, 0x56, 0xc2, 0xb0, 0x00, 0x83, 0x6c, 0x37, 0xee, 0xf1, 0xcd, 0xcd, + 0x2a, 0xe0, 0x84, 0x07, 0xe9, 0xd4, 0x3d, 0xf1, 0x22, 0x6f, 0x1a, 0xc2, 0x80, 0x23, 0x68, 0x86, 0x5d, 0x7a, 0xa3, + 0xdd, 0x58, 0x4e, 0x83, 0xb1, 0x10, 0x3f, 0x89, 0x0a, 0xfe, 0x02, 0xe3, 0x11, 0xe1, 0x08, 0x8d, 0x7d, 0x9f, 0x9d, + 0xb3, 0x91, 0xb2, 0x33, 0x80, 0x50, 0x91, 0xdb, 0x73, 0x47, 0xa1, 0xd1, 0x0c, 0xe6, 0x0e, 0xc3, 0x83, 0x81, 0x0d, + 0x7b, 0x09, 0x76, 0x65, 0x18, 0x1d, 0x76, 0x86, 0x83, 0x34, 0x5c, 0xb0, 0x40, 0xd3, 0x56, 0x16, 0xcd, 0x6b, 0x45, + 0xdd, 0xe1, 0xc0, 0x99, 0x80, 0x91, 0x0e, 0xb6, 0xb8, 0x83, 0x6f, 0x18, 0xa1, 0x28, 0xc2, 0x77, 0xec, 0xe4, 0xd9, + 0xf9, 0xdc, 0xb1, 0x77, 0xb7, 0xec, 0x4d, 0x2c, 0xf5, 0x6c, 0x60, 0x2f, 0x98, 0x3b, 0x3c, 0x73, 0xcd, 0xce, 0xdb, + 0x43, 0x04, 0x15, 0x0b, 0x71, 0xf2, 0xb3, 0x81, 0xdd, 0x17, 0x53, 0xb7, 0x61, 0xd0, 0x54, 0x2e, 0x3f, 0xae, 0xe8, + 0x01, 0xa1, 0xaa, 0xba, 0x2a, 0xe8, 0xa0, 0xac, 0x1b, 0x38, 0x53, 0x13, 0x89, 0x16, 0x4e, 0x26, 0xa9, 0x00, 0x0e, + 0x0f, 0x36, 0x83, 0x49, 0x8d, 0x6e, 0xdb, 0xc3, 0xc1, 0x59, 0x70, 0xcf, 0xbe, 0xa7, 0x5e, 0x4e, 0x59, 0x70, 0xc2, + 0xc4, 0xf4, 0xa7, 0x20, 0xed, 0xf0, 0xe7, 0x09, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x21, 0x8b, 0xe6, 0x58, 0x74, 0x10, 0x20, 0xa8, 0x5e, 0xa1, 0xad, 0x3f, 0xb1, 0x26, 0xe3, 0x90, 0x60, 0xbf, 0x7b, 0x17, 0x96, 0x66, 0xb3, 0x33, 0xc4, - 0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc7, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36, - 0x43, 0x41, 0xe1, 0x78, 0xf3, 0x3d, 0x0b, 0xa6, 0xfd, 0xb0, 0x3d, 0x70, 0x72, 0xa1, 0x3a, 0x12, 0x3c, 0xb7, 0x14, - 0x12, 0xbc, 0xed, 0x4d, 0x41, 0xa0, 0x23, 0xe7, 0x6e, 0xd8, 0x9b, 0xaa, 0x10, 0x8a, 0x3e, 0x6e, 0x8e, 0xdd, 0x20, - 0x86, 0x1f, 0x4e, 0x0b, 0x99, 0x66, 0xaa, 0xfb, 0x6a, 0xcd, 0xec, 0x06, 0x63, 0x65, 0x91, 0x27, 0x61, 0xb6, 0xe9, - 0x60, 0x84, 0x16, 0x24, 0xed, 0xee, 0x00, 0x60, 0xd8, 0x74, 0x14, 0xa7, 0x6d, 0x29, 0x56, 0x53, 0xf6, 0xf9, 0x61, - 0xb5, 0x1c, 0x6c, 0x10, 0x31, 0xbf, 0xd2, 0x3e, 0x00, 0x56, 0x90, 0x78, 0xf9, 0x50, 0x9d, 0x79, 0x3d, 0xaf, 0x9d, - 0x6f, 0x2d, 0x95, 0x28, 0x62, 0x9e, 0x21, 0xa1, 0x78, 0xa9, 0xdd, 0x30, 0x61, 0x6e, 0xcf, 0x91, 0x18, 0x9a, 0xe5, - 0xc3, 0x36, 0x30, 0xbd, 0x0a, 0xb0, 0xa7, 0xe6, 0xb6, 0x48, 0xc2, 0xaa, 0xb9, 0x77, 0x08, 0xac, 0x3d, 0x0c, 0x5f, - 0x89, 0x13, 0xc7, 0x9e, 0x8a, 0xe6, 0xb3, 0x24, 0x7c, 0xde, 0x38, 0x2e, 0x8e, 0xf0, 0x44, 0x68, 0xdf, 0x1f, 0x2d, - 0x72, 0x90, 0x07, 0xfc, 0x35, 0x58, 0x06, 0xa1, 0x6c, 0x8a, 0x8e, 0x1e, 0x1e, 0x01, 0x7b, 0x84, 0x78, 0x23, 0x6c, - 0x6e, 0x54, 0xa3, 0x45, 0x49, 0xc6, 0x0b, 0x1d, 0x0c, 0xf7, 0xb8, 0x74, 0xed, 0x51, 0x30, 0xc8, 0x13, 0x63, 0x07, - 0xcf, 0xfc, 0xfd, 0x11, 0x56, 0xe3, 0x04, 0x85, 0x5b, 0xd2, 0x6e, 0xab, 0xc4, 0xdf, 0xbe, 0x9f, 0x82, 0x04, 0xc7, - 0x3a, 0xf0, 0xb3, 0xee, 0xde, 0x4d, 0x24, 0x52, 0xbb, 0x69, 0x8f, 0x4e, 0x22, 0x30, 0x1e, 0x9c, 0xfb, 0x29, 0x54, - 0x23, 0x89, 0xa8, 0x28, 0x47, 0x0b, 0xd4, 0x3c, 0x55, 0xab, 0xe0, 0x3b, 0x34, 0x23, 0xf0, 0x1c, 0xc3, 0xd6, 0xe4, - 0xa7, 0xea, 0xc6, 0x22, 0x96, 0xef, 0xba, 0x74, 0xb4, 0x85, 0x07, 0x90, 0x82, 0xd1, 0x04, 0xc3, 0xb8, 0x14, 0x94, - 0xac, 0xf8, 0xef, 0xa3, 0x11, 0x2b, 0x9f, 0x1e, 0x66, 0x9b, 0x9b, 0x43, 0x71, 0x6e, 0x41, 0x8c, 0xc3, 0x8d, 0xe8, - 0x6a, 0x5c, 0x01, 0x50, 0x9f, 0xce, 0x89, 0xeb, 0x81, 0x69, 0xc5, 0x9a, 0x2e, 0xc5, 0x3e, 0x39, 0xcc, 0x00, 0x14, - 0xdc, 0x72, 0x0e, 0xfd, 0xc1, 0x9f, 0x86, 0xe0, 0x1e, 0xfb, 0x5f, 0xba, 0x5b, 0x4a, 0xd0, 0xf4, 0xe4, 0x99, 0xe2, - 0x92, 0xce, 0x58, 0x3b, 0x1e, 0xc5, 0x46, 0x83, 0xc2, 0x4b, 0x01, 0x03, 0xd0, 0xe6, 0x20, 0x13, 0x2a, 0x0e, 0x42, - 0x8e, 0x0a, 0x6c, 0x1f, 0x37, 0x3f, 0xc7, 0x9d, 0xfd, 0x14, 0x2c, 0xbc, 0x81, 0x7e, 0x7b, 0x0c, 0x6f, 0x7f, 0xd2, - 0x6f, 0x2f, 0x59, 0xf0, 0x4b, 0x29, 0x43, 0xf7, 0xb5, 0x29, 0x1e, 0xa8, 0x29, 0x4a, 0xb1, 0x44, 0x06, 0x0d, 0x99, - 0x9b, 0xaf, 0xc4, 0x6c, 0xb8, 0x5b, 0xa2, 0xda, 0x91, 0xa2, 0x2b, 0xf7, 0x79, 0x74, 0x82, 0xc4, 0x75, 0x4d, 0x52, - 0x18, 0xb9, 0x04, 0x26, 0xc2, 0x15, 0xdf, 0x12, 0x73, 0xf6, 0xdb, 0x60, 0x83, 0xd7, 0xf2, 0x0e, 0xd0, 0xbe, 0x63, - 0xb3, 0x39, 0xbf, 0xd8, 0x27, 0x45, 0x1f, 0xc8, 0xb4, 0x01, 0x71, 0x76, 0xde, 0xee, 0xc5, 0xbb, 0xbc, 0x17, 0x83, - 0x54, 0xcf, 0x15, 0x8b, 0xe1, 0x5e, 0xf5, 0xde, 0x62, 0x94, 0xd2, 0x64, 0x26, 0xaf, 0x86, 0x5e, 0x57, 0xa2, 0xb7, - 0xb9, 0x09, 0x08, 0xf6, 0x8c, 0xae, 0x5c, 0x74, 0x2d, 0x4b, 0x41, 0x13, 0x80, 0xe8, 0x51, 0x9d, 0xe5, 0x88, 0xe3, - 0x30, 0x9b, 0x0d, 0x05, 0x07, 0x73, 0xd7, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, 0xb0, 0x03, 0x98, 0x11, 0x97, 0xb7, - 0x3a, 0x22, 0x3a, 0x2c, 0xfa, 0xeb, 0xf8, 0xf6, 0x47, 0x8f, 0x6d, 0x76, 0x5c, 0xd0, 0x20, 0xb5, 0xb1, 0x1e, 0x56, - 0x63, 0x41, 0x7d, 0xf8, 0x51, 0x53, 0xa9, 0x2c, 0x36, 0x37, 0xcb, 0xfa, 0x51, 0xad, 0xda, 0xc1, 0xb5, 0xd3, 0x94, - 0xf3, 0x66, 0x36, 0x08, 0x07, 0x22, 0x26, 0x50, 0xa0, 0xa5, 0x95, 0x15, 0x03, 0x0c, 0x29, 0xcb, 0x51, 0x3e, 0x85, - 0xcc, 0x8b, 0xcb, 0x52, 0xa7, 0xbe, 0xc8, 0x78, 0x64, 0x88, 0xa7, 0x9e, 0x64, 0xac, 0x80, 0x82, 0xf5, 0x52, 0x2f, - 0xa1, 0x25, 0x02, 0xcc, 0x9f, 0xa9, 0x1c, 0x1a, 0x61, 0x81, 0x44, 0xa1, 0x61, 0x96, 0x28, 0xe3, 0xb3, 0x08, 0x63, - 0xd0, 0xf6, 0x4f, 0x6a, 0xb1, 0xaf, 0x42, 0x19, 0x1d, 0xc5, 0x61, 0x3e, 0x0c, 0xa8, 0x7e, 0x21, 0x25, 0xd8, 0x34, - 0x7c, 0x0f, 0x6c, 0x54, 0x39, 0x9e, 0x24, 0x08, 0x9f, 0xc6, 0x39, 0x23, 0x4f, 0x61, 0x43, 0xc2, 0x2c, 0x4d, 0xdb, - 0x48, 0xb5, 0x8b, 0xcc, 0x20, 0x94, 0x0b, 0xf3, 0x4f, 0x8d, 0xb3, 0x8b, 0x2c, 0x5c, 0x69, 0x0d, 0xe6, 0xc7, 0x1b, - 0x13, 0xa0, 0xec, 0xf2, 0x32, 0x13, 0x3e, 0x6e, 0x44, 0xf6, 0x86, 0xae, 0x98, 0x0e, 0x14, 0x52, 0x81, 0x13, 0x91, - 0xc5, 0x43, 0x67, 0x28, 0x34, 0xc2, 0x01, 0x9d, 0x22, 0xe7, 0xae, 0xb1, 0xe9, 0xf3, 0x81, 0xf6, 0x8d, 0xd2, 0xd0, - 0x49, 0x40, 0x08, 0x08, 0xdc, 0x0d, 0x6b, 0x2a, 0x1d, 0xa4, 0x41, 0x42, 0xa5, 0xe8, 0xe7, 0x00, 0xfe, 0x61, 0x24, - 0x29, 0x00, 0xf6, 0x43, 0x35, 0x52, 0x44, 0x59, 0x16, 0xb8, 0x00, 0x34, 0xd7, 0x3e, 0xae, 0x84, 0x2f, 0x0c, 0x54, - 0x98, 0x9e, 0x66, 0xe5, 0xa5, 0x50, 0x22, 0xef, 0xd6, 0xa4, 0xac, 0x91, 0x4c, 0x3e, 0x45, 0x87, 0x4f, 0x79, 0xd7, - 0xaf, 0x25, 0x1e, 0xba, 0xe0, 0x29, 0x2c, 0xab, 0x7a, 0x7e, 0x15, 0x72, 0x72, 0xae, 0x41, 0x57, 0x48, 0xa1, 0xbf, - 0xe2, 0x24, 0xef, 0xbd, 0xf2, 0xab, 0x5a, 0x6a, 0x0c, 0x65, 0xef, 0xd7, 0x35, 0xc3, 0xf2, 0x72, 0x5e, 0x85, 0x29, - 0x08, 0xb8, 0x25, 0x4b, 0x82, 0xa5, 0xd4, 0x10, 0x60, 0x61, 0x7b, 0xa4, 0x95, 0x82, 0xbc, 0xd4, 0xe1, 0x9d, 0xa7, - 0x60, 0x05, 0x18, 0x87, 0x5a, 0x2a, 0x99, 0x46, 0x12, 0x5f, 0x2a, 0x51, 0x60, 0xca, 0xfd, 0x11, 0xf8, 0xa9, 0xcd, - 0x93, 0xae, 0x73, 0xd7, 0x8f, 0x67, 0x98, 0xda, 0x43, 0xa0, 0xc7, 0xde, 0x1d, 0x30, 0x25, 0xea, 0x3a, 0xac, 0x20, - 0x0e, 0xcd, 0x6a, 0x9a, 0x05, 0xcc, 0x98, 0x36, 0x68, 0xc9, 0x36, 0xd8, 0x72, 0x39, 0xd8, 0x47, 0x62, 0x7b, 0x56, - 0x2b, 0x20, 0x74, 0x0d, 0x1a, 0x18, 0x72, 0x97, 0x0a, 0x2d, 0xcc, 0x7b, 0x5d, 0x2a, 0xc2, 0xfd, 0x39, 0xe0, 0xd2, - 0x0a, 0xce, 0xbc, 0x8c, 0x06, 0xde, 0x8f, 0x8f, 0x13, 0x4c, 0x7c, 0x41, 0xac, 0xc0, 0x0e, 0x0e, 0x3a, 0xcd, 0xa6, - 0xc0, 0xa9, 0xb8, 0x48, 0x19, 0x2c, 0x2b, 0x4a, 0x6d, 0xf8, 0x21, 0x45, 0xb6, 0xee, 0xf2, 0x40, 0x77, 0x21, 0x16, - 0xc0, 0x4e, 0xbf, 0x60, 0xe4, 0x5b, 0xd6, 0xcb, 0x80, 0xc1, 0xa9, 0xd6, 0x38, 0x08, 0xfc, 0xe6, 0x66, 0x32, 0x2c, - 0x53, 0x62, 0xbb, 0x26, 0xab, 0x0b, 0xc8, 0x61, 0xa8, 0x26, 0xee, 0x20, 0x2c, 0x95, 0x3d, 0x5e, 0x94, 0x33, 0x5c, - 0x2e, 0x65, 0x21, 0x37, 0xcf, 0xab, 0x69, 0x3e, 0xb7, 0xd2, 0x6c, 0x3a, 0xde, 0x8a, 0x2f, 0x0a, 0xfe, 0x81, 0x13, - 0x4b, 0xab, 0x9e, 0x52, 0x2b, 0x3c, 0xca, 0xdc, 0x92, 0x75, 0x4a, 0x6a, 0x75, 0xdd, 0x40, 0x35, 0xc2, 0xd3, 0x34, - 0x6c, 0x04, 0x42, 0x4c, 0x70, 0xf1, 0xeb, 0x26, 0x13, 0xd3, 0xde, 0x12, 0x52, 0x47, 0xd8, 0x3d, 0x94, 0x13, 0xdc, - 0xd5, 0x3c, 0xfb, 0x3c, 0x9c, 0x5f, 0xcd, 0xdc, 0x7b, 0x06, 0x73, 0x3f, 0x0e, 0xb9, 0xc1, 0xe8, 0xb1, 0x4c, 0xf8, - 0x91, 0xb1, 0x8f, 0x5c, 0x55, 0x3d, 0x39, 0x09, 0x2b, 0x91, 0x25, 0x9e, 0x8c, 0xa3, 0x0e, 0xe3, 0x54, 0xb4, 0x26, - 0xc8, 0x2e, 0x2f, 0x0b, 0x73, 0x2f, 0x50, 0xd0, 0xd4, 0xe3, 0xf5, 0x38, 0x6d, 0xc5, 0xce, 0x46, 0x24, 0x72, 0xef, - 0x55, 0x2d, 0x12, 0x59, 0xf1, 0x39, 0x8e, 0x74, 0xc5, 0x41, 0xee, 0x93, 0x93, 0xd5, 0x4d, 0x2a, 0x74, 0x8b, 0x46, - 0xdb, 0xd8, 0xa3, 0xfa, 0x40, 0x52, 0xcf, 0xa8, 0xc0, 0xaa, 0xc6, 0xbe, 0x7b, 0xb7, 0x23, 0xd2, 0x2d, 0x95, 0x62, - 0x83, 0xa5, 0x85, 0xd1, 0x8c, 0x51, 0x30, 0x28, 0x29, 0x32, 0x50, 0xa3, 0xfc, 0x0a, 0xc1, 0xb0, 0x47, 0x0d, 0x40, - 0x71, 0xae, 0xaf, 0x7e, 0x5c, 0x4a, 0xb6, 0x10, 0x90, 0xb8, 0x4b, 0x06, 0x62, 0x4d, 0x30, 0x33, 0xf2, 0xc9, 0x7b, - 0xe0, 0xbc, 0x01, 0x43, 0x1f, 0x01, 0xfc, 0x02, 0xb1, 0xe9, 0xc1, 0xc4, 0xb6, 0x89, 0x28, 0xfa, 0x6c, 0xe0, 0x39, - 0x00, 0x3b, 0xaf, 0x42, 0xa3, 0xef, 0xaa, 0x14, 0x30, 0x64, 0x03, 0x37, 0x60, 0x55, 0x58, 0x6e, 0xef, 0x39, 0xb8, - 0x0d, 0xf0, 0xfa, 0x4c, 0x36, 0xdf, 0xc0, 0x3c, 0xc1, 0xea, 0xec, 0xc2, 0xaf, 0x2c, 0x6b, 0x71, 0xee, 0x74, 0xd0, - 0xa8, 0x57, 0x94, 0x10, 0xb5, 0xfb, 0x58, 0x7b, 0x80, 0x11, 0x16, 0xf1, 0xfe, 0x0a, 0xdf, 0xf5, 0xb8, 0xe5, 0x9e, - 0x46, 0x8b, 0x30, 0x5d, 0x25, 0x8d, 0x41, 0xc9, 0xba, 0x9f, 0x8c, 0xb8, 0x97, 0xfb, 0x22, 0x16, 0x5c, 0xe1, 0xc8, - 0xaa, 0x90, 0x62, 0x03, 0x49, 0x7a, 0xda, 0xa3, 0x03, 0xf6, 0x8d, 0x66, 0x2f, 0xa0, 0xcc, 0xfb, 0x8a, 0x54, 0x12, - 0x52, 0x9a, 0xdd, 0x10, 0x49, 0xc2, 0x5a, 0x91, 0xa7, 0xce, 0xfb, 0x8e, 0xf6, 0xb9, 0x95, 0x44, 0x30, 0x82, 0x93, - 0x30, 0x1d, 0x2b, 0x0f, 0x9a, 0x02, 0x5c, 0x45, 0x47, 0x4c, 0xdf, 0x04, 0xe4, 0x37, 0x03, 0xb9, 0xbd, 0x92, 0x5c, - 0x9b, 0x6b, 0x18, 0x9e, 0x20, 0xc1, 0xaa, 0x48, 0x04, 0x1e, 0x51, 0x03, 0x8e, 0xf9, 0x3a, 0xcf, 0x03, 0x4c, 0xf8, - 0xda, 0xde, 0x04, 0x80, 0x72, 0x72, 0x55, 0x9c, 0x95, 0x40, 0x37, 0x60, 0xb9, 0x3e, 0x4e, 0x8d, 0x8a, 0xc4, 0xc5, - 0x8d, 0xe9, 0xea, 0x96, 0xfe, 0x0c, 0x2d, 0x67, 0x32, 0xc4, 0x74, 0x10, 0x04, 0x64, 0xea, 0x3b, 0xe6, 0x08, 0x99, - 0x2b, 0xac, 0xcf, 0xb9, 0x53, 0x9b, 0xba, 0xc7, 0xa8, 0x9b, 0x27, 0xa9, 0xc5, 0xeb, 0xb4, 0x29, 0x25, 0x62, 0x52, - 0x62, 0x6e, 0x88, 0x54, 0x6c, 0xa6, 0xc4, 0x9d, 0x5b, 0xdf, 0x68, 0x21, 0x6d, 0xb4, 0x0d, 0x91, 0x83, 0xcd, 0x2a, - 0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x5f, 0x51, 0x92, 0x0e, 0x73, 0x4c, 0x04, 0xab, 0x17, 0x53, 0x91, 0xbf, - 0x73, 0x74, 0x9a, 0xbd, 0x41, 0x0f, 0x52, 0x6f, 0x20, 0x31, 0x6b, 0xe2, 0xbb, 0x90, 0x86, 0x3a, 0x42, 0xa0, 0x32, - 0xaa, 0x65, 0x3a, 0x4e, 0xac, 0xc2, 0x37, 0x82, 0xaf, 0xde, 0xea, 0xe3, 0x7c, 0xe3, 0xb9, 0xb1, 0x1a, 0x41, 0x0c, - 0xde, 0x42, 0x3e, 0xf4, 0xa4, 0x08, 0x07, 0xc2, 0xe5, 0x9b, 0x9b, 0xbd, 0x7c, 0x97, 0x57, 0x21, 0x92, 0x0a, 0xc6, - 0x18, 0x33, 0x8a, 0x71, 0x4f, 0xd4, 0xd4, 0x62, 0x0e, 0x03, 0xcb, 0xd6, 0x61, 0x8e, 0x07, 0x00, 0xd0, 0xd2, 0x94, - 0x5e, 0x35, 0x15, 0x2a, 0xcf, 0x73, 0x09, 0x9f, 0xea, 0x10, 0x55, 0x35, 0x7e, 0xbb, 0x3e, 0x03, 0x85, 0xe0, 0xbe, - 0xd3, 0xf1, 0xf0, 0x10, 0x02, 0x56, 0x51, 0xc8, 0x02, 0xbd, 0x41, 0x7b, 0x55, 0x22, 0x14, 0x33, 0x27, 0xeb, 0x31, - 0xc3, 0x49, 0x05, 0x5b, 0xa8, 0x84, 0xa5, 0xd2, 0x02, 0xbf, 0xda, 0x08, 0xcd, 0x53, 0xc6, 0xbd, 0x57, 0x15, 0xce, - 0xa0, 0x3f, 0x98, 0xb7, 0xca, 0xa8, 0x6f, 0x57, 0x4e, 0x64, 0x2a, 0x30, 0x71, 0x33, 0x4b, 0xed, 0xf7, 0xcb, 0x3a, - 0xed, 0xe7, 0x15, 0x72, 0x9f, 0x93, 0xe6, 0xeb, 0xdc, 0x42, 0xf3, 0xc9, 0x70, 0xbf, 0x52, 0x7e, 0x68, 0x61, 0xd4, - 0x94, 0x5f, 0x5e, 0x57, 0x7e, 0x85, 0xa7, 0xc2, 0x5b, 0xfd, 0x2e, 0x0a, 0x5d, 0xd4, 0xe7, 0x60, 0x08, 0xe9, 0x47, - 0x70, 0x0d, 0x0d, 0x1e, 0x14, 0xc9, 0x62, 0xb1, 0x76, 0x41, 0x5c, 0x1f, 0x73, 0xaa, 0x1d, 0xca, 0x18, 0x23, 0x9e, - 0x96, 0x1c, 0x24, 0x19, 0x1c, 0x8c, 0xdf, 0xc0, 0x80, 0x98, 0x94, 0x84, 0x74, 0x08, 0x9d, 0xb5, 0x99, 0x88, 0xca, - 0x5d, 0xbc, 0xd9, 0xb8, 0xac, 0x29, 0x14, 0x61, 0x27, 0x98, 0xa9, 0x94, 0x0a, 0x02, 0x69, 0xf2, 0xdd, 0xe9, 0xd4, - 0x82, 0xa1, 0x85, 0x6b, 0x2a, 0x20, 0xaf, 0xed, 0x7a, 0xd0, 0xe4, 0x3d, 0xc5, 0xd0, 0xd7, 0xa9, 0x11, 0x2f, 0x33, - 0xf8, 0x1a, 0x36, 0x7f, 0x4d, 0x94, 0xe4, 0x21, 0x13, 0xb1, 0x57, 0xf0, 0x89, 0x90, 0x4d, 0xc1, 0xce, 0x04, 0xfa, - 0xa1, 0x5d, 0xd9, 0x4b, 0x77, 0x8b, 0xca, 0xa5, 0x45, 0x63, 0x2b, 0x51, 0xb3, 0xe6, 0x87, 0xf1, 0x66, 0x0a, 0xfb, - 0xd9, 0xa3, 0x04, 0x02, 0xd2, 0x54, 0x4e, 0x52, 0xcd, 0x7b, 0x98, 0x0e, 0x01, 0x24, 0xd8, 0xfd, 0x04, 0x16, 0xfa, - 0x4d, 0x89, 0x09, 0x16, 0x55, 0x63, 0xb7, 0x39, 0x68, 0xcd, 0x39, 0x69, 0xbe, 0x39, 0x6a, 0xed, 0x4d, 0x65, 0x3d, - 0x63, 0x76, 0x80, 0x6d, 0xbb, 0x9b, 0xc5, 0x61, 0xba, 0xd9, 0x19, 0x1a, 0x82, 0x0b, 0x8f, 0xff, 0x93, 0x12, 0xd3, - 0x40, 0x72, 0xa9, 0x1b, 0x3f, 0xa1, 0x0e, 0xc3, 0xff, 0x96, 0xa4, 0x80, 0x07, 0xb5, 0xd5, 0x58, 0x71, 0xee, 0x15, - 0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, 0x89, 0x62, 0x9e, 0x13, 0x00, 0xa3, 0xd8, 0xfc, - 0x29, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4d, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0x4f, 0xe8, 0x48, 0x45, 0x65, 0x73, 0x12, - 0xf3, 0x6f, 0x0b, 0x30, 0xcd, 0x89, 0x0f, 0xf5, 0x5c, 0xc3, 0x50, 0x80, 0xaf, 0x6c, 0x28, 0x35, 0xdb, 0xe3, 0xdf, - 0x3b, 0xdb, 0x7d, 0x49, 0x14, 0xc1, 0x02, 0x0d, 0xba, 0x5c, 0x81, 0x2f, 0x60, 0x19, 0xdc, 0x92, 0x7e, 0x0a, 0xbe, - 0x97, 0x57, 0xc1, 0x67, 0xec, 0x7f, 0x01, 0x68, 0x55, 0x60, 0x40, 0xb9, 0xd3, 0x34, 0xac, 0x84, 0xb8, 0x44, 0x85, - 0x59, 0xc5, 0xf9, 0xe3, 0x3a, 0xaf, 0x9b, 0x96, 0x25, 0x06, 0xe5, 0xe7, 0xae, 0xe1, 0xc6, 0xf7, 0x1a, 0xf9, 0xe3, - 0x7b, 0xcf, 0x41, 0xb7, 0x13, 0x69, 0xef, 0xde, 0xcd, 0xef, 0x90, 0x85, 0x86, 0xf7, 0xc2, 0xe6, 0xd0, 0x16, 0xe9, - 0x92, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x07, 0x0c, 0xc1, 0x63, 0xa7, 0x32, 0xf9, - 0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, 0xe4, 0xc5, 0x3a, 0xd9, 0x9e, 0x9d, 0xc2, 0xf3, - 0xcb, 0xb8, 0x00, 0xea, 0x00, 0xfa, 0x15, 0x95, 0xc5, 0x06, 0x72, 0x71, 0x53, 0xd6, 0x7a, 0x45, 0xe3, 0xf1, 0xb5, - 0x5d, 0x58, 0x5d, 0x81, 0x4f, 0xa3, 0x74, 0x9c, 0x88, 0x49, 0xcc, 0xa4, 0xca, 0x35, 0xb9, 0x36, 0xba, 0x97, 0xb6, - 0x68, 0x9e, 0x0b, 0x09, 0x5e, 0x11, 0xb8, 0x21, 0xf4, 0x95, 0xbe, 0x5c, 0x6f, 0xa0, 0xe0, 0x51, 0x7b, 0x73, 0x11, - 0x4c, 0x4c, 0x3c, 0x66, 0x48, 0x4d, 0xbf, 0x0e, 0xa7, 0x56, 0x16, 0x2b, 0x0e, 0xbf, 0xce, 0x19, 0x6b, 0x28, 0x00, - 0xe2, 0x93, 0x07, 0x57, 0xbb, 0x49, 0xaf, 0x94, 0x76, 0x50, 0x1a, 0x21, 0xbe, 0xad, 0xf0, 0x75, 0x97, 0x8a, 0xaf, - 0x5c, 0x75, 0xef, 0x6b, 0xc6, 0x8c, 0x0b, 0x46, 0xcf, 0xf9, 0x2c, 0x69, 0x5c, 0xbb, 0xa1, 0xbb, 0x3a, 0x3f, 0x7a, - 0x3f, 0xc8, 0xbc, 0x85, 0x19, 0xb0, 0x09, 0xa8, 0x82, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x3b, 0xf3, 0x88, 0x57, - 0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xbb, 0xc1, 0x77, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26, 0xca, - 0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54, 0x44, - 0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d, 0x46, - 0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85, 0x77, - 0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0, 0x74, - 0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x75, 0xbe, 0x6a, 0x95, 0x58, 0xba, - 0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f, 0xcd, - 0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45, 0x90, - 0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa, 0x66, - 0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97, 0x97, - 0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x98, 0xad, 0x1f, 0xbc, - 0xb9, 0x7b, 0x05, 0x2c, 0xc7, 0x80, 0xdd, 0x31, 0x73, 0x1a, 0x43, 0x55, 0x1b, 0xf8, 0x87, 0xf5, 0x83, 0xad, 0xdb, - 0xc3, 0x3f, 0x0c, 0x7e, 0x08, 0xae, 0x6d, 0x6c, 0x6c, 0xe3, 0xed, 0x5a, 0x22, 0xc8, 0x2b, 0x3c, 0xd0, 0xc7, 0xab, - 0x8f, 0x82, 0x96, 0xeb, 0xc4, 0xf6, 0xc0, 0xa1, 0xb0, 0x35, 0xc8, 0x37, 0x29, 0x93, 0x46, 0x8b, 0x82, 0x67, 0x33, - 0x39, 0x43, 0x21, 0xaf, 0xf9, 0x38, 0x68, 0x3b, 0xc2, 0xdf, 0xc0, 0xa9, 0x1d, 0x2f, 0x2f, 0x3f, 0x41, 0x1f, 0xf0, - 0x74, 0xa5, 0x34, 0x15, 0x71, 0x4a, 0xb9, 0x45, 0x57, 0xeb, 0x3c, 0x18, 0x29, 0x2e, 0xa6, 0xa8, 0x74, 0xdc, 0xe5, - 0xb5, 0xb3, 0x91, 0xd3, 0x5f, 0xe2, 0xd5, 0x45, 0xba, 0x7c, 0x24, 0xb2, 0x55, 0x4b, 0xef, 0x37, 0x7d, 0xba, 0x6d, - 0xcf, 0x18, 0x9f, 0x66, 0x63, 0x3a, 0x98, 0xf1, 0x71, 0x22, 0xbc, 0x3e, 0x31, 0xd6, 0x77, 0x8b, 0xc0, 0x74, 0x73, - 0x6c, 0xf2, 0xc3, 0xf1, 0x7a, 0xb3, 0x59, 0xe3, 0x0e, 0xde, 0x38, 0x4f, 0x9c, 0x65, 0x89, 0x11, 0x95, 0xa5, 0x86, - 0x07, 0xb4, 0x42, 0xdc, 0xbc, 0x67, 0x02, 0xe3, 0xb2, 0x0b, 0x92, 0xda, 0x6e, 0x20, 0x70, 0xb1, 0x27, 0x31, 0x4b, - 0xc6, 0xb6, 0x07, 0xe5, 0x81, 0xbe, 0x18, 0x4d, 0xb7, 0x80, 0x69, 0x79, 0xed, 0xec, 0x2c, 0xb5, 0xbd, 0x6a, 0xaa, - 0x00, 0x66, 0xc9, 0xf2, 0xf8, 0x04, 0x59, 0xf7, 0x5b, 0xe8, 0x22, 0x06, 0x8c, 0x8d, 0x2b, 0x73, 0xee, 0x72, 0xdd, - 0x8a, 0xf8, 0x46, 0x13, 0x69, 0x52, 0x1f, 0x52, 0xdf, 0x61, 0x58, 0xab, 0xab, 0x1c, 0x24, 0x70, 0x8f, 0xbc, 0x5b, - 0xe2, 0xd2, 0xd3, 0x67, 0x16, 0x93, 0x2a, 0x7d, 0x4b, 0x5d, 0x8b, 0x6b, 0x86, 0xbd, 0xe2, 0x01, 0xd8, 0x1f, 0x18, - 0xb7, 0x88, 0x45, 0xbc, 0x9d, 0xd7, 0x52, 0x58, 0x1b, 0x73, 0xa0, 0xb9, 0xe1, 0x06, 0xbf, 0xb1, 0x6a, 0xcd, 0xc0, - 0x0c, 0x33, 0xce, 0x48, 0x7e, 0x33, 0xee, 0x55, 0x4d, 0x1c, 0xb9, 0x0a, 0x20, 0xfa, 0x96, 0x74, 0x49, 0x0e, 0xaf, - 0x64, 0xb9, 0xea, 0x0c, 0xf9, 0x57, 0x58, 0x67, 0xbd, 0x38, 0x01, 0x33, 0x69, 0xca, 0x4b, 0x4c, 0x4c, 0x11, 0x97, - 0x9b, 0x65, 0xcc, 0xd3, 0xf4, 0x59, 0xb4, 0x83, 0x93, 0x1b, 0x09, 0x1c, 0xb1, 0x6f, 0x2c, 0x43, 0x33, 0x61, 0x23, - 0x26, 0xd2, 0xa8, 0x94, 0x12, 0x3e, 0x90, 0x4b, 0x2d, 0xf9, 0xcb, 0x5c, 0x5e, 0x7d, 0xb9, 0x4d, 0x70, 0x40, 0x5e, - 0x03, 0xcb, 0xa1, 0x71, 0xdc, 0x32, 0x90, 0x88, 0xc5, 0x80, 0x18, 0xb5, 0x2a, 0x57, 0x93, 0x51, 0x9d, 0xcc, 0x57, - 0xc8, 0x85, 0x8a, 0x3c, 0xb8, 0x25, 0x50, 0xf2, 0xe7, 0x98, 0x3a, 0x98, 0x95, 0xda, 0x4d, 0x8b, 0x4d, 0x92, 0xf7, - 0xcc, 0x80, 0xe4, 0xfa, 0x6b, 0x78, 0x68, 0xfc, 0xe2, 0x95, 0x39, 0x25, 0x7c, 0x51, 0xc6, 0xd2, 0xd2, 0x98, 0x4b, - 0xff, 0x42, 0xde, 0xa7, 0x95, 0x80, 0xfd, 0x0a, 0x62, 0xca, 0xc0, 0x25, 0x36, 0x2e, 0x48, 0xca, 0x6b, 0x79, 0xca, - 0xee, 0x6b, 0x28, 0xdf, 0x15, 0x93, 0xae, 0x52, 0x59, 0x57, 0x58, 0x75, 0xbf, 0x2e, 0x58, 0x7e, 0xb1, 0xcf, 0x30, - 0x37, 0x19, 0x0d, 0xb2, 0x15, 0x33, 0x9b, 0xf2, 0xab, 0xbd, 0x6b, 0xbf, 0xf2, 0x50, 0xd2, 0xa1, 0x5a, 0xa5, 0x9b, - 0x57, 0x6e, 0x38, 0xc6, 0x8d, 0x1b, 0x8e, 0x00, 0x36, 0x86, 0x9d, 0x2a, 0x52, 0xeb, 0xfc, 0xf7, 0xd5, 0xf0, 0x13, - 0xed, 0xb5, 0xa1, 0xde, 0x75, 0xc3, 0xb5, 0xe9, 0xe9, 0xd7, 0xa0, 0x6a, 0x64, 0x09, 0x5d, 0x87, 0x2a, 0x26, 0x23, - 0x51, 0x62, 0xba, 0x4a, 0x79, 0xd4, 0xd7, 0x88, 0x73, 0x10, 0x37, 0x94, 0xbf, 0xf8, 0xe7, 0xf0, 0xe2, 0x28, 0x40, - 0x23, 0x6a, 0x39, 0xc9, 0x52, 0xde, 0x9a, 0x44, 0xb3, 0x38, 0xb9, 0x08, 0x16, 0x71, 0x6b, 0x96, 0xa5, 0x59, 0x31, - 0x07, 0xae, 0xf4, 0x8a, 0x0b, 0xb0, 0xe1, 0x67, 0xad, 0x45, 0xec, 0x3d, 0x67, 0xc9, 0x29, 0xe3, 0xf1, 0x28, 0xf2, - 0xec, 0xbd, 0x1c, 0xc4, 0x83, 0xf5, 0x3a, 0xca, 0xf3, 0xec, 0xcc, 0xf6, 0xde, 0x65, 0xc7, 0xc0, 0xb4, 0xde, 0x9b, - 0xf3, 0x8b, 0x13, 0x96, 0x7a, 0xef, 0x8f, 0x17, 0x29, 0x5f, 0x78, 0x45, 0x94, 0x16, 0xad, 0x82, 0xe5, 0xf1, 0x04, - 0xd4, 0x44, 0x92, 0xe5, 0x2d, 0xcc, 0x7f, 0x9e, 0xb1, 0x20, 0x89, 0x4f, 0xa6, 0xdc, 0x1a, 0x47, 0xf9, 0xa7, 0x5e, - 0xab, 0x35, 0xcf, 0xe3, 0x59, 0x94, 0x5f, 0xb4, 0xa8, 0x45, 0xf0, 0x45, 0x7b, 0x3b, 0xfa, 0x6a, 0x72, 0xbf, 0xc7, - 0x73, 0xe8, 0x1b, 0x23, 0x15, 0x03, 0x10, 0x3e, 0xd6, 0xf6, 0x4e, 0x7b, 0x56, 0xdc, 0x11, 0x27, 0x4a, 0x51, 0xca, - 0xcb, 0x23, 0xef, 0x23, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79, 0x01, - 0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04, 0xf7, - 0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66, 0x05, - 0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2, 0x93, - 0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff, 0x9d, - 0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39, 0xf8, - 0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe, 0x5e, - 0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01, 0xc8, - 0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e, 0x8e, - 0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f, 0xc5, - 0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98, 0xe8, - 0xc3, 0x55, 0x1f, 0xb1, 0xf9, 0xfa, 0xc6, 0x2f, 0xd5, 0x78, 0x67, 0x2a, 0x6f, 0x50, 0xa8, 0x08, 0xf5, 0xcd, 0x16, - 0xcc, 0x78, 0xcb, 0xfb, 0x1d, 0x7d, 0x50, 0x35, 0xf8, 0x9a, 0x91, 0xd6, 0x0b, 0xb8, 0x67, 0xe6, 0x02, 0xf5, 0xd2, - 0x3e, 0x86, 0xa4, 0x5a, 0x2d, 0x17, 0xf4, 0x06, 0xc3, 0x10, 0x12, 0x1d, 0x08, 0x3a, 0xf9, 0xa0, 0xa0, 0x6f, 0x6a, - 0x64, 0x6e, 0x50, 0x38, 0x99, 0x0b, 0x5b, 0x3e, 0xd3, 0x72, 0x1d, 0x94, 0x34, 0x78, 0xd9, 0x1f, 0x98, 0x6c, 0x00, - 0xd2, 0x9b, 0x42, 0x5d, 0x7f, 0x09, 0x85, 0x2b, 0xa5, 0x1c, 0xa9, 0xd9, 0x75, 0x57, 0xf4, 0x61, 0x55, 0x62, 0xca, - 0x48, 0x3e, 0x1c, 0xfe, 0x3b, 0x0c, 0x7b, 0x47, 0x3b, 0x96, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0xa9, 0x5b, 0xf5, 0xf8, - 0x6d, 0x52, 0xb8, 0xb6, 0xc7, 0xb4, 0x9c, 0x47, 0x37, 0xb8, 0xf6, 0x91, 0x03, 0x4e, 0x87, 0x20, 0xe2, 0x8e, 0x81, - 0x8c, 0x72, 0x28, 0x08, 0x51, 0x75, 0x8d, 0x29, 0xdf, 0x8d, 0xee, 0x5f, 0xfa, 0x8b, 0x34, 0x06, 0x49, 0xf7, 0x31, - 0x1e, 0xd3, 0xbd, 0x93, 0x78, 0x4c, 0x07, 0x11, 0x2d, 0x4a, 0x3c, 0xc2, 0xc8, 0x36, 0x14, 0xa8, 0xef, 0xb0, 0xc0, - 0xb3, 0x4c, 0x64, 0xb1, 0x5b, 0x36, 0x1e, 0x26, 0x18, 0xaa, 0x72, 0x9c, 0xcd, 0xa2, 0x38, 0x0d, 0xf0, 0xfb, 0x20, - 0x9e, 0x1e, 0x31, 0xc0, 0x2e, 0x1e, 0xfc, 0x64, 0x32, 0x17, 0xad, 0xe3, 0xfa, 0xbf, 0x80, 0x1c, 0xa1, 0xfe, 0xa5, - 0xf4, 0xc3, 0x34, 0x5c, 0xea, 0x98, 0xb7, 0x5e, 0x0a, 0xb2, 0x87, 0x2b, 0x9b, 0x95, 0x51, 0x9c, 0x63, 0x97, 0xd3, - 0x8f, 0x41, 0xab, 0x13, 0x74, 0xb4, 0xeb, 0x5a, 0xbb, 0x8d, 0x2a, 0x72, 0x59, 0xe4, 0x8d, 0x46, 0x82, 0x41, 0x3f, - 0x0b, 0x38, 0xab, 0x77, 0x0d, 0xab, 0x27, 0xf9, 0x12, 0x03, 0x38, 0x27, 0xa9, 0x53, 0x03, 0x82, 0xce, 0x02, 0xae, - 0x98, 0xca, 0x2d, 0x23, 0x52, 0x4a, 0x8f, 0x69, 0x03, 0xd7, 0xef, 0x12, 0xe1, 0xbd, 0xa1, 0x7a, 0x0a, 0x94, 0x62, - 0xb9, 0xf1, 0xd1, 0xae, 0xd8, 0xf1, 0x16, 0xf1, 0x58, 0x68, 0xc3, 0x16, 0xb4, 0xad, 0x3f, 0x8d, 0x80, 0x4a, 0x9f, - 0x42, 0x7b, 0x63, 0xe9, 0xa8, 0xc4, 0xfa, 0x1c, 0xe6, 0xda, 0x13, 0x5a, 0x8f, 0x6e, 0xe4, 0xdb, 0xfd, 0x8d, 0x25, - 0x2f, 0x77, 0xb7, 0x44, 0xef, 0xfe, 0x51, 0x59, 0x90, 0x82, 0x32, 0x03, 0x69, 0xd5, 0x14, 0xa2, 0x0e, 0x86, 0xa5, - 0xf4, 0x5d, 0x1c, 0x37, 0xd7, 0x46, 0x97, 0x88, 0x18, 0x4b, 0xb6, 0x2b, 0x30, 0x5d, 0x29, 0xca, 0x61, 0x4f, 0xea, - 0x84, 0x94, 0x42, 0xe4, 0x60, 0xf4, 0x56, 0xa1, 0x38, 0x42, 0x08, 0x06, 0x1b, 0xcb, 0xb8, 0x0c, 0x37, 0x96, 0x59, - 0x79, 0x04, 0x96, 0x09, 0x42, 0x95, 0xab, 0xcf, 0xbb, 0xc0, 0xc4, 0x22, 0xc8, 0x62, 0xd1, 0x08, 0x38, 0x2d, 0x2b, - 0x6d, 0x6b, 0x20, 0xa0, 0x01, 0x0f, 0x10, 0x0b, 0xc0, 0x76, 0xa3, 0x5e, 0x0c, 0x70, 0x11, 0xad, 0xfb, 0x30, 0xd0, - 0xee, 0x96, 0x68, 0x04, 0x78, 0xe5, 0x08, 0x72, 0x85, 0x16, 0xa6, 0xe3, 0x98, 0xa8, 0x8d, 0xe3, 0x53, 0x4d, 0x3a, - 0xca, 0x4d, 0xee, 0xef, 0x26, 0xd1, 0x31, 0x4b, 0x60, 0xc8, 0xe2, 0xf2, 0xb2, 0x0d, 0x23, 0x89, 0x57, 0x6b, 0x37, - 0x4e, 0xe7, 0x0b, 0xf9, 0x99, 0x2d, 0x98, 0xb8, 0x83, 0x27, 0x9f, 0x78, 0x0b, 0x60, 0xa0, 0x8e, 0xf2, 0x02, 0x39, - 0x00, 0x80, 0x48, 0xa7, 0x08, 0x08, 0x5d, 0xc5, 0x16, 0x50, 0x1a, 0x8f, 0x57, 0xcb, 0x60, 0x27, 0xce, 0xb1, 0x34, - 0x85, 0xe7, 0x59, 0x9c, 0xe2, 0x63, 0x81, 0x8f, 0xd1, 0x39, 0x3e, 0x66, 0xf0, 0xa8, 0x71, 0xcf, 0x4b, 0xfb, 0x6f, - 0xb2, 0x02, 0x96, 0x26, 0x40, 0x76, 0x79, 0x09, 0xf2, 0x5e, 0x93, 0x60, 0x77, 0x0b, 0x88, 0x85, 0x9c, 0x22, 0x3e, - 0xba, 0xc2, 0x4c, 0x32, 0xb2, 0x62, 0xde, 0x12, 0xe5, 0x16, 0x69, 0xd5, 0x10, 0x9c, 0xae, 0xdc, 0x69, 0x18, 0x0f, - 0x9e, 0x4c, 0x2f, 0x79, 0x82, 0x2f, 0xae, 0x6d, 0x89, 0xaf, 0x62, 0x08, 0xa2, 0xd0, 0x23, 0x62, 0xa8, 0xcb, 0xb8, - 0xfc, 0xde, 0x4d, 0x1c, 0xda, 0x38, 0x0b, 0xd8, 0x6f, 0xa8, 0x05, 0x78, 0x14, 0x27, 0xa2, 0xf1, 0x1a, 0x7c, 0x1a, - 0x79, 0x82, 0x84, 0xce, 0xee, 0x56, 0x05, 0x1b, 0x00, 0x3f, 0x12, 0xb7, 0xac, 0x1d, 0x91, 0x03, 0x69, 0x8b, 0x72, - 0x3a, 0x3b, 0x97, 0x5b, 0x5a, 0x46, 0x76, 0x45, 0xac, 0x5c, 0xa3, 0x4a, 0x39, 0x8b, 0xf6, 0x24, 0x4a, 0xd7, 0x35, - 0x05, 0xe8, 0xe7, 0x8c, 0x8d, 0x3d, 0xdb, 0x02, 0x59, 0x2a, 0x9e, 0x3f, 0x26, 0xec, 0x94, 0xc9, 0x2f, 0xa5, 0xe8, - 0x41, 0x74, 0xe5, 0x08, 0x54, 0x32, 0x97, 0x97, 0x38, 0x25, 0x7b, 0x2a, 0x1c, 0x25, 0x25, 0xea, 0x88, 0x78, 0xb6, - 0x31, 0x68, 0x73, 0x8e, 0x76, 0x7d, 0x58, 0xaf, 0x03, 0xd6, 0xae, 0x2d, 0xe0, 0x25, 0x3b, 0xee, 0x66, 0xe4, 0x60, - 0x00, 0x36, 0x99, 0xc0, 0x76, 0x51, 0x91, 0x65, 0x2d, 0x0b, 0x04, 0x54, 0xe0, 0x94, 0x7a, 0xb6, 0x68, 0x61, 0x57, - 0x6d, 0xf5, 0x93, 0x24, 0x4e, 0x92, 0x8d, 0x3e, 0xad, 0x99, 0x0b, 0xb8, 0x63, 0x43, 0x44, 0x5a, 0x1b, 0xf2, 0xcd, - 0xfe, 0xb7, 0x7f, 0xfe, 0x9f, 0xff, 0x15, 0x06, 0xa6, 0x7e, 0x6e, 0x69, 0x5d, 0xdd, 0xea, 0x7f, 0x40, 0xab, 0x45, - 0x7a, 0x43, 0xbb, 0xbf, 0xfe, 0xe3, 0x7f, 0x83, 0x66, 0x74, 0x23, 0xc7, 0x2d, 0x8f, 0x08, 0xa2, 0x11, 0x1a, 0x41, - 0x9f, 0x05, 0x52, 0x6d, 0x90, 0x2b, 0x67, 0xfa, 0x27, 0x04, 0xbb, 0xe0, 0xd9, 0xfc, 0x5a, 0x70, 0x10, 0xea, 0x51, - 0x92, 0x15, 0x4c, 0xc3, 0x23, 0x64, 0xed, 0xe7, 0x01, 0x44, 0x73, 0xcd, 0x81, 0xcb, 0x0b, 0x4b, 0x8f, 0x23, 0x96, - 0x67, 0xdd, 0x38, 0x8d, 0xd5, 0x2b, 0x18, 0x27, 0x74, 0x28, 0xae, 0x00, 0xeb, 0x25, 0x9e, 0xe0, 0x81, 0x04, 0x82, - 0x5b, 0xff, 0xca, 0xd7, 0xfa, 0xc1, 0x34, 0x7f, 0x8a, 0xb1, 0x44, 0x28, 0x45, 0x8d, 0x00, 0x3f, 0x41, 0x68, 0x7d, - 0xd4, 0xcf, 0xd1, 0xb9, 0x7e, 0x46, 0xc1, 0x26, 0x26, 0x00, 0x5d, 0x34, 0x43, 0x33, 0xc3, 0x9c, 0x41, 0xa4, 0x01, - 0x54, 0x7e, 0xa4, 0x91, 0x4d, 0x22, 0x84, 0xd7, 0x47, 0x4c, 0xba, 0xc4, 0x2b, 0x36, 0x8b, 0x9c, 0x7d, 0x4c, 0xb2, - 0x33, 0x0c, 0x4e, 0x21, 0x91, 0xae, 0xaa, 0x2f, 0xff, 0xf5, 0x5f, 0x7c, 0xff, 0x5f, 0xff, 0xe5, 0x8a, 0x06, 0x53, - 0xd8, 0x07, 0x60, 0x4d, 0xf2, 0x50, 0xd3, 0xb9, 0x81, 0xd6, 0xfa, 0x41, 0x11, 0xcf, 0xf5, 0x35, 0x12, 0x71, 0x2c, - 0x95, 0x78, 0xcb, 0x47, 0x42, 0x5b, 0x33, 0xc5, 0xcd, 0xb3, 0x20, 0x64, 0x57, 0x4c, 0x83, 0x55, 0x37, 0xcc, 0x73, - 0xe4, 0x06, 0xd7, 0xd0, 0xe5, 0x33, 0x31, 0x5e, 0x0f, 0xc6, 0x8d, 0x10, 0x78, 0x20, 0xfd, 0x85, 0x7e, 0x78, 0x22, - 0xa4, 0x7b, 0x20, 0x96, 0x41, 0xca, 0xfa, 0x1a, 0x40, 0x9e, 0x75, 0x40, 0x13, 0x50, 0x93, 0xb8, 0xd2, 0xad, 0x64, - 0x8a, 0x1c, 0xe7, 0xfd, 0x57, 0x78, 0xab, 0xce, 0xc2, 0xde, 0xa8, 0x59, 0x0b, 0x32, 0x04, 0x38, 0x19, 0x02, 0x52, - 0x03, 0x99, 0x3a, 0x18, 0x3d, 0x8c, 0x6c, 0xbd, 0xae, 0xfd, 0x88, 0xdd, 0x6b, 0xda, 0x92, 0x4b, 0x6d, 0x19, 0x4b, - 0x4b, 0x56, 0x6a, 0x4b, 0xfc, 0x76, 0x4a, 0x43, 0x5b, 0xc6, 0x57, 0x6a, 0x4b, 0xa4, 0xdc, 0x00, 0x47, 0x0e, 0xed, - 0x4d, 0x0c, 0xa3, 0x18, 0xba, 0x99, 0xa3, 0x5d, 0x02, 0xbe, 0xf3, 0xe8, 0x93, 0x34, 0x4b, 0x08, 0x01, 0x8c, 0x23, - 0x68, 0x43, 0x4b, 0x60, 0x00, 0x2a, 0xf6, 0xa8, 0xd4, 0x9b, 0x1e, 0x1f, 0x8d, 0x09, 0xb8, 0xbb, 0x9c, 0x30, 0x14, - 0xc9, 0x47, 0x5b, 0x38, 0x84, 0xd8, 0x2b, 0x25, 0x3d, 0x03, 0x52, 0x5b, 0x38, 0xce, 0x91, 0xb7, 0x14, 0x01, 0xa9, - 0xc0, 0x7e, 0xfb, 0x66, 0xff, 0xc0, 0xf6, 0x8e, 0xb3, 0xf1, 0x45, 0x60, 0x83, 0x33, 0x01, 0x86, 0x87, 0xeb, 0xf3, - 0x29, 0x4b, 0x1d, 0x65, 0xce, 0x67, 0x09, 0xb8, 0x33, 0xd9, 0x89, 0xf8, 0x7e, 0x42, 0x33, 0x98, 0x0e, 0x34, 0xa5, - 0x0f, 0x2c, 0xf6, 0x77, 0xb9, 0xf8, 0xf6, 0x28, 0xcf, 0xf1, 0xb1, 0x8f, 0xe9, 0x04, 0xbb, 0x5b, 0xf0, 0x80, 0x2f, - 0xfb, 0xa8, 0x8a, 0xf4, 0x9b, 0x80, 0xb3, 0x10, 0xef, 0x5b, 0xd8, 0x7e, 0x4b, 0xf5, 0x45, 0x28, 0xfa, 0x92, 0xd1, - 0xb4, 0xc5, 0x5d, 0x99, 0x71, 0x34, 0xf6, 0x18, 0xad, 0x34, 0xb2, 0xb8, 0x81, 0x1a, 0x7c, 0xac, 0x4b, 0x84, 0xe6, - 0x37, 0x8a, 0x68, 0x94, 0x4a, 0x4f, 0xcb, 0x2a, 0x9c, 0x90, 0x2c, 0x3b, 0x31, 0x19, 0xfc, 0x24, 0xf0, 0x8f, 0xcc, - 0x6f, 0x85, 0x89, 0xcf, 0xfa, 0x68, 0x24, 0x0f, 0xff, 0xec, 0x7d, 0x64, 0xde, 0xc5, 0x11, 0xb5, 0x54, 0x8e, 0x29, - 0x46, 0x4c, 0xd0, 0x81, 0x6f, 0xab, 0x08, 0x04, 0x98, 0x26, 0x49, 0x34, 0x2f, 0x58, 0xa0, 0x1e, 0xa4, 0x8f, 0x8a, - 0xae, 0xee, 0x6a, 0x50, 0xc0, 0x34, 0x61, 0x4a, 0x3e, 0x5d, 0x9a, 0x4e, 0xec, 0x03, 0x70, 0x62, 0x31, 0x01, 0xbf, - 0x15, 0x81, 0xe2, 0x4d, 0x83, 0x84, 0x4d, 0x78, 0xc9, 0xf1, 0x86, 0xf7, 0x52, 0x45, 0x0d, 0xfc, 0xee, 0x0e, 0x38, - 0xb6, 0x96, 0x8f, 0xff, 0xdf, 0x34, 0xf6, 0x38, 0x48, 0xc1, 0x11, 0xa5, 0x2b, 0x1f, 0x78, 0x9d, 0x0e, 0x20, 0x32, - 0xdf, 0x97, 0xc6, 0x44, 0x23, 0x86, 0x11, 0x95, 0x92, 0xe7, 0x20, 0xb2, 0x3d, 0x9e, 0x9b, 0xed, 0x40, 0xd4, 0xae, - 0x84, 0x55, 0x56, 0x1d, 0xfb, 0x6d, 0x57, 0x9a, 0xff, 0xab, 0x8d, 0x55, 0x74, 0xa4, 0xfe, 0xb6, 0x42, 0x21, 0x23, - 0x8e, 0x53, 0x0a, 0x2d, 0xb3, 0x14, 0x3d, 0x4c, 0x9c, 0x56, 0x23, 0x3c, 0x37, 0x1a, 0x89, 0x25, 0xed, 0xf8, 0x43, - 0xda, 0xf1, 0x24, 0xc1, 0x86, 0x4b, 0x31, 0xf7, 0x28, 0x4a, 0x46, 0x0e, 0x02, 0x60, 0xb5, 0xac, 0x47, 0x40, 0x4d, - 0x57, 0x45, 0x19, 0xfc, 0x87, 0x48, 0xdc, 0x52, 0xc8, 0xbb, 0x35, 0x54, 0x3a, 0x1a, 0x96, 0x65, 0xef, 0x8c, 0x39, - 0x87, 0xbf, 0xc9, 0x0b, 0x03, 0xe2, 0x9e, 0xa8, 0xfe, 0xd6, 0x5e, 0xbb, 0x74, 0x87, 0xde, 0x5f, 0x8c, 0x0f, 0x98, - 0xd9, 0x8a, 0xa1, 0x6d, 0x0f, 0x96, 0xe1, 0x2f, 0x21, 0xf6, 0x7d, 0xe5, 0xd8, 0x68, 0x55, 0x52, 0xcd, 0x45, 0x8b, - 0xf8, 0xcb, 0xc6, 0x6e, 0x22, 0xdc, 0xfd, 0xfd, 0x55, 0x51, 0x8b, 0x6f, 0x6e, 0x8e, 0x5a, 0xb0, 0x5b, 0x46, 0x2d, - 0xbe, 0xf9, 0x83, 0xa3, 0x16, 0xdf, 0x37, 0xa3, 0x16, 0xbf, 0x7e, 0x4e, 0xd4, 0x22, 0xcf, 0xce, 0x8a, 0xb0, 0x23, - 0x4f, 0xc9, 0x41, 0xe6, 0xfc, 0x6d, 0xc2, 0x17, 0x30, 0x51, 0x23, 0x78, 0x41, 0xd1, 0x0a, 0x91, 0xd8, 0x07, 0x92, - 0x5d, 0xc6, 0x0a, 0xda, 0x3a, 0x83, 0xae, 0x75, 0x5f, 0x5d, 0x19, 0x02, 0x6f, 0xcd, 0xd5, 0x97, 0xdd, 0xba, 0x2a, - 0x9a, 0x10, 0xd0, 0x37, 0x3f, 0x75, 0xc7, 0xee, 0xa6, 0x4a, 0xdd, 0x32, 0x47, 0xe8, 0xa9, 0x88, 0xbc, 0x60, 0x9f, - 0xa5, 0xfd, 0x9f, 0x0e, 0x3b, 0xbd, 0xed, 0xce, 0x0c, 0x7a, 0x83, 0x0e, 0x85, 0xb7, 0x76, 0x6f, 0x7b, 0x1b, 0xdf, - 0xce, 0xd4, 0x5b, 0x17, 0xdf, 0x62, 0xf5, 0xb6, 0x83, 0x6f, 0x23, 0xf5, 0xf6, 0x00, 0xdf, 0xc6, 0xea, 0xed, 0x21, - 0xbe, 0x9d, 0xda, 0xe5, 0x21, 0xd7, 0xc0, 0x3d, 0x04, 0xbe, 0x22, 0x43, 0x3f, 0x50, 0x65, 0xb0, 0x69, 0xf1, 0xaa, - 0x5d, 0x74, 0x12, 0xc4, 0x9e, 0x70, 0x88, 0x82, 0xdc, 0x3b, 0x03, 0xc9, 0x1f, 0x50, 0x66, 0xd9, 0x53, 0xfc, 0xe6, - 0x02, 0xf8, 0x0f, 0x07, 0xf1, 0x8c, 0xa9, 0x8f, 0xcf, 0x2a, 0xac, 0xc1, 0x86, 0x3c, 0x6c, 0x0f, 0xcb, 0x9e, 0x5e, - 0x27, 0x11, 0x2c, 0x51, 0x27, 0xf7, 0xb4, 0x72, 0x55, 0x9d, 0x98, 0xae, 0xa5, 0x57, 0xf8, 0x0a, 0x3d, 0x62, 0xb8, - 0xd0, 0x13, 0xb0, 0x8d, 0x5a, 0xe7, 0xe0, 0x74, 0xad, 0xd5, 0x2d, 0x08, 0x91, 0xd6, 0x26, 0x84, 0x93, 0x7e, 0x3b, - 0x88, 0x4e, 0xf4, 0xf3, 0x2b, 0x30, 0x76, 0xa3, 0x13, 0x76, 0x93, 0x9e, 0x21, 0x10, 0x4d, 0x1d, 0xa3, 0x80, 0x20, - 0x6b, 0x08, 0x96, 0x06, 0x9d, 0x3f, 0xa9, 0x63, 0x90, 0x3a, 0x75, 0xad, 0x43, 0xd3, 0xd7, 0x8b, 0x80, 0xa2, 0x55, - 0xc1, 0x2e, 0xd8, 0xdc, 0x54, 0x2a, 0x28, 0x0c, 0x15, 0x58, 0x70, 0xad, 0x2a, 0xd2, 0xfe, 0xf1, 0x95, 0x0a, 0xc9, - 0x52, 0xba, 0xc8, 0x8c, 0xe4, 0xeb, 0x30, 0xfe, 0xaa, 0x78, 0xfc, 0xa2, 0x33, 0xc2, 0x3f, 0x52, 0xf8, 0x7e, 0x31, - 0x99, 0x4c, 0xae, 0xd5, 0x4d, 0x5f, 0x8c, 0x27, 0xac, 0xcb, 0x76, 0x7a, 0x18, 0xe5, 0x6d, 0x49, 0x71, 0xd8, 0x29, - 0x89, 0x76, 0xcb, 0xdb, 0x35, 0x46, 0xc9, 0x09, 0xea, 0xea, 0xf6, 0x4a, 0xac, 0x04, 0xaa, 0x2c, 0x41, 0x78, 0x9f, - 0xc4, 0x69, 0xd0, 0x2e, 0xfd, 0x53, 0x29, 0xf5, 0xbf, 0x78, 0xf4, 0xe8, 0x51, 0xe9, 0x8f, 0xd5, 0x5b, 0x7b, 0x3c, - 0x2e, 0xfd, 0xd1, 0x52, 0xa3, 0xd1, 0x6e, 0x4f, 0x26, 0xa5, 0x1f, 0xab, 0x82, 0xed, 0xee, 0x68, 0xbc, 0xdd, 0x2d, - 0xfd, 0x33, 0xa3, 0x45, 0xe9, 0x33, 0xf9, 0x96, 0xb3, 0x71, 0x2d, 0x54, 0xfc, 0xb0, 0x0d, 0x95, 0x82, 0xd1, 0x96, - 0xe8, 0xe0, 0x89, 0xc7, 0x20, 0x5a, 0xf0, 0x0c, 0x6c, 0xab, 0xb2, 0xc7, 0x40, 0x3e, 0x4f, 0xa4, 0x6c, 0x17, 0xdf, - 0x76, 0x45, 0x89, 0xfe, 0xab, 0x29, 0xd1, 0x91, 0x99, 0x49, 0x9a, 0x33, 0xd2, 0x03, 0xcd, 0x6a, 0xe4, 0x2c, 0xaa, - 0xfe, 0x35, 0x64, 0x95, 0xb0, 0x47, 0x69, 0x83, 0x2d, 0x85, 0x8c, 0xff, 0xf6, 0x2a, 0x19, 0xff, 0xdd, 0xcd, 0x32, - 0xfe, 0xf8, 0x76, 0x22, 0xfe, 0xbb, 0x3f, 0x58, 0xc4, 0x7f, 0x6b, 0x8a, 0x78, 0x21, 0xc4, 0x2e, 0xc0, 0x7a, 0x25, - 0xb3, 0xf5, 0x38, 0x3b, 0x6f, 0xe1, 0x96, 0xc8, 0x6d, 0x92, 0x9e, 0x1b, 0xb7, 0x12, 0xfe, 0x6b, 0x72, 0x7f, 0xd4, - 0x60, 0xc6, 0x87, 0x62, 0x79, 0x76, 0x72, 0x92, 0x30, 0x25, 0xe3, 0x8d, 0x0a, 0xb2, 0x88, 0xdf, 0xa4, 0xa1, 0xfd, - 0x06, 0x9c, 0x53, 0xa3, 0x64, 0x32, 0x81, 0xa2, 0xc9, 0xc4, 0x56, 0xb9, 0xb1, 0x20, 0xcf, 0xa8, 0xd5, 0xeb, 0x5a, - 0x09, 0xb5, 0xfa, 0xfa, 0x6b, 0xb3, 0xcc, 0x2c, 0x90, 0x51, 0x28, 0xd3, 0x98, 0x90, 0x35, 0xe3, 0xb8, 0xc0, 0x3d, - 0x58, 0x7d, 0xd8, 0x16, 0xed, 0x95, 0x19, 0x28, 0x95, 0x78, 0x84, 0x5f, 0x4c, 0x69, 0x7e, 0x44, 0x44, 0xe4, 0x31, - 0xaf, 0x22, 0x57, 0x9d, 0x75, 0x1a, 0xdf, 0xab, 0xab, 0xce, 0x37, 0x61, 0xf1, 0x65, 0x2e, 0xc3, 0xe3, 0x8b, 0x17, - 0x63, 0xe7, 0x02, 0xec, 0xd8, 0xb8, 0x78, 0x93, 0x36, 0x72, 0xc4, 0x04, 0xd8, 0x61, 0x68, 0x62, 0x5a, 0x0a, 0x82, - 0x55, 0xc9, 0xf2, 0x55, 0x65, 0xcf, 0xe8, 0x24, 0x53, 0x89, 0x70, 0xc8, 0x41, 0x8d, 0x2c, 0x81, 0x39, 0x98, 0xd4, - 0x85, 0xf4, 0xe1, 0x72, 0x91, 0x60, 0x71, 0x2a, 0xbf, 0x70, 0x4d, 0x91, 0xff, 0xa5, 0xd4, 0x1f, 0xf2, 0xe8, 0xbd, - 0xea, 0x89, 0xc1, 0x76, 0x31, 0xc3, 0xb8, 0x54, 0x01, 0x76, 0x20, 0xdc, 0x1c, 0x3f, 0xc7, 0x23, 0x86, 0x50, 0x71, - 0xec, 0x8a, 0x7a, 0xf8, 0xf9, 0x93, 0xea, 0xab, 0x90, 0xb5, 0x2f, 0x08, 0x36, 0x78, 0x00, 0xbf, 0xec, 0xcf, 0x51, - 0x1b, 0x64, 0x0b, 0xee, 0x38, 0xd4, 0xca, 0x71, 0x4b, 0xaf, 0xbb, 0xd3, 0x06, 0x15, 0xe3, 0x8b, 0x6f, 0xfe, 0x38, - 0xba, 0xb3, 0xc4, 0xf7, 0xaa, 0xb0, 0xf9, 0xca, 0x37, 0xb8, 0x34, 0x89, 0xf1, 0x23, 0x21, 0x02, 0x51, 0xe3, 0x9e, - 0x88, 0x5a, 0xc4, 0xe6, 0xbb, 0xaf, 0xdc, 0x37, 0x83, 0xb0, 0xee, 0x3a, 0x0e, 0x96, 0x31, 0xb2, 0x7a, 0x21, 0xb6, - 0x15, 0x56, 0xcd, 0x2a, 0x38, 0x37, 0xe8, 0xcc, 0xe2, 0xcc, 0x88, 0x39, 0xd7, 0xb6, 0x41, 0xa9, 0x82, 0xce, 0x22, - 0x72, 0x7c, 0x81, 0xf1, 0x51, 0xe1, 0xfb, 0x2a, 0xa0, 0xeb, 0x5e, 0xa7, 0x01, 0x39, 0xfa, 0xa3, 0x9a, 0xd1, 0x55, - 0x95, 0x2a, 0x28, 0xcd, 0x13, 0x02, 0x03, 0x19, 0x0a, 0xfe, 0xc2, 0x1a, 0xa7, 0x42, 0x6f, 0xc1, 0x34, 0x24, 0x80, - 0xb5, 0x47, 0x86, 0x6e, 0x89, 0xad, 0xc0, 0x16, 0xd2, 0x02, 0x94, 0x1e, 0x76, 0xe8, 0x5b, 0x35, 0xd0, 0xd3, 0xd5, - 0x98, 0xf1, 0x75, 0x4e, 0xda, 0xc5, 0x91, 0x5f, 0x9c, 0x79, 0xf0, 0xcf, 0xfa, 0x72, 0x09, 0x52, 0xfe, 0xf8, 0x53, - 0xcc, 0xc1, 0xa6, 0x9e, 0xb7, 0x30, 0x02, 0x42, 0x21, 0x4c, 0xa9, 0x0e, 0xe9, 0xd8, 0x51, 0x5c, 0x0f, 0xea, 0x2d, - 0x0a, 0xf4, 0xe5, 0xc8, 0x69, 0x09, 0xd2, 0x2c, 0x65, 0xbd, 0xfa, 0xf1, 0xb2, 0xe9, 0x37, 0x28, 0x62, 0x0d, 0x97, - 0x19, 0xfa, 0x7e, 0xfc, 0x02, 0x7c, 0x3f, 0xa1, 0x46, 0xdb, 0xca, 0x69, 0x68, 0xaf, 0x6d, 0x1f, 0x48, 0xda, 0x6e, - 0x92, 0xb5, 0x90, 0xaf, 0x3a, 0x47, 0x57, 0x39, 0x37, 0x37, 0x1d, 0xb6, 0x76, 0x77, 0x76, 0x3c, 0xf5, 0xcf, 0x38, - 0xa5, 0x6e, 0x16, 0xd3, 0x61, 0xeb, 0x6d, 0x20, 0x0b, 0xa2, 0x09, 0x7e, 0x4d, 0xef, 0x36, 0x2d, 0x8f, 0x29, 0xd3, - 0x71, 0x89, 0x6a, 0x3d, 0xe8, 0x3c, 0x02, 0x6f, 0xed, 0xd6, 0xc3, 0x5f, 0x8f, 0x7e, 0x29, 0x69, 0xa4, 0x2e, 0xac, - 0xda, 0x76, 0x0f, 0xe5, 0x45, 0x12, 0x5d, 0x80, 0xd3, 0x48, 0x36, 0xc6, 0x31, 0x06, 0x70, 0x7b, 0xf3, 0x4c, 0x66, - 0x0d, 0xe4, 0x2c, 0xa1, 0x5f, 0x59, 0x21, 0x97, 0x62, 0xfb, 0xc1, 0xfc, 0x5c, 0xad, 0x46, 0xa7, 0x91, 0x0d, 0xf0, - 0x87, 0x1e, 0xfa, 0x5f, 0x9d, 0x65, 0x50, 0x3f, 0xb8, 0xde, 0x01, 0x18, 0x84, 0x61, 0xd3, 0xca, 0x05, 0x54, 0x6d, - 0x28, 0x31, 0xd2, 0x1e, 0xaa, 0x81, 0x2c, 0x7f, 0x1b, 0x54, 0x65, 0x54, 0xb0, 0x1e, 0x7e, 0xd6, 0x30, 0x06, 0xd7, - 0x54, 0x1a, 0x4f, 0xb3, 0x78, 0x3c, 0x4e, 0x58, 0x4f, 0xd9, 0x47, 0x56, 0xe7, 0x01, 0x66, 0x0d, 0x98, 0x4b, 0x56, - 0x5f, 0x15, 0x83, 0x78, 0x9a, 0x4e, 0xd1, 0x31, 0xd8, 0x6b, 0xf8, 0x6d, 0xc2, 0xb5, 0xe4, 0x94, 0xc7, 0xe9, 0xed, - 0x8a, 0x78, 0xf4, 0x5c, 0xc7, 0x65, 0x07, 0x8c, 0x45, 0x5a, 0xf0, 0x76, 0x8f, 0x67, 0xf3, 0xa0, 0xb5, 0x5d, 0x47, - 0x04, 0xab, 0x34, 0x0a, 0xde, 0x1a, 0xb4, 0x3c, 0xb4, 0x0e, 0x84, 0x96, 0xb3, 0xfc, 0x8e, 0x2c, 0xa3, 0x01, 0xf0, - 0xfb, 0x77, 0xba, 0xa8, 0xac, 0x23, 0xf3, 0xff, 0x67, 0xb7, 0x7c, 0xb5, 0x7e, 0xb7, 0x7c, 0xa5, 0x76, 0xcb, 0xf5, - 0x1c, 0xfb, 0xc5, 0xa4, 0x83, 0x7f, 0x7a, 0x15, 0x42, 0xb0, 0x2a, 0x40, 0x0e, 0x0b, 0xed, 0xe2, 0x56, 0x17, 0xfe, - 0xa3, 0xa1, 0xdb, 0x1e, 0xfe, 0xf1, 0xc1, 0x02, 0x6c, 0x5b, 0x58, 0x88, 0xff, 0xda, 0xb5, 0xaa, 0xce, 0x7d, 0xac, - 0xc3, 0x5e, 0x3b, 0xab, 0x75, 0xdd, 0xeb, 0x37, 0x2d, 0xc8, 0x2b, 0xee, 0x04, 0x4a, 0x18, 0x83, 0xab, 0x16, 0x1d, - 0x1f, 0x43, 0xe9, 0x24, 0x1b, 0x2d, 0x8a, 0xbf, 0x97, 0xf0, 0x4b, 0x22, 0x5e, 0xbb, 0xa5, 0x1b, 0xe3, 0xa8, 0xae, - 0x22, 0x05, 0x45, 0x8d, 0xb0, 0xd4, 0xeb, 0x14, 0x14, 0xc0, 0x98, 0xcc, 0xe9, 0xfa, 0xf7, 0xd7, 0x6c, 0x82, 0xbf, - 0xc9, 0xda, 0xac, 0x45, 0xe6, 0xdf, 0x4b, 0x8c, 0x6b, 0x89, 0xf0, 0x59, 0x34, 0x30, 0xd7, 0xb0, 0xfd, 0x68, 0x3d, - 0xb8, 0x87, 0x6a, 0xa6, 0xa1, 0x52, 0x0a, 0x52, 0xef, 0x80, 0x17, 0x10, 0x2d, 0x12, 0x7e, 0xfd, 0xa8, 0x57, 0x71, - 0xc6, 0xca, 0xa8, 0xd7, 0x08, 0xf4, 0xaa, 0xed, 0x2d, 0xa5, 0xf4, 0x17, 0x5f, 0xdd, 0xc7, 0x3f, 0x22, 0xf0, 0x75, - 0x5c, 0xf9, 0x46, 0x22, 0x36, 0x80, 0xbe, 0xd1, 0x46, 0xcd, 0xf9, 0x11, 0x1a, 0x9c, 0xfc, 0x9f, 0xdb, 0xb6, 0x46, - 0x63, 0xfd, 0x56, 0xcd, 0xa5, 0x55, 0xfa, 0x59, 0xad, 0x3f, 0x6f, 0xf0, 0x5b, 0xb6, 0x1d, 0x09, 0x87, 0xa0, 0xde, - 0x56, 0xfe, 0xfa, 0x90, 0x95, 0xc6, 0x8a, 0xe2, 0xb7, 0x6d, 0x5f, 0x99, 0xc4, 0xd4, 0x63, 0x23, 0x3c, 0xd6, 0x4e, - 0xa4, 0x3c, 0x6f, 0xc6, 0x1e, 0xc2, 0x8f, 0xfc, 0x91, 0x85, 0xf7, 0xf0, 0xcb, 0x5b, 0xd6, 0xf9, 0x2c, 0x49, 0xc1, + 0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc5, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36, + 0x43, 0x41, 0xe1, 0x78, 0xf3, 0x80, 0x05, 0xd3, 0x7e, 0xd8, 0x1e, 0x38, 0xb9, 0x50, 0x1d, 0x09, 0x9e, 0x5b, 0x0a, + 0x09, 0xde, 0xf6, 0xa6, 0x20, 0xd0, 0x91, 0x73, 0x37, 0xec, 0x4d, 0x55, 0x08, 0x45, 0x1f, 0x37, 0xc7, 0x6e, 0x10, + 0xc3, 0x0f, 0xa7, 0x85, 0x4c, 0x33, 0xd5, 0x7d, 0xb5, 0x66, 0x76, 0x83, 0xb1, 0xb2, 0xc8, 0x93, 0x30, 0xdb, 0x74, + 0x30, 0x42, 0x0b, 0x92, 0x76, 0x77, 0x00, 0x30, 0x6c, 0x3a, 0x8a, 0xd3, 0xb6, 0x14, 0xab, 0x29, 0xfb, 0xfc, 0x50, + 0x2f, 0xc7, 0x94, 0x0d, 0xa6, 0xcc, 0xaf, 0xb4, 0x0f, 0x80, 0x15, 0x24, 0x5e, 0x3e, 0x54, 0x67, 0x5e, 0xcf, 0x6b, + 0xe7, 0x5b, 0x4b, 0x25, 0x8a, 0x98, 0x67, 0x48, 0x28, 0x5e, 0x6a, 0x37, 0x4c, 0x98, 0xdb, 0x73, 0x24, 0x86, 0x66, + 0xf9, 0xb0, 0x0d, 0x4c, 0xaf, 0x02, 0xec, 0xa9, 0xb9, 0x2d, 0x92, 0xb0, 0x6a, 0xee, 0x1d, 0x02, 0x6b, 0x0f, 0x81, + 0x87, 0x68, 0x1b, 0xf5, 0x54, 0x34, 0x9f, 0x25, 0xe1, 0xf3, 0xc6, 0x71, 0x71, 0x84, 0x27, 0x42, 0xfb, 0xfe, 0x68, + 0x91, 0x83, 0x3c, 0xe0, 0xaf, 0xc1, 0x32, 0x08, 0x65, 0x53, 0x74, 0xf4, 0xf0, 0x08, 0xd8, 0x23, 0xc4, 0x1b, 0x61, + 0x73, 0xa3, 0x1a, 0x2d, 0x4a, 0x32, 0x5e, 0xe8, 0x60, 0xb8, 0xc7, 0xa5, 0x6b, 0x8f, 0x82, 0x41, 0x9e, 0x18, 0x3b, + 0x78, 0xe6, 0xef, 0x8f, 0xb0, 0x1a, 0x27, 0x28, 0xdc, 0x92, 0x76, 0x5b, 0x25, 0xfe, 0xf6, 0xfd, 0x14, 0x24, 0x38, + 0xd6, 0x81, 0x9f, 0x75, 0xf7, 0x6e, 0x22, 0x91, 0xda, 0x4d, 0x7b, 0x74, 0x12, 0x81, 0xf1, 0xe0, 0xdc, 0x4f, 0xa1, + 0x1a, 0x49, 0x44, 0x45, 0x39, 0x5a, 0xa0, 0xe6, 0xa9, 0x5a, 0x05, 0xdf, 0xa1, 0x19, 0x81, 0xe7, 0x18, 0xb6, 0x26, + 0x3f, 0x55, 0x37, 0x16, 0xb1, 0x7c, 0xd7, 0xa5, 0xa3, 0x2d, 0x3c, 0x80, 0x14, 0x8c, 0x26, 0x18, 0xc6, 0xa5, 0xa0, + 0x64, 0xc5, 0x7f, 0x1f, 0x8d, 0x58, 0xf9, 0xf4, 0x30, 0xdb, 0xdc, 0x1c, 0x8a, 0x73, 0x0b, 0x62, 0x1c, 0x6e, 0x44, + 0x57, 0xe3, 0x0a, 0x80, 0xfa, 0x74, 0x4e, 0x5c, 0x0f, 0x4c, 0x2b, 0xd6, 0x74, 0x29, 0xf6, 0xc9, 0x61, 0x06, 0xa0, + 0xe0, 0x96, 0x73, 0xe8, 0x0f, 0xfe, 0x3c, 0x04, 0xf7, 0xd8, 0xff, 0x93, 0xbb, 0xa5, 0x04, 0x4d, 0x4f, 0x9e, 0x29, + 0x2e, 0xe9, 0x8c, 0xb5, 0xe3, 0x51, 0x6c, 0x34, 0x28, 0xbc, 0x14, 0x30, 0x00, 0x6d, 0x0e, 0x32, 0xa1, 0xe2, 0x20, + 0xe4, 0xa8, 0xc0, 0xf6, 0x71, 0xf3, 0x73, 0xdc, 0xd9, 0x4f, 0xc1, 0xc2, 0x1b, 0xe8, 0xb7, 0x97, 0xf0, 0xf6, 0x67, + 0xfd, 0xf6, 0x0b, 0x0b, 0x7e, 0x29, 0x65, 0xe8, 0xbe, 0x36, 0xc5, 0x03, 0x35, 0x45, 0x29, 0x96, 0xc8, 0xa0, 0x21, + 0x73, 0xf3, 0x95, 0x98, 0x0d, 0x77, 0x4b, 0x20, 0x86, 0x12, 0x5d, 0xb9, 0xcf, 0xa3, 0x13, 0x24, 0xae, 0x6b, 0x92, + 0xc2, 0xc8, 0x25, 0x30, 0x11, 0xae, 0xf8, 0x96, 0x98, 0xb3, 0xdf, 0x06, 0x1b, 0xbc, 0x96, 0x77, 0x80, 0xf6, 0x1d, + 0x9b, 0xcd, 0xf9, 0xc5, 0x3e, 0x29, 0xfa, 0x40, 0xa6, 0x0d, 0x88, 0xb3, 0xf3, 0x76, 0x2f, 0xde, 0xe5, 0xbd, 0x18, + 0xa4, 0x7a, 0xae, 0x58, 0x0c, 0xf7, 0xaa, 0xf7, 0x16, 0xa3, 0x94, 0x26, 0x33, 0x79, 0x35, 0xf4, 0xba, 0x12, 0xbd, + 0xcd, 0x4d, 0x40, 0xb0, 0x67, 0x74, 0xe5, 0xa2, 0x6b, 0x59, 0x0a, 0x9a, 0x00, 0x44, 0x8f, 0xea, 0x2c, 0x47, 0x1c, + 0x87, 0xd9, 0x6c, 0x50, 0x3c, 0x62, 0xee, 0xda, 0x51, 0x71, 0x4c, 0xec, 0x2e, 0x13, 0x76, 0x00, 0x33, 0xe2, 0xf2, + 0x56, 0x47, 0x44, 0x87, 0x45, 0x7f, 0x1d, 0xdf, 0xfe, 0xe8, 0xb1, 0xcd, 0x8e, 0x0b, 0x1a, 0xa4, 0x36, 0xd6, 0xc3, + 0x6a, 0x2c, 0xa8, 0x0f, 0x3f, 0x6a, 0x2a, 0x95, 0xc5, 0xe6, 0x66, 0x59, 0x3f, 0xaa, 0x55, 0x3b, 0xb8, 0x76, 0x9a, + 0x72, 0xde, 0xcc, 0x06, 0xe1, 0x40, 0xc4, 0x04, 0x0a, 0xb4, 0xb4, 0xb2, 0x62, 0x80, 0x21, 0x65, 0x39, 0xca, 0xa7, + 0x90, 0x79, 0x71, 0x59, 0xea, 0xd4, 0x17, 0x19, 0x8f, 0x0c, 0xf1, 0xd4, 0x93, 0x8c, 0x15, 0x50, 0xb0, 0x5e, 0xea, + 0x25, 0xb4, 0x44, 0x80, 0xf9, 0x33, 0x95, 0x43, 0x23, 0x2c, 0x90, 0x28, 0x34, 0xcc, 0x12, 0x65, 0x7c, 0x16, 0x61, + 0x0c, 0xda, 0xfe, 0x49, 0x2d, 0xf6, 0x55, 0x28, 0xa3, 0xa3, 0x38, 0xcc, 0x87, 0x01, 0xd5, 0x2f, 0xa4, 0x04, 0x9b, + 0x86, 0xef, 0x81, 0x8d, 0x2a, 0xc7, 0x93, 0x04, 0xe1, 0xd3, 0x38, 0x67, 0xe4, 0x29, 0x6c, 0x48, 0x98, 0xa5, 0x69, + 0x1b, 0xa9, 0x76, 0x91, 0x19, 0x84, 0x72, 0x51, 0xf0, 0x1a, 0x67, 0x17, 0x59, 0xb8, 0xd2, 0x1a, 0xcc, 0x8f, 0x37, + 0x26, 0x40, 0xd9, 0xe5, 0x65, 0x26, 0x7c, 0xdc, 0x88, 0xec, 0x0d, 0x5d, 0x31, 0x1d, 0x28, 0xa4, 0x02, 0x27, 0x22, + 0x8b, 0x87, 0xce, 0x50, 0x68, 0x84, 0x03, 0x3a, 0x45, 0xce, 0x5d, 0x63, 0xd3, 0xe7, 0x03, 0xed, 0x1b, 0xa5, 0xa1, + 0x93, 0x80, 0x10, 0x10, 0xb8, 0x1b, 0xd6, 0x54, 0x3a, 0x48, 0x83, 0x84, 0x4a, 0xd1, 0xcf, 0x01, 0xfc, 0xc3, 0x48, + 0x52, 0x00, 0xec, 0x87, 0x6a, 0xa4, 0x88, 0xb2, 0x2c, 0x70, 0x01, 0x68, 0xae, 0x7d, 0x5c, 0x09, 0x5f, 0x18, 0xa8, + 0x30, 0x3d, 0xcd, 0xca, 0x4b, 0xa1, 0x44, 0x1e, 0xaf, 0x49, 0x59, 0x23, 0x99, 0x7c, 0x8a, 0x0e, 0x9f, 0xf2, 0xae, + 0x5f, 0x4b, 0x3c, 0x74, 0xc1, 0x53, 0x58, 0x56, 0xf5, 0xfc, 0x2a, 0xe4, 0xe4, 0x5c, 0x83, 0xae, 0x90, 0x42, 0x7f, + 0xc5, 0x49, 0xde, 0x7b, 0xe5, 0x57, 0xb5, 0xd4, 0x18, 0xca, 0xde, 0xaf, 0x6b, 0x86, 0xe5, 0xe5, 0xbc, 0x0a, 0x53, + 0x10, 0x70, 0x4b, 0x96, 0x04, 0x4b, 0xa9, 0x21, 0xc0, 0xc2, 0xf6, 0x48, 0x2b, 0x05, 0x79, 0xa9, 0xc3, 0x3b, 0x4f, + 0xc1, 0x0a, 0x30, 0x0e, 0xb5, 0x54, 0x32, 0x8d, 0x24, 0xbe, 0x54, 0xa2, 0xc0, 0x94, 0xfb, 0x23, 0xf0, 0x53, 0x9b, + 0x27, 0x5d, 0xe7, 0xae, 0x1f, 0xcf, 0x30, 0xb5, 0x87, 0x40, 0x8f, 0xbd, 0x3b, 0x60, 0x4a, 0xd4, 0x75, 0x58, 0x41, + 0x1c, 0x9a, 0xd5, 0x34, 0x0b, 0x98, 0x31, 0x6d, 0xd0, 0x92, 0x6d, 0xb0, 0xe5, 0x72, 0xb0, 0x8f, 0xc4, 0xf6, 0xac, + 0x56, 0x40, 0xe8, 0x1a, 0x34, 0x30, 0xe4, 0x2e, 0x15, 0x5a, 0x98, 0xf7, 0xba, 0x54, 0x84, 0xfb, 0x73, 0xc0, 0xa5, + 0x15, 0x9c, 0x79, 0x19, 0x0d, 0xbc, 0x1f, 0x1f, 0x27, 0x98, 0xf8, 0x82, 0x58, 0x81, 0x1d, 0x1c, 0x74, 0x9a, 0x4d, + 0x81, 0x53, 0x71, 0x91, 0x32, 0x58, 0x56, 0x94, 0xda, 0xf0, 0x43, 0x8a, 0x6c, 0xdd, 0xe5, 0x81, 0xee, 0x42, 0x2c, + 0x80, 0x9d, 0x7e, 0xc3, 0xc8, 0xb7, 0xac, 0x97, 0x01, 0x83, 0x53, 0xad, 0x71, 0x10, 0xf8, 0xcd, 0xcd, 0x64, 0x58, + 0xa6, 0xc4, 0x76, 0x4d, 0x56, 0x17, 0x90, 0xc3, 0x50, 0x4d, 0xdc, 0x41, 0x58, 0x2a, 0x7b, 0xbc, 0x28, 0x67, 0xb8, + 0x5c, 0xca, 0x42, 0x6e, 0x9e, 0x57, 0xd3, 0x7c, 0x6e, 0xa5, 0xd9, 0x74, 0xbc, 0x15, 0x5f, 0x14, 0xfc, 0x03, 0x27, + 0x96, 0x56, 0x3d, 0xa5, 0x56, 0x78, 0x94, 0xb9, 0x25, 0xeb, 0x94, 0xd4, 0xea, 0xba, 0x81, 0x6a, 0x84, 0xa7, 0x69, + 0xd8, 0x08, 0x84, 0x98, 0xe0, 0xe2, 0xd7, 0x4d, 0x26, 0xa6, 0xbd, 0x25, 0xa4, 0x8e, 0xb0, 0x7b, 0x28, 0x27, 0xb8, + 0xab, 0x79, 0xf6, 0x79, 0x38, 0xbf, 0x9a, 0xb9, 0xf7, 0x0c, 0xe6, 0x7e, 0x1c, 0x72, 0x83, 0xd1, 0x63, 0x99, 0xf0, + 0x23, 0x63, 0x1f, 0xb9, 0xaa, 0x7a, 0x72, 0x12, 0x56, 0x22, 0x4b, 0x3c, 0x19, 0x47, 0x1d, 0xc6, 0xa9, 0x68, 0x4d, + 0x90, 0x5d, 0x5e, 0x16, 0xe6, 0x5e, 0xa0, 0xa0, 0xa9, 0xc7, 0xeb, 0x71, 0xda, 0x8a, 0x9d, 0x8d, 0x48, 0xe4, 0xde, + 0xab, 0x5a, 0x24, 0xb2, 0xe2, 0x73, 0x1c, 0xe9, 0x8a, 0x83, 0xdc, 0x27, 0x27, 0xab, 0x9b, 0x54, 0xe8, 0x16, 0x8d, + 0xb6, 0xb1, 0x47, 0xf5, 0x81, 0xa4, 0x9e, 0x51, 0x81, 0x55, 0x8d, 0x7d, 0xf7, 0x6e, 0x47, 0xa4, 0x5b, 0x2a, 0xc5, + 0x06, 0x4b, 0x0b, 0xa3, 0x19, 0xa3, 0x60, 0x50, 0x52, 0x64, 0xa0, 0x46, 0xf9, 0x15, 0x82, 0x61, 0x8f, 0x1a, 0x80, + 0xe2, 0x5c, 0x5f, 0xfd, 0xb8, 0x94, 0x6c, 0x21, 0x20, 0x71, 0x97, 0x0c, 0xc4, 0x9a, 0x60, 0x66, 0xe4, 0x93, 0xf7, + 0xc0, 0x79, 0x03, 0x86, 0x0e, 0x01, 0xf8, 0x05, 0x62, 0xd3, 0x83, 0x89, 0x6d, 0x13, 0x51, 0xf4, 0xd9, 0xc0, 0x73, + 0x00, 0x76, 0x5e, 0x85, 0x46, 0xdf, 0x55, 0x29, 0x60, 0xc8, 0x06, 0x6e, 0xc0, 0xaa, 0xb0, 0xdc, 0xde, 0x73, 0x70, + 0x1b, 0xe0, 0xf5, 0x99, 0x6c, 0xbe, 0x81, 0x79, 0x82, 0xd5, 0xd9, 0x85, 0x5f, 0x59, 0xd6, 0xe2, 0xdc, 0xe9, 0xa0, + 0x51, 0xaf, 0x28, 0x21, 0x6a, 0xf7, 0xb1, 0xf6, 0x39, 0x46, 0x58, 0xc4, 0xfb, 0x2b, 0x7c, 0xd7, 0xe3, 0x96, 0x7b, + 0x1a, 0x2d, 0xc2, 0x74, 0x95, 0x34, 0x06, 0x25, 0xeb, 0x7e, 0x32, 0xe2, 0x5e, 0xee, 0x8b, 0x58, 0x70, 0x85, 0x23, + 0xab, 0x42, 0x8a, 0x0d, 0x24, 0xe9, 0x69, 0x8f, 0x0e, 0xd8, 0x37, 0x9a, 0xbd, 0x80, 0x32, 0xef, 0x2b, 0x52, 0x49, + 0x48, 0x69, 0x76, 0x43, 0x24, 0x09, 0x6b, 0x45, 0x9e, 0x3a, 0xef, 0x3b, 0xda, 0xe7, 0x56, 0x12, 0xc1, 0x08, 0x4e, + 0xc2, 0x74, 0xac, 0x3c, 0x68, 0x0a, 0x70, 0x15, 0x1d, 0x31, 0x7d, 0x13, 0x90, 0xdf, 0x0c, 0xe4, 0xf6, 0x4a, 0x72, + 0x6d, 0xae, 0x61, 0x78, 0x82, 0x04, 0xab, 0x22, 0x11, 0x78, 0x44, 0x8d, 0x09, 0xc9, 0xeb, 0x3c, 0x0f, 0x30, 0xe1, + 0x6b, 0x7b, 0x13, 0x00, 0xca, 0xc9, 0x55, 0x71, 0x56, 0x02, 0xdd, 0x80, 0xe5, 0xfa, 0x38, 0x35, 0x2a, 0x12, 0x17, + 0x37, 0xa6, 0xab, 0x5b, 0xfa, 0x33, 0xb4, 0x9c, 0xc9, 0x10, 0xd3, 0x41, 0x10, 0x90, 0xa9, 0x8f, 0x99, 0x23, 0x64, + 0xae, 0xb0, 0x3e, 0xe7, 0x4e, 0x6d, 0xea, 0x1e, 0xa3, 0x6e, 0x9e, 0xa4, 0x16, 0xaf, 0xd3, 0xa6, 0x94, 0x88, 0x49, + 0x89, 0x39, 0x13, 0xa9, 0xd8, 0x4c, 0x89, 0x3b, 0xb7, 0xbe, 0xd1, 0x42, 0xda, 0x68, 0x33, 0x91, 0x83, 0xcd, 0x2a, + 0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x91, 0xb1, 0x96, 0x63, 0xe6, 0x98, 0x08, 0x56, 0x2f, 0xa6, 0x22, 0x7f, + 0xe7, 0xe8, 0x34, 0x7b, 0x83, 0x1e, 0xa4, 0xde, 0x40, 0x62, 0xd6, 0xc4, 0x77, 0x21, 0x0d, 0x75, 0x84, 0x40, 0x65, + 0x54, 0xcb, 0x74, 0x9c, 0x58, 0x85, 0x6f, 0x04, 0x5f, 0xbd, 0xd5, 0xc7, 0xf9, 0xc6, 0x73, 0x63, 0x35, 0x82, 0x18, + 0xbc, 0x85, 0x7c, 0xe8, 0x49, 0x11, 0x0e, 0x84, 0xcb, 0x37, 0x37, 0x7b, 0xf9, 0x2e, 0xaf, 0x42, 0x24, 0x15, 0x8c, + 0x31, 0x66, 0x14, 0xe3, 0x9e, 0xa8, 0xa9, 0xc5, 0x1c, 0x06, 0x96, 0xad, 0xc3, 0x1c, 0x0f, 0x00, 0xa0, 0xa5, 0x29, + 0xbd, 0x6a, 0x2a, 0x54, 0x9e, 0xe7, 0x12, 0x3e, 0xd5, 0x21, 0xaa, 0x6a, 0xfc, 0x76, 0x7d, 0x06, 0x0a, 0xc1, 0x7d, + 0xa7, 0xe3, 0xe1, 0x21, 0x04, 0xac, 0xa2, 0x90, 0x05, 0x7a, 0x83, 0xf6, 0xaa, 0x44, 0x28, 0x66, 0x4e, 0xd6, 0x63, + 0x86, 0x93, 0x0a, 0xb6, 0x50, 0x09, 0x4b, 0xa5, 0x05, 0x7e, 0xb5, 0x11, 0x9a, 0xa7, 0x8c, 0x7b, 0xaf, 0x2a, 0x9c, + 0x41, 0x7f, 0x30, 0x6f, 0x95, 0x51, 0xdf, 0xae, 0x9c, 0xc8, 0x54, 0x60, 0xe2, 0x66, 0x96, 0xda, 0xef, 0x97, 0x75, + 0xda, 0xcf, 0x2b, 0xe4, 0x3e, 0x27, 0xcd, 0xd7, 0xb9, 0x85, 0xe6, 0x93, 0xe1, 0x7e, 0xa5, 0xfc, 0xd0, 0xc2, 0xa8, + 0x29, 0xbf, 0xbc, 0xae, 0xfc, 0x0a, 0x4f, 0x85, 0xb7, 0xfa, 0x5d, 0x14, 0xba, 0xa8, 0xcf, 0xc1, 0x10, 0xd2, 0x8f, + 0xe0, 0x1a, 0x1a, 0x3c, 0x28, 0x92, 0xc5, 0x62, 0xed, 0x82, 0xb8, 0x3e, 0xe6, 0x54, 0x3b, 0x94, 0x31, 0x46, 0x3c, + 0x2d, 0x39, 0x48, 0x32, 0x38, 0x18, 0xbf, 0x81, 0x01, 0x31, 0x29, 0x09, 0xe9, 0x10, 0x3a, 0x6b, 0x33, 0x11, 0x95, + 0xbb, 0x78, 0xb3, 0x71, 0x59, 0x53, 0x28, 0xc2, 0x4e, 0x30, 0x53, 0x29, 0x15, 0x04, 0xd2, 0xe4, 0xbb, 0xd3, 0xa9, + 0x05, 0x43, 0x0b, 0xd7, 0x54, 0x40, 0x5e, 0xdb, 0xf5, 0xa0, 0xc9, 0x7b, 0x8a, 0xa1, 0xaf, 0x53, 0x23, 0x5e, 0x66, + 0xf0, 0x35, 0x6c, 0xfe, 0x9a, 0x28, 0xc9, 0x43, 0x26, 0x62, 0xaf, 0xe0, 0x13, 0x21, 0x9b, 0x82, 0x9d, 0x09, 0xf4, + 0x43, 0xbb, 0xb2, 0x97, 0xee, 0x16, 0x95, 0x4b, 0x8b, 0xc6, 0x56, 0xa2, 0x66, 0xcd, 0x0f, 0xe3, 0xcd, 0x14, 0xf6, + 0xb3, 0x47, 0x09, 0x04, 0xa4, 0xa9, 0x9c, 0xa4, 0x9a, 0xf7, 0x30, 0x1d, 0x02, 0x48, 0xb0, 0xfb, 0x09, 0x2c, 0xf4, + 0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, 0x73, 0xd0, 0x9a, 0x73, 0xd2, 0x7c, 0x73, 0xd4, 0xda, 0x9b, 0xca, 0x7a, + 0xc6, 0xec, 0x00, 0xdb, 0x76, 0x37, 0x8b, 0xc3, 0x74, 0xb3, 0x33, 0x34, 0x04, 0x17, 0x1e, 0xff, 0x27, 0x25, 0xa6, + 0x81, 0xe4, 0x52, 0x37, 0x7e, 0x42, 0x1d, 0x86, 0xff, 0x2d, 0x49, 0x01, 0x0f, 0x6a, 0xab, 0xb1, 0xe2, 0xdc, 0x2b, + 0x8e, 0x92, 0xcb, 0xaa, 0xda, 0xd5, 0x12, 0x34, 0x74, 0x23, 0x19, 0x13, 0xc5, 0x3c, 0x27, 0x00, 0x46, 0xb1, 0xf9, + 0x53, 0xa6, 0x93, 0xbc, 0x7f, 0x59, 0x9b, 0xda, 0xed, 0xfb, 0x7e, 0x94, 0x9f, 0xd0, 0x91, 0x8a, 0xca, 0xe6, 0x24, + 0xe6, 0xdf, 0x16, 0x60, 0x9a, 0x13, 0x1f, 0xea, 0xb9, 0x86, 0xa1, 0x00, 0x5f, 0xd9, 0x50, 0x6a, 0xb6, 0x97, 0xbf, + 0x77, 0xb6, 0xfb, 0x92, 0x28, 0x82, 0x05, 0x1a, 0x74, 0xb9, 0x02, 0x5f, 0xc0, 0x32, 0xb8, 0x25, 0xfd, 0xf4, 0xa6, + 0xbf, 0x0a, 0x3e, 0x63, 0xff, 0x0b, 0x40, 0xab, 0x02, 0x03, 0xca, 0x9d, 0xa6, 0x61, 0x25, 0xc4, 0x25, 0x2a, 0xcc, + 0x2a, 0xce, 0x1f, 0xd7, 0x79, 0xdd, 0xb4, 0x2c, 0x31, 0x28, 0x3f, 0x77, 0x0d, 0x37, 0xbe, 0xd7, 0xc8, 0x1f, 0xdf, + 0x7b, 0x0e, 0xba, 0x9d, 0x48, 0x7b, 0xf7, 0x6e, 0x7e, 0x87, 0x2c, 0x34, 0xbc, 0x17, 0x36, 0x87, 0xb6, 0x48, 0x97, + 0x5c, 0x3d, 0x63, 0x31, 0xde, 0x16, 0xa1, 0x32, 0x7c, 0xc0, 0x82, 0x39, 0x60, 0x08, 0x1e, 0x3b, 0x95, 0xc9, 0x67, + 0xd8, 0x68, 0x8a, 0x5d, 0x73, 0x61, 0xf0, 0x81, 0xaa, 0x2c, 0x24, 0x2f, 0xd6, 0xc9, 0xf6, 0xec, 0x14, 0x9e, 0x5f, + 0xc6, 0x05, 0x50, 0x07, 0xd0, 0xaf, 0xa8, 0x2c, 0x36, 0x90, 0x8b, 0x9b, 0xb2, 0xd6, 0x2b, 0x1a, 0x8f, 0xaf, 0xed, + 0xc2, 0xea, 0x0a, 0x7c, 0x1a, 0xa5, 0xe3, 0x44, 0x4c, 0x62, 0x26, 0x55, 0xae, 0xc9, 0xb5, 0xd1, 0xbd, 0xb4, 0x45, + 0xf3, 0x5c, 0x48, 0xf0, 0x8a, 0xc0, 0x0d, 0xa1, 0xaf, 0xf4, 0xe5, 0x7a, 0x03, 0x05, 0x8f, 0xda, 0x9b, 0x8b, 0x60, + 0x62, 0xe2, 0x31, 0x43, 0x6a, 0xfa, 0x75, 0x38, 0x15, 0xdf, 0xfc, 0xb6, 0xe2, 0xf0, 0xeb, 0x9c, 0xb1, 0x86, 0x02, + 0x20, 0x3e, 0x79, 0x70, 0xb5, 0x9b, 0xf4, 0x4a, 0x69, 0x07, 0xa5, 0x11, 0xe2, 0xdb, 0x0a, 0x5f, 0x77, 0xa9, 0xf8, + 0xca, 0x55, 0xf7, 0xbe, 0x8e, 0x99, 0x71, 0xc1, 0xe8, 0x39, 0x9f, 0x25, 0x8d, 0x6b, 0x37, 0x74, 0x57, 0xe7, 0x47, + 0xef, 0x07, 0x99, 0xb7, 0x70, 0x0c, 0x6c, 0x72, 0xcc, 0x9c, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x5b, 0xf3, 0x88, + 0x57, 0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xdb, 0xc1, 0xb7, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26, + 0xca, 0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54, + 0x44, 0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d, + 0x46, 0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85, + 0x77, 0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0, + 0x74, 0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x74, 0xbe, 0x6a, 0x95, 0x58, + 0xba, 0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f, + 0xcd, 0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45, + 0x90, 0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa, + 0x66, 0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97, + 0x97, 0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x23, 0x5b, 0x3f, + 0x78, 0x73, 0xf7, 0x0a, 0x58, 0x3e, 0x02, 0x76, 0x1f, 0x99, 0xd3, 0x18, 0xaa, 0xda, 0xc0, 0x3f, 0xac, 0x1f, 0x6c, + 0xdd, 0x1e, 0xfe, 0x61, 0xf0, 0x43, 0x70, 0x6d, 0x63, 0x63, 0x1b, 0x6f, 0xd7, 0x12, 0x41, 0x5e, 0xe1, 0x81, 0x3e, + 0x5e, 0x7d, 0x14, 0xb4, 0x5c, 0x27, 0xb6, 0x07, 0x0e, 0x85, 0xad, 0x41, 0xbe, 0x49, 0x99, 0x34, 0x5a, 0x14, 0x3c, + 0x9b, 0xc9, 0x19, 0x0a, 0x79, 0xcd, 0xc7, 0x41, 0xdb, 0x11, 0xfe, 0x06, 0x4e, 0xed, 0x78, 0x79, 0xf9, 0x09, 0xfa, + 0x80, 0xa7, 0x2b, 0xa5, 0xa9, 0x88, 0x53, 0xca, 0x2d, 0xba, 0x5a, 0xe7, 0xc1, 0x48, 0x71, 0x31, 0x45, 0xa5, 0xe3, + 0x2e, 0xaf, 0x9d, 0x8d, 0x9c, 0xfe, 0x12, 0xaf, 0x2e, 0xd2, 0xe5, 0x23, 0x91, 0xad, 0x5a, 0x7a, 0x2f, 0xf4, 0xe9, + 0xb6, 0x3d, 0x63, 0x7c, 0x9a, 0x8d, 0xe9, 0x60, 0xc6, 0xc7, 0x89, 0xf0, 0xfa, 0xc4, 0x58, 0xdf, 0x2d, 0x02, 0xd3, + 0xcd, 0xb1, 0xc9, 0x0f, 0xc7, 0xeb, 0xcd, 0x66, 0x8d, 0x3b, 0x78, 0xe3, 0x3c, 0x71, 0x96, 0x25, 0x46, 0x54, 0x96, + 0x1a, 0x1e, 0xd0, 0x0a, 0x71, 0xf3, 0x9e, 0x09, 0x8c, 0xcb, 0x2e, 0x48, 0x6a, 0xbb, 0x81, 0xc0, 0xc5, 0x9e, 0xc4, + 0x2c, 0x19, 0xdb, 0x1e, 0x94, 0x07, 0xfa, 0x62, 0x34, 0xdd, 0x02, 0xa6, 0xe5, 0xb5, 0xb3, 0xb3, 0xd4, 0xf6, 0xaa, + 0xa9, 0x02, 0x98, 0x25, 0xcb, 0xe3, 0x13, 0x64, 0xdd, 0x6f, 0xa0, 0x8b, 0x18, 0x30, 0x36, 0xae, 0xcc, 0xb9, 0xcb, + 0x75, 0x2b, 0xe2, 0x1b, 0x4d, 0xa4, 0x49, 0x7d, 0x48, 0x7d, 0x87, 0x61, 0xad, 0xae, 0x72, 0x90, 0xc0, 0x3d, 0xf2, + 0x6e, 0x89, 0x4b, 0x4f, 0x9f, 0x59, 0x4c, 0xaa, 0xf4, 0x2d, 0x75, 0x2d, 0xae, 0x19, 0xf6, 0x8a, 0x07, 0x60, 0x7f, + 0x60, 0xdc, 0x22, 0x16, 0xf1, 0x76, 0x5e, 0x4b, 0x61, 0x6d, 0xcc, 0x81, 0xe6, 0x86, 0x1b, 0xbc, 0x60, 0xd5, 0x9a, + 0x81, 0x19, 0x66, 0x9c, 0x91, 0xfc, 0x66, 0xdc, 0xab, 0x9a, 0x38, 0x72, 0x15, 0x40, 0xf4, 0x2d, 0xe9, 0x92, 0x1c, + 0x5e, 0xc9, 0x72, 0xd5, 0x19, 0xf2, 0xaf, 0xb0, 0xce, 0x7a, 0x71, 0x02, 0x66, 0xd2, 0x94, 0x97, 0x98, 0x98, 0x22, + 0x2e, 0x37, 0xcb, 0x98, 0xa7, 0xe9, 0xb3, 0x68, 0x07, 0x27, 0x37, 0x12, 0x38, 0x62, 0xdf, 0x58, 0x86, 0x66, 0xc2, + 0x46, 0x4c, 0xa4, 0x51, 0x29, 0x25, 0x7c, 0x20, 0x97, 0x5a, 0xf2, 0x97, 0xb9, 0xbc, 0xfa, 0x72, 0x9b, 0xe0, 0x80, + 0xbc, 0x06, 0x96, 0x43, 0xe3, 0xb8, 0x65, 0x20, 0x11, 0x8b, 0x01, 0x31, 0x6a, 0x55, 0xae, 0x26, 0xa3, 0x3a, 0x99, + 0xaf, 0x90, 0x0b, 0x15, 0x79, 0x70, 0x4b, 0xa0, 0xe4, 0xcf, 0x31, 0x75, 0x30, 0x2b, 0xb5, 0x9b, 0x16, 0x9b, 0x24, + 0xef, 0x99, 0x01, 0xc9, 0xf5, 0xd7, 0xf0, 0xd0, 0xf8, 0xc5, 0x2b, 0x73, 0x4a, 0xf8, 0xa2, 0x8c, 0xa5, 0xa5, 0x31, + 0x97, 0xfe, 0x83, 0xbc, 0x4f, 0x2b, 0x01, 0xfb, 0x15, 0xc4, 0x94, 0x81, 0x4b, 0x6c, 0x5c, 0x90, 0x94, 0xd7, 0xf2, + 0x94, 0xdd, 0xd7, 0x50, 0xbe, 0x2b, 0x26, 0x5d, 0xa5, 0xb2, 0xae, 0xb0, 0xea, 0x7e, 0x5d, 0xb0, 0xfc, 0x62, 0x9f, + 0x61, 0x6e, 0x32, 0x1a, 0x64, 0x2b, 0x66, 0x36, 0xe5, 0x57, 0x7b, 0xd7, 0x7e, 0xe5, 0xa1, 0xa4, 0x43, 0xb5, 0x4a, + 0x37, 0xaf, 0xdc, 0x70, 0x8c, 0x1b, 0x37, 0x1c, 0x01, 0x6c, 0x0c, 0x3b, 0x55, 0xa4, 0xd6, 0xf9, 0xef, 0xab, 0xe1, + 0x27, 0xda, 0x6b, 0x43, 0xbd, 0xeb, 0x86, 0x6b, 0xd3, 0xd3, 0xaf, 0x41, 0xd5, 0xc8, 0x12, 0xba, 0x0e, 0x55, 0x4c, + 0x46, 0xa2, 0xc4, 0x74, 0x95, 0xf2, 0xa8, 0xaf, 0x11, 0xe7, 0x20, 0x6e, 0x28, 0x7f, 0xf1, 0x2f, 0xe1, 0xc5, 0x51, + 0x80, 0x46, 0xd4, 0x72, 0x92, 0xa5, 0xbc, 0x35, 0x89, 0x66, 0x71, 0x72, 0x11, 0x2c, 0xe2, 0xd6, 0x2c, 0x4b, 0xb3, + 0x62, 0x0e, 0x5c, 0xe9, 0x15, 0x17, 0x60, 0xc3, 0xcf, 0x5a, 0x8b, 0xd8, 0x7b, 0xce, 0x92, 0x53, 0xc6, 0xe3, 0x51, + 0xe4, 0xd9, 0x7b, 0x39, 0x88, 0x07, 0xeb, 0x75, 0x94, 0xe7, 0xd9, 0x99, 0xed, 0xbd, 0xcb, 0x8e, 0x81, 0x69, 0xbd, + 0x37, 0xe7, 0x17, 0x27, 0x2c, 0xf5, 0xde, 0x1f, 0x2f, 0x52, 0xbe, 0xf0, 0x8a, 0x28, 0x2d, 0x5a, 0x05, 0xcb, 0xe3, + 0x09, 0xa8, 0x89, 0x24, 0xcb, 0x5b, 0x98, 0xff, 0x3c, 0x63, 0x41, 0x12, 0x9f, 0x4c, 0xb9, 0x35, 0x8e, 0xf2, 0x4f, + 0xbd, 0x56, 0x6b, 0x9e, 0xc7, 0xb3, 0x28, 0xbf, 0x68, 0x51, 0x8b, 0xe0, 0x8b, 0xf6, 0x76, 0xf4, 0xe5, 0xe4, 0x7e, + 0x8f, 0xe7, 0xd0, 0x37, 0x46, 0x2a, 0x06, 0x20, 0x7c, 0xac, 0xed, 0x9d, 0xf6, 0xac, 0xb8, 0x23, 0x4e, 0x94, 0xa2, + 0x94, 0x97, 0x47, 0xde, 0x19, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79, + 0x01, 0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04, + 0xf7, 0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66, + 0x05, 0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2, + 0x93, 0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff, + 0xad, 0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39, + 0xf8, 0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe, + 0x5e, 0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01, + 0xc8, 0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e, + 0x8e, 0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f, + 0xc5, 0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98, + 0xe8, 0xab, 0xab, 0x3e, 0x62, 0xf3, 0xdd, 0x8d, 0x5f, 0xaa, 0xf1, 0x2e, 0x54, 0xde, 0xa0, 0x50, 0x11, 0xea, 0x9b, + 0x2d, 0x98, 0xf1, 0x96, 0xf7, 0x3b, 0xfa, 0xa0, 0x6a, 0xf0, 0x1d, 0x23, 0xad, 0x17, 0x70, 0xcf, 0xcc, 0x05, 0xea, + 0xa5, 0x7d, 0x0c, 0x49, 0xb5, 0x5a, 0x2e, 0xe8, 0x0d, 0x86, 0x21, 0x24, 0x3a, 0x10, 0x74, 0xf2, 0x41, 0x41, 0xdf, + 0xd4, 0xc8, 0xdc, 0xa0, 0x70, 0x32, 0x17, 0xb6, 0x7c, 0xa6, 0xe5, 0x3a, 0x28, 0x69, 0xf0, 0xb2, 0xbf, 0x62, 0xb2, + 0x01, 0x48, 0xef, 0x4a, 0xd2, 0x7e, 0xaf, 0x4f, 0x9e, 0x94, 0xc7, 0x97, 0x8d, 0x88, 0x70, 0xe0, 0xea, 0xf3, 0x29, + 0xba, 0xdd, 0xfa, 0x3b, 0x31, 0x46, 0x46, 0xcd, 0x96, 0xed, 0x0e, 0x98, 0x4e, 0xca, 0xc2, 0xe4, 0x33, 0x56, 0xe2, + 0x28, 0x5f, 0xb3, 0xf0, 0x7b, 0x0c, 0xbc, 0xb2, 0x50, 0x78, 0x69, 0xca, 0x47, 0x9b, 0x5d, 0x77, 0xfb, 0x1f, 0x16, + 0x3c, 0xa6, 0x64, 0xe7, 0xc3, 0xe1, 0xbf, 0xc1, 0x67, 0x70, 0x34, 0x06, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0x04, 0x5c, + 0x0d, 0x26, 0xd8, 0xa4, 0xcb, 0x6d, 0x8f, 0x69, 0x15, 0x82, 0x1e, 0x76, 0xed, 0xfb, 0x09, 0x9c, 0xce, 0x57, 0xc4, + 0xf5, 0x05, 0x19, 0x40, 0x51, 0x10, 0xa2, 0x56, 0x1c, 0x53, 0x2a, 0x1d, 0x5d, 0xed, 0xf4, 0x17, 0x69, 0x0c, 0x42, + 0xf4, 0x63, 0x3c, 0xa6, 0x2b, 0x2d, 0xf1, 0x98, 0xce, 0x38, 0x5a, 0x94, 0xd3, 0x84, 0x41, 0x73, 0x28, 0x90, 0xb4, + 0xc5, 0x67, 0x99, 0x23, 0x63, 0xb7, 0x6c, 0x3c, 0xa7, 0x30, 0xb4, 0xf0, 0x38, 0x9b, 0x45, 0x71, 0x1a, 0xe0, 0xa7, + 0x47, 0x3c, 0x3d, 0x62, 0x80, 0x5d, 0x3c, 0xf8, 0xa9, 0xc8, 0xdc, 0x71, 0xfd, 0x5f, 0x40, 0x44, 0x51, 0xff, 0x52, + 0xba, 0x78, 0x1a, 0x2e, 0x75, 0x82, 0x5c, 0x2f, 0x05, 0xb1, 0xc6, 0x95, 0x39, 0xcc, 0x28, 0x84, 0xb2, 0xcb, 0xe9, + 0xc7, 0xa0, 0xd5, 0x09, 0x3a, 0xda, 0x2b, 0xae, 0x5d, 0x74, 0x15, 0x69, 0x32, 0xf2, 0xb2, 0x24, 0xc1, 0xa0, 0x9f, + 0x05, 0x9c, 0xd5, 0xbb, 0x86, 0xd5, 0x93, 0x2c, 0x8f, 0xb1, 0xa1, 0x93, 0xd4, 0xa9, 0x01, 0x41, 0xc7, 0x0c, 0x57, + 0x4c, 0xe5, 0x96, 0x11, 0x31, 0xe1, 0x63, 0x92, 0x0d, 0xf5, 0x6b, 0x4a, 0x78, 0x25, 0xa9, 0x9e, 0x5d, 0xa5, 0xb3, + 0xa4, 0x8f, 0x76, 0x85, 0x30, 0xb1, 0x88, 0xc7, 0x42, 0x1b, 0x76, 0xb7, 0x6d, 0xfd, 0x79, 0x04, 0x54, 0xfa, 0x14, + 0xda, 0x1b, 0x4b, 0x47, 0xe5, 0xec, 0xe7, 0x30, 0xd7, 0x9e, 0x50, 0xa8, 0x74, 0xd9, 0xdf, 0xee, 0x6f, 0x2c, 0x79, + 0xb9, 0xbb, 0x25, 0x7a, 0xf7, 0x8f, 0xca, 0x82, 0x74, 0x9f, 0x19, 0xa3, 0xab, 0xa6, 0x10, 0x75, 0x30, 0x2c, 0x65, + 0x06, 0xe3, 0xb8, 0xb9, 0xb6, 0xe7, 0x44, 0x30, 0x5a, 0xb2, 0x5d, 0x81, 0x99, 0x50, 0x51, 0x0e, 0xdb, 0x5d, 0xe7, + 0xba, 0x14, 0x22, 0xbd, 0xa3, 0xb7, 0x0a, 0xc5, 0x11, 0x42, 0x30, 0xd8, 0x58, 0xc6, 0x65, 0xb8, 0xb1, 0x64, 0xe9, + 0x28, 0x1b, 0xb3, 0xf7, 0xef, 0x5e, 0xe0, 0x75, 0x86, 0x2c, 0x45, 0xb9, 0x97, 0xb9, 0xe5, 0x11, 0x18, 0x42, 0x08, + 0x69, 0xae, 0xbe, 0x26, 0x03, 0xc0, 0x88, 0x98, 0x8e, 0x45, 0xa3, 0x22, 0x28, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x10, + 0x1c, 0x4e, 0x2c, 0x00, 0x53, 0x91, 0x7a, 0x31, 0xc0, 0x4f, 0xb4, 0xee, 0xc3, 0x40, 0xbb, 0x5b, 0xa2, 0x11, 0xe0, + 0x9a, 0x23, 0x1a, 0x15, 0xaa, 0x98, 0xfd, 0x63, 0xa2, 0x3b, 0x8e, 0x4f, 0x35, 0x39, 0x29, 0x15, 0xba, 0xbf, 0x9b, + 0x44, 0xc7, 0x2c, 0x81, 0x21, 0x8b, 0xcb, 0xcb, 0x36, 0x8c, 0x24, 0x5e, 0xad, 0xdd, 0x38, 0x9d, 0x2f, 0xe4, 0x57, + 0xbd, 0x60, 0xe2, 0x0e, 0x1e, 0xb4, 0xe2, 0xa5, 0x83, 0x81, 0x3a, 0x39, 0x0c, 0xe4, 0x00, 0x00, 0x22, 0x1d, 0x5a, + 0x20, 0x74, 0x15, 0xab, 0x40, 0x69, 0x3c, 0x5e, 0x2d, 0x83, 0xdd, 0x39, 0xc7, 0xd2, 0x14, 0x9e, 0x67, 0x71, 0x8a, + 0x8f, 0x05, 0x3e, 0x46, 0xe7, 0xf8, 0x98, 0xc1, 0xa3, 0xc6, 0x3d, 0x2f, 0xed, 0x7f, 0xd7, 0x55, 0xc9, 0xe4, 0x0a, + 0x58, 0x9a, 0x00, 0xd9, 0xe5, 0x25, 0xa8, 0x17, 0x4d, 0x82, 0xdd, 0x2d, 0x20, 0x16, 0x72, 0x8f, 0xf8, 0xc6, 0x0b, + 0x33, 0xc9, 0xc8, 0x8a, 0x79, 0x4b, 0x94, 0x5b, 0xa4, 0xc4, 0x43, 0xf0, 0xf1, 0x72, 0xa7, 0x61, 0xab, 0x78, 0x32, + 0x9b, 0xe5, 0x09, 0xbe, 0xb8, 0xb6, 0x25, 0x3e, 0xc2, 0x21, 0x88, 0x42, 0x8f, 0x88, 0xa1, 0x2e, 0xe3, 0xf2, 0xf3, + 0x3a, 0x71, 0x68, 0xe3, 0x2c, 0x60, 0x2e, 0xa2, 0xd2, 0xe1, 0x51, 0x9c, 0x88, 0xc6, 0x6b, 0xf0, 0x69, 0xa4, 0x25, + 0x12, 0x3a, 0xbb, 0x5b, 0x15, 0x6c, 0x00, 0xfc, 0x48, 0x5c, 0xea, 0x76, 0x44, 0xca, 0xa5, 0x2d, 0xca, 0xe9, 0xa8, + 0x5e, 0x6e, 0x73, 0x19, 0x48, 0x16, 0xa1, 0x79, 0x8d, 0x2a, 0xa5, 0x48, 0xda, 0x93, 0x28, 0x5d, 0xd7, 0x14, 0xa0, + 0x9f, 0x33, 0x36, 0xf6, 0x6c, 0x0b, 0xe4, 0xab, 0x78, 0xfe, 0x98, 0xb0, 0x53, 0x26, 0x3f, 0xcc, 0xa2, 0x07, 0xd1, + 0x95, 0x23, 0xb0, 0x00, 0xb8, 0xbc, 0x33, 0x2a, 0xd9, 0x53, 0xe1, 0x28, 0x29, 0x51, 0x47, 0xc4, 0xb3, 0x8d, 0x41, + 0x9b, 0x73, 0xb4, 0xeb, 0xc3, 0x7a, 0xa0, 0x93, 0x6c, 0x5b, 0xc0, 0x4b, 0x66, 0xe3, 0xcd, 0xc8, 0xc1, 0x00, 0xc7, + 0x39, 0x36, 0x4d, 0x59, 0x51, 0xac, 0x03, 0x0b, 0x9c, 0x60, 0xcf, 0xae, 0x9a, 0xd8, 0xb5, 0x0e, 0x00, 0x40, 0x77, + 0x67, 0x47, 0x4c, 0x0b, 0x15, 0x6c, 0x32, 0x81, 0x8d, 0x87, 0x1a, 0x23, 0xe1, 0x18, 0xf6, 0x0f, 0xfb, 0xf6, 0x6b, + 0xd8, 0xe3, 0x36, 0xf8, 0x57, 0xae, 0x3e, 0xc0, 0xa5, 0xe9, 0x95, 0x10, 0x32, 0xe6, 0x10, 0x9d, 0x8d, 0x61, 0xf4, + 0x93, 0x81, 0x54, 0x36, 0xfa, 0xb4, 0x06, 0x27, 0xe0, 0xc2, 0x0d, 0x11, 0x40, 0x6e, 0xc8, 0x56, 0xfb, 0x5f, 0xff, + 0xe9, 0x7f, 0xfd, 0x37, 0x18, 0x9b, 0xfa, 0xb9, 0xa5, 0x75, 0x75, 0xab, 0xff, 0x09, 0xad, 0x16, 0xe9, 0x0d, 0xed, + 0xfe, 0xfa, 0x0f, 0xff, 0x1d, 0x9a, 0xd1, 0x45, 0x23, 0x90, 0x59, 0x04, 0xd1, 0x08, 0x6d, 0xbb, 0xcf, 0x02, 0xa9, + 0x36, 0xc8, 0x95, 0x33, 0xfd, 0x23, 0x82, 0x5d, 0xf0, 0x6c, 0x7e, 0x2d, 0x38, 0x08, 0xf5, 0x28, 0xc9, 0x0a, 0xa6, + 0xe1, 0x11, 0x72, 0xfe, 0xf3, 0x00, 0xa2, 0xb9, 0xe6, 0xb0, 0x9b, 0x0a, 0x4b, 0x8f, 0x23, 0x56, 0x68, 0xdd, 0x38, + 0x8d, 0x05, 0x2c, 0x18, 0x27, 0x74, 0x28, 0x5c, 0x02, 0x4b, 0x26, 0x9e, 0xe0, 0x81, 0x04, 0x8f, 0x5b, 0xff, 0x78, + 0xd9, 0xfa, 0xc1, 0x34, 0xc3, 0x89, 0xb1, 0x44, 0x84, 0x48, 0x8d, 0x00, 0x3f, 0x41, 0x38, 0x7e, 0xd4, 0xcf, 0xd1, + 0xb9, 0x7e, 0x46, 0x01, 0x2a, 0x26, 0x00, 0x3d, 0x38, 0x43, 0x13, 0xc7, 0x9c, 0x41, 0x64, 0x37, 0x54, 0xee, 0xb1, + 0x91, 0x24, 0x23, 0x84, 0xe4, 0x47, 0xcc, 0x25, 0xc5, 0x9b, 0x43, 0x8b, 0x9c, 0x7d, 0x4c, 0xb2, 0x33, 0x8c, 0xb9, + 0x21, 0x91, 0xae, 0xaa, 0x2f, 0xff, 0xe5, 0x9f, 0x7d, 0xff, 0x5f, 0xfe, 0xf9, 0x8a, 0x06, 0x53, 0xd8, 0x13, 0x60, + 0x24, 0xf3, 0x50, 0xd3, 0xb9, 0x81, 0xd6, 0xfa, 0x41, 0x11, 0xcf, 0xf5, 0x35, 0x12, 0x71, 0x2c, 0x95, 0x78, 0xcb, + 0x47, 0x42, 0x5b, 0x33, 0xc5, 0xcd, 0xb3, 0x20, 0x64, 0x57, 0x4c, 0x83, 0x55, 0x37, 0xcc, 0x73, 0xe4, 0x06, 0xd7, + 0xd0, 0xe5, 0x33, 0x31, 0x5e, 0x0f, 0xc6, 0x8d, 0x10, 0x78, 0xa0, 0x65, 0x84, 0x1e, 0x7a, 0x22, 0xb4, 0x48, 0x20, + 0x96, 0x41, 0xea, 0x94, 0x1a, 0x40, 0x9e, 0x75, 0x40, 0x13, 0x50, 0x93, 0xb8, 0xd2, 0xe1, 0x64, 0x06, 0x1d, 0xe7, + 0xfd, 0x57, 0x78, 0x59, 0xd0, 0xc2, 0xde, 0xa8, 0xc1, 0x0b, 0x32, 0x38, 0x38, 0x19, 0x1c, 0x52, 0xd3, 0x99, 0xba, + 0x1e, 0x1d, 0xa7, 0x6c, 0xbd, 0x4e, 0xff, 0x88, 0xdd, 0x6b, 0x5a, 0x99, 0x4b, 0xad, 0x1c, 0x4b, 0x2b, 0x5a, 0x6a, + 0x65, 0xfc, 0x24, 0x4c, 0x43, 0x2b, 0xc7, 0x57, 0x6a, 0x65, 0xa4, 0xdc, 0x00, 0x47, 0x0e, 0xed, 0x4d, 0x8c, 0x0e, + 0x19, 0x36, 0x00, 0x47, 0xfb, 0x67, 0x34, 0x65, 0xa3, 0x4f, 0xd2, 0xfc, 0x21, 0x04, 0x30, 0x3c, 0xa2, 0x8d, 0x3c, + 0x81, 0x01, 0xa8, 0xf2, 0xa3, 0x52, 0x6f, 0x7a, 0x7c, 0x34, 0x26, 0xe0, 0xee, 0x72, 0xc2, 0x50, 0xf4, 0xc3, 0x9a, + 0x7d, 0xcd, 0xca, 0x2d, 0x1c, 0x47, 0x6c, 0x18, 0xf1, 0x0c, 0x98, 0x6d, 0xe1, 0x60, 0x47, 0xde, 0x52, 0x04, 0xdb, + 0x02, 0xfb, 0xed, 0x9b, 0xfd, 0x03, 0xdb, 0x3b, 0xce, 0xc6, 0x17, 0x81, 0x0d, 0xde, 0x0c, 0x58, 0x39, 0xae, 0xcf, + 0xa7, 0x2c, 0x75, 0x94, 0x3f, 0x91, 0x25, 0xe0, 0xaa, 0x65, 0x27, 0xe2, 0xdb, 0x10, 0xcd, 0x83, 0x02, 0x20, 0x2c, + 0x7d, 0x3c, 0xb2, 0xbf, 0xcb, 0xc5, 0x77, 0x55, 0x79, 0x8e, 0x8f, 0x7d, 0x4c, 0x95, 0xd8, 0xdd, 0x82, 0x07, 0x7c, + 0xd9, 0x47, 0xbd, 0xa7, 0xdf, 0x04, 0xb0, 0x85, 0x78, 0xdf, 0xc2, 0xf6, 0x5b, 0xaa, 0x2f, 0x42, 0xd1, 0x97, 0xdc, + 0xa6, 0x4d, 0xfe, 0xca, 0x66, 0xa4, 0xb1, 0xc7, 0x68, 0x12, 0x92, 0xc9, 0x0f, 0x24, 0xe1, 0x63, 0x5d, 0x22, 0xcc, + 0x0c, 0xa3, 0x88, 0x46, 0xa9, 0x8c, 0x02, 0x59, 0x85, 0x13, 0x92, 0x19, 0x29, 0x26, 0x83, 0x9f, 0x04, 0xfe, 0x91, + 0xf9, 0x1d, 0x34, 0xf1, 0xc9, 0x22, 0x8d, 0xe4, 0xe1, 0x5f, 0xbc, 0x33, 0xe6, 0x5d, 0x1c, 0x51, 0x4b, 0xe5, 0x74, + 0x63, 0x34, 0x08, 0x83, 0x13, 0x6d, 0x15, 0x5d, 0x01, 0x3b, 0x28, 0x89, 0xe6, 0x05, 0x0b, 0xd4, 0x83, 0xf4, 0xbf, + 0xd1, 0x8d, 0x5f, 0x0d, 0x78, 0x98, 0xf6, 0x52, 0xc9, 0xa7, 0x4b, 0xd3, 0x41, 0x7f, 0x00, 0x0e, 0x3a, 0x5e, 0x2e, + 0x68, 0x45, 0xa0, 0xe5, 0xd3, 0x20, 0x61, 0x13, 0x5e, 0x72, 0xbc, 0xbd, 0xbe, 0x54, 0x11, 0x11, 0xbf, 0xbb, 0x03, + 0x4e, 0xbb, 0xe5, 0xe3, 0xff, 0x37, 0x8d, 0x3d, 0x0e, 0x52, 0x70, 0xb2, 0xe9, 0x3a, 0x0b, 0x5e, 0x15, 0x04, 0x88, + 0xcc, 0xf7, 0xa5, 0x31, 0xd1, 0x88, 0x61, 0xb4, 0xa8, 0xe4, 0x39, 0xc8, 0x6d, 0x8f, 0xe7, 0x66, 0x3b, 0x90, 0xb7, + 0x2b, 0x21, 0xa3, 0xd5, 0xa0, 0xc5, 0xb6, 0x2b, 0xfd, 0x8f, 0xd5, 0xc6, 0x2a, 0xf2, 0x53, 0x7f, 0x5b, 0xa1, 0x90, + 0x11, 0xa3, 0x2a, 0x85, 0xaa, 0x59, 0x8a, 0x1e, 0x26, 0x4e, 0xab, 0xd1, 0xab, 0x1b, 0x2d, 0xd2, 0x92, 0xb6, 0xfd, + 0x21, 0x6d, 0x7b, 0x12, 0x63, 0xc3, 0xa5, 0x98, 0x7b, 0x14, 0x25, 0x23, 0x07, 0x01, 0xb0, 0x5a, 0xd6, 0x23, 0xa0, + 0xa6, 0xab, 0x22, 0x28, 0xfe, 0x43, 0x24, 0x6e, 0x29, 0x84, 0xde, 0x1a, 0x2a, 0x1d, 0x0d, 0xcb, 0xb2, 0x77, 0xc1, + 0x9c, 0xc3, 0xdf, 0xe4, 0x65, 0x08, 0x71, 0x07, 0x56, 0x7f, 0x47, 0xb0, 0x5d, 0xba, 0x43, 0x8f, 0x31, 0xe3, 0xeb, + 0x6c, 0xb6, 0xe2, 0x68, 0xdb, 0xeb, 0x52, 0x3c, 0x01, 0x7b, 0xbf, 0x72, 0x6c, 0x34, 0x62, 0xa9, 0xea, 0xa2, 0x45, + 0x1c, 0x66, 0x53, 0x47, 0x11, 0xcd, 0xff, 0xe6, 0xaa, 0xa0, 0xcc, 0xb7, 0x37, 0x07, 0x65, 0xf8, 0x2d, 0x83, 0x32, + 0xdf, 0xfe, 0xc1, 0x41, 0x99, 0x6f, 0xcc, 0xa0, 0x0c, 0xca, 0xca, 0x17, 0x9f, 0x13, 0x39, 0xc9, 0xb3, 0xb3, 0x22, + 0xec, 0xc8, 0x24, 0x00, 0x10, 0x3b, 0xff, 0x31, 0x21, 0x14, 0x98, 0xa8, 0x11, 0x40, 0xa1, 0x88, 0x89, 0xc8, 0x5b, + 0x04, 0x09, 0x2f, 0xe3, 0x15, 0x6d, 0x9d, 0x20, 0xd8, 0xba, 0xaf, 0x6e, 0x44, 0x81, 0x77, 0xe8, 0xea, 0xb0, 0x51, + 0x57, 0x45, 0x34, 0x02, 0xfa, 0xa4, 0xa9, 0xee, 0xd8, 0xdd, 0x54, 0x99, 0x69, 0xe6, 0x08, 0x3d, 0x75, 0xe0, 0x20, + 0x38, 0x68, 0x69, 0xff, 0xe7, 0xc3, 0x4e, 0x6f, 0xbb, 0x33, 0x83, 0xde, 0xa0, 0x4b, 0xe1, 0xad, 0xdd, 0xdb, 0xde, + 0xc6, 0xb7, 0x33, 0xf5, 0xd6, 0xc5, 0xb7, 0x58, 0xbd, 0xed, 0xe0, 0xdb, 0x48, 0xbd, 0x3d, 0xc0, 0xb7, 0xb1, 0x7a, + 0x7b, 0x88, 0x6f, 0xa7, 0x76, 0x79, 0xc8, 0x35, 0x70, 0x0f, 0x81, 0xb1, 0xc8, 0xb1, 0x08, 0x54, 0x19, 0xec, 0x5b, + 0xbc, 0x49, 0x18, 0x9d, 0x04, 0xb1, 0x27, 0x1c, 0xb0, 0x20, 0xf7, 0xce, 0x40, 0xf8, 0x07, 0x94, 0x38, 0xf7, 0x14, + 0x3f, 0x29, 0x01, 0xfe, 0xca, 0x41, 0x3c, 0x63, 0xea, 0xdb, 0xba, 0x0a, 0x6b, 0xb0, 0x25, 0x0f, 0xdb, 0xc3, 0xb2, + 0xa7, 0xd7, 0x49, 0x04, 0x6c, 0x54, 0x62, 0x02, 0xad, 0x5c, 0x55, 0x27, 0xa6, 0x6b, 0xe9, 0x15, 0xbe, 0x42, 0x95, + 0x18, 0x2e, 0xfb, 0x04, 0x6c, 0xa4, 0xd6, 0x39, 0x38, 0x79, 0x6b, 0xd5, 0x0b, 0x42, 0xa4, 0x15, 0x0a, 0xe1, 0xa4, + 0xdf, 0x0e, 0xa2, 0x13, 0xfd, 0xfc, 0x0a, 0x8c, 0xde, 0xe8, 0x84, 0xdd, 0xa4, 0x6a, 0x08, 0x44, 0x53, 0xcd, 0x28, + 0x20, 0xc8, 0x2a, 0x82, 0xa5, 0x41, 0x67, 0x53, 0xaa, 0x19, 0xa4, 0x4e, 0x5d, 0xf1, 0xd0, 0xf4, 0xf5, 0x22, 0xa0, + 0x68, 0x55, 0xb0, 0x0b, 0xb6, 0x37, 0x95, 0x0a, 0x0a, 0x43, 0x05, 0x16, 0x5c, 0xab, 0x8d, 0xb4, 0x3f, 0x7e, 0xa5, + 0x4e, 0xb2, 0x94, 0x3a, 0x32, 0x0f, 0x2a, 0xf4, 0x29, 0xc5, 0xaa, 0x84, 0xfc, 0xa2, 0x33, 0xc2, 0x3f, 0x52, 0xfe, + 0x7e, 0x31, 0x99, 0x4c, 0xae, 0x55, 0x4f, 0x5f, 0x8c, 0x27, 0xac, 0xcb, 0x76, 0x7a, 0x18, 0xc4, 0x6e, 0x49, 0x89, + 0xd8, 0x29, 0x89, 0x76, 0xcb, 0xdb, 0x35, 0x46, 0xe1, 0x09, 0x1a, 0xeb, 0xf6, 0x7a, 0xac, 0x04, 0xaa, 0x2c, 0x41, + 0x7e, 0x9f, 0xc4, 0x69, 0xd0, 0x2e, 0xfd, 0x53, 0x29, 0xf8, 0xbf, 0x78, 0xf4, 0xe8, 0x51, 0xe9, 0x8f, 0xd5, 0x5b, + 0x7b, 0x3c, 0x2e, 0xfd, 0xd1, 0x52, 0xa3, 0xd1, 0x6e, 0x4f, 0x26, 0xa5, 0x1f, 0xab, 0x82, 0xed, 0xee, 0x68, 0xbc, + 0xdd, 0x2d, 0xfd, 0x33, 0xa3, 0x45, 0xe9, 0x33, 0xf9, 0x96, 0xb3, 0x71, 0x2d, 0x12, 0xfe, 0xb0, 0x0d, 0x95, 0x82, + 0xd1, 0x96, 0xe8, 0xe8, 0x89, 0xc7, 0x20, 0x5a, 0xf0, 0x0c, 0x6c, 0x2c, 0xe0, 0x6d, 0x10, 0xd0, 0x13, 0x29, 0xde, + 0xc5, 0xa7, 0x6b, 0x51, 0xa8, 0xbf, 0x30, 0x65, 0x3a, 0x32, 0x33, 0xc9, 0x73, 0x4e, 0xaa, 0xa0, 0x59, 0x8d, 0x9c, + 0x45, 0xd5, 0x2f, 0x42, 0x5e, 0x49, 0x7b, 0x94, 0x36, 0xd8, 0x52, 0xc8, 0xf8, 0x1f, 0xaf, 0x92, 0xf1, 0x3f, 0xdc, + 0x2c, 0xe3, 0x8f, 0x6f, 0x27, 0xe2, 0x7f, 0xf8, 0x83, 0x45, 0xfc, 0x8f, 0xa6, 0x88, 0x17, 0x42, 0x6c, 0x0f, 0xac, + 0x58, 0x32, 0x5f, 0x8f, 0xb3, 0xf3, 0x16, 0x6e, 0x89, 0xdc, 0x26, 0xe9, 0xb9, 0x71, 0x2b, 0xe1, 0xbf, 0x26, 0xb5, + 0x49, 0x0d, 0x66, 0x7c, 0x07, 0x97, 0x67, 0x27, 0x27, 0x09, 0x53, 0x32, 0xde, 0xa8, 0x20, 0xcb, 0xf8, 0x4d, 0x1a, + 0xda, 0x6f, 0xc0, 0x49, 0x35, 0x4a, 0x26, 0x13, 0x28, 0x9a, 0x4c, 0x6c, 0x95, 0xfa, 0x0b, 0xf2, 0x8c, 0x5a, 0xbd, + 0xae, 0x95, 0x50, 0xab, 0xaf, 0xbe, 0x32, 0xcb, 0xcc, 0x02, 0x19, 0xf5, 0x32, 0xed, 0x09, 0x59, 0x33, 0x8e, 0x0b, + 0xdc, 0x83, 0xd5, 0x77, 0x7b, 0xd1, 0x64, 0x99, 0x81, 0x52, 0x89, 0x47, 0xf8, 0x41, 0x98, 0xe6, 0x37, 0x52, 0x44, + 0x9a, 0xf6, 0x2a, 0x72, 0xd5, 0x51, 0xae, 0xf1, 0x39, 0xbe, 0xea, 0xf8, 0x16, 0x16, 0x5f, 0xa6, 0x6a, 0x3c, 0xbe, + 0x78, 0x31, 0x76, 0xf6, 0xc0, 0x94, 0x8d, 0x8b, 0x37, 0x69, 0x23, 0x05, 0x4e, 0x80, 0x1d, 0x86, 0x26, 0xa6, 0xa5, + 0x20, 0x58, 0x75, 0x17, 0xa0, 0xaa, 0xec, 0x19, 0x9d, 0x64, 0xa6, 0x14, 0x0e, 0x39, 0xa8, 0x91, 0x25, 0x30, 0x07, + 0x93, 0xba, 0x90, 0xbe, 0xcb, 0x2e, 0xf2, 0x47, 0x4e, 0xe5, 0x07, 0xbc, 0xe9, 0xf4, 0x61, 0x29, 0xf5, 0x87, 0xcc, + 0x2c, 0xa8, 0x7a, 0x62, 0xc0, 0x5f, 0xcc, 0x30, 0x2e, 0x55, 0x90, 0x1f, 0x08, 0x37, 0xc7, 0xaf, 0x0d, 0x89, 0x21, + 0x54, 0x2c, 0xbd, 0xa2, 0xde, 0xe5, 0xa5, 0xf9, 0xd1, 0xcb, 0xda, 0x07, 0x12, 0x1b, 0x3c, 0xc0, 0xf0, 0x6b, 0xb5, + 0xa8, 0x0d, 0xb2, 0x05, 0x77, 0x1c, 0x6a, 0xe5, 0xb8, 0xa5, 0xb7, 0xd3, 0x6e, 0x83, 0x8a, 0xf1, 0xc5, 0x27, 0x8d, + 0x1c, 0xdd, 0x59, 0xe2, 0x7b, 0x55, 0xe8, 0x7e, 0xe5, 0x13, 0x63, 0x9a, 0xc4, 0xf8, 0x0d, 0x14, 0x81, 0xa8, 0x71, + 0x0d, 0x46, 0x2d, 0x62, 0xf3, 0xdd, 0x57, 0x6e, 0x9c, 0x41, 0x58, 0x77, 0x1d, 0x07, 0xcb, 0x34, 0xd1, 0x7a, 0x21, + 0xb6, 0x15, 0x56, 0xcd, 0x2a, 0x38, 0x37, 0xe8, 0xcc, 0xe2, 0xcc, 0x88, 0x71, 0xd7, 0xb6, 0x41, 0xa9, 0x82, 0xdc, + 0x22, 0x52, 0xbd, 0x87, 0xf1, 0x58, 0xe1, 0x03, 0x2b, 0xa0, 0xeb, 0xde, 0xa7, 0x01, 0x39, 0xfa, 0xa5, 0x9a, 0xd1, + 0x55, 0x95, 0x2a, 0x28, 0xcd, 0x53, 0x0a, 0x03, 0x19, 0x0a, 0x36, 0xc3, 0x1a, 0xa7, 0x42, 0x6f, 0xc1, 0x34, 0x24, + 0x80, 0xb5, 0x53, 0x86, 0x9e, 0x89, 0xad, 0xc0, 0x16, 0xd2, 0x02, 0x94, 0x1e, 0x76, 0xe8, 0x5b, 0x35, 0xd0, 0xd3, + 0xd5, 0x18, 0xf5, 0x75, 0x7e, 0xda, 0xc5, 0x91, 0x5f, 0x9c, 0x79, 0xf0, 0xcf, 0xfa, 0xd3, 0x12, 0xa4, 0xfc, 0xf1, + 0xa7, 0x98, 0x83, 0x51, 0x3d, 0x6f, 0x61, 0x24, 0x84, 0x42, 0xa6, 0x52, 0x1d, 0xd2, 0xa9, 0xaa, 0xb8, 0xfd, 0xd4, + 0x5b, 0x14, 0xe8, 0xce, 0x91, 0xdf, 0x12, 0xa4, 0x59, 0xca, 0x7a, 0xf5, 0xd3, 0x73, 0xd3, 0x75, 0x50, 0xc4, 0x1a, + 0x2e, 0x33, 0x74, 0xff, 0xf8, 0x05, 0xb8, 0x7f, 0x42, 0x8d, 0xb6, 0x95, 0xdf, 0xd0, 0x5e, 0xdb, 0x3e, 0x90, 0xb4, + 0xdd, 0x24, 0x6b, 0x21, 0x5f, 0xf5, 0x8f, 0xae, 0xf2, 0x6f, 0x6e, 0x3a, 0x4b, 0xc6, 0xf8, 0xac, 0xfa, 0x67, 0x1c, + 0xc2, 0x37, 0x8b, 0xe9, 0x2c, 0xf9, 0x36, 0x90, 0x05, 0xd1, 0x04, 0x3f, 0x16, 0x78, 0x9b, 0x96, 0xc7, 0x94, 0xc8, + 0xb9, 0x44, 0xb5, 0x1e, 0x74, 0x1e, 0x81, 0xc3, 0x76, 0xeb, 0xe1, 0xaf, 0x47, 0xbf, 0x94, 0x34, 0x52, 0xf7, 0x71, + 0x6d, 0xbb, 0x87, 0xf2, 0x22, 0x89, 0x2e, 0xc0, 0x6f, 0x24, 0x1b, 0xe3, 0x18, 0x03, 0xb9, 0xbd, 0x79, 0x26, 0x93, + 0x22, 0x72, 0x96, 0xd0, 0x6f, 0xe4, 0x90, 0x4b, 0xb1, 0xfd, 0x60, 0x7e, 0xae, 0x56, 0xa3, 0xd3, 0x48, 0x76, 0xf8, + 0x43, 0x73, 0x1a, 0xae, 0x4e, 0xa2, 0xa8, 0x9f, 0xcb, 0xef, 0x00, 0x0c, 0xc2, 0xb0, 0x69, 0xe5, 0x02, 0xaa, 0x36, + 0x94, 0x18, 0x59, 0x1d, 0xd5, 0x40, 0x96, 0xbf, 0x0d, 0xaa, 0x32, 0x2a, 0x58, 0x0f, 0xbf, 0xda, 0x18, 0x83, 0x77, + 0x2a, 0x8d, 0xa7, 0x59, 0x3c, 0x1e, 0x27, 0xac, 0xa7, 0xec, 0x23, 0xab, 0xf3, 0x00, 0x93, 0x22, 0xcc, 0x25, 0xab, + 0xaf, 0x8a, 0x41, 0x3c, 0x4d, 0xa7, 0xe8, 0x18, 0xec, 0x35, 0xfc, 0xf4, 0xe2, 0x5a, 0x72, 0xca, 0x6c, 0x81, 0x76, + 0x45, 0x3c, 0x7a, 0xae, 0xe3, 0xb2, 0x03, 0xc6, 0x22, 0x2d, 0x78, 0xbb, 0xc7, 0xb3, 0x79, 0xd0, 0xda, 0xae, 0x23, + 0x82, 0x55, 0x1a, 0x05, 0x6f, 0x0d, 0x5a, 0x1e, 0x5a, 0x07, 0x42, 0xcb, 0x59, 0x7e, 0x47, 0x96, 0xd1, 0x00, 0xf8, + 0x79, 0x3f, 0x5d, 0x54, 0xd6, 0x91, 0xf9, 0xf7, 0xd9, 0x2d, 0x5f, 0xae, 0xdf, 0x2d, 0x5f, 0xaa, 0xdd, 0x72, 0x3d, + 0xc7, 0x7e, 0x31, 0xe9, 0xe0, 0x9f, 0x5e, 0x85, 0x10, 0xac, 0x0a, 0x90, 0xc3, 0x42, 0xbb, 0xb8, 0xd5, 0x85, 0xff, + 0x68, 0xe8, 0xb6, 0x87, 0x7f, 0x7c, 0xb0, 0x00, 0xdb, 0x16, 0x16, 0xe2, 0xbf, 0x76, 0xad, 0xaa, 0x73, 0x1f, 0xeb, + 0xb0, 0xd7, 0xce, 0x6a, 0x5d, 0xf7, 0xfa, 0x4d, 0x0b, 0xf2, 0x8a, 0x3b, 0x81, 0x12, 0xc6, 0xe0, 0xaa, 0x45, 0xc7, + 0xc7, 0x50, 0x3a, 0xc9, 0x46, 0x8b, 0xe2, 0xef, 0x24, 0xfc, 0x92, 0x88, 0xd7, 0x6e, 0xe9, 0xc6, 0x38, 0xaa, 0xab, + 0xc8, 0xb0, 0x51, 0x23, 0x2c, 0xf5, 0x3a, 0x05, 0x05, 0x30, 0x26, 0x73, 0xba, 0xfe, 0xfd, 0x35, 0x9b, 0xe0, 0x3f, + 0x64, 0x6d, 0xd6, 0x22, 0xf3, 0x6f, 0x25, 0xc6, 0xb5, 0x44, 0xf8, 0x2c, 0x1a, 0x98, 0x6b, 0xd8, 0x7e, 0xb4, 0x1e, + 0xdc, 0x43, 0x35, 0xd3, 0x50, 0x29, 0x05, 0xa9, 0x77, 0xc0, 0x0b, 0x88, 0x16, 0x09, 0xbf, 0x7e, 0xd4, 0xab, 0x38, + 0x63, 0x65, 0xd4, 0x6b, 0x04, 0x7a, 0xd5, 0xf6, 0x96, 0x52, 0xfa, 0x8b, 0x2f, 0xef, 0xe3, 0x1f, 0x11, 0xfb, 0x3a, + 0xae, 0x7c, 0x23, 0x11, 0x1b, 0x40, 0xdf, 0x68, 0xa3, 0xe6, 0xfc, 0x08, 0x0d, 0x4e, 0xfe, 0xcf, 0x6d, 0x5b, 0xa3, + 0xb1, 0x7e, 0xab, 0xe6, 0xd2, 0x2a, 0xfd, 0xac, 0xd6, 0x9f, 0x37, 0xf8, 0x2d, 0xdb, 0x8e, 0x84, 0x43, 0x50, 0x6f, + 0x2b, 0x7f, 0x3b, 0xca, 0x4a, 0x63, 0x45, 0xf1, 0xdb, 0xb6, 0xaf, 0x4c, 0x62, 0xea, 0xb1, 0x11, 0x1e, 0x6b, 0x27, + 0x52, 0x9e, 0x6f, 0x63, 0x0f, 0xe1, 0x47, 0xfe, 0x85, 0x85, 0xf7, 0xf0, 0xc3, 0x62, 0xd6, 0xf9, 0x2c, 0x49, 0xc1, 0xac, 0x9a, 0x72, 0x3e, 0x0f, 0xb6, 0xb6, 0xce, 0xce, 0xce, 0xfc, 0xb3, 0x6d, 0x3f, 0xcb, 0x4f, 0xb6, 0xba, 0xed, - 0x76, 0x1b, 0x3f, 0x98, 0x64, 0x5b, 0xa7, 0x31, 0x3b, 0x7b, 0x0c, 0xee, 0x87, 0xfd, 0xd0, 0x7a, 0x64, 0x3d, 0xdc, + 0x76, 0x1b, 0xbf, 0x07, 0x65, 0x5b, 0xa7, 0x31, 0x3b, 0x7b, 0x0c, 0xee, 0x87, 0xfd, 0xd0, 0x7a, 0x64, 0x3d, 0xdc, 0xb6, 0x76, 0x1e, 0xd8, 0x16, 0x29, 0x00, 0x28, 0xd9, 0xb6, 0x2d, 0xa1, 0x00, 0x42, 0x1b, 0x8a, 0xfb, 0xbb, 0x27, - 0xca, 0x86, 0xc3, 0x84, 0x74, 0x61, 0x21, 0x81, 0xff, 0x96, 0x7d, 0x62, 0xf5, 0xad, 0x2e, 0xca, 0x5a, 0x52, 0x8d, + 0xca, 0x86, 0xc3, 0x7c, 0x7b, 0x61, 0x21, 0x81, 0xff, 0x96, 0x7d, 0x62, 0xf5, 0xad, 0x2e, 0xca, 0x5a, 0x52, 0x8d, 0xa8, 0x57, 0xdc, 0xef, 0xa3, 0x68, 0x1e, 0x10, 0x1b, 0x99, 0x85, 0x18, 0x26, 0x13, 0xa5, 0x34, 0x05, 0xda, 0xa5, - 0xc7, 0xf0, 0x04, 0x6e, 0xc1, 0xd4, 0x82, 0xe7, 0x57, 0xdd, 0x87, 0xa0, 0xe3, 0x4e, 0x5b, 0xf7, 0x47, 0xed, 0x56, - 0xc7, 0xea, 0xb4, 0xba, 0xfe, 0x43, 0xab, 0x2b, 0xfe, 0x07, 0x19, 0xb9, 0x6d, 0x75, 0xe0, 0x69, 0xdb, 0x82, 0xf7, - 0xd3, 0xfb, 0x22, 0x1d, 0x22, 0xb2, 0xb7, 0xfa, 0xbb, 0xf8, 0xfb, 0x83, 0x00, 0xa9, 0xaf, 0x6c, 0xf1, 0x1b, 0xcf, - 0xec, 0x2f, 0xcc, 0xd2, 0xce, 0xa3, 0xb5, 0xc5, 0xdd, 0x87, 0x6b, 0x8b, 0xb7, 0x1f, 0xac, 0x2d, 0xbe, 0xbf, 0x53, - 0x2f, 0xde, 0x3a, 0x11, 0x55, 0x5a, 0x2e, 0x84, 0xf6, 0x2c, 0x02, 0x46, 0x39, 0x77, 0x3a, 0x00, 0x67, 0xdb, 0x6a, - 0xe1, 0x8f, 0x87, 0x5d, 0x57, 0xf7, 0x3a, 0xc6, 0x5e, 0x1a, 0xcb, 0x87, 0x8f, 0x00, 0xcb, 0xe7, 0xdd, 0x07, 0x23, - 0x6c, 0x47, 0x88, 0xc2, 0xbf, 0xd3, 0xed, 0x47, 0x23, 0xd0, 0x08, 0x16, 0xfe, 0x83, 0x3f, 0xd3, 0x9d, 0xee, 0x48, - 0xbc, 0xb4, 0xb1, 0xfe, 0x43, 0xe7, 0x61, 0x01, 0x4d, 0xf1, 0xcf, 0x6f, 0xda, 0x84, 0x46, 0x03, 0xde, 0x1c, 0xf7, - 0x3e, 0xd0, 0xe8, 0xd1, 0xb4, 0xeb, 0x7f, 0x75, 0xfa, 0xd0, 0x7f, 0x34, 0xed, 0x3c, 0xfc, 0x20, 0xde, 0x12, 0xa0, - 0xe0, 0x57, 0xf8, 0xef, 0xc3, 0x76, 0x7b, 0xda, 0xea, 0xf8, 0x8f, 0x4e, 0xb7, 0xfd, 0xed, 0xa4, 0xf5, 0xc0, 0x7f, - 0x84, 0xff, 0xaa, 0xe1, 0xa6, 0xd9, 0x8c, 0xd9, 0x16, 0xae, 0x77, 0xc3, 0xef, 0x35, 0xe7, 0xe8, 0xde, 0xb7, 0x76, - 0xee, 0x3f, 0x7f, 0x04, 0x6b, 0x34, 0xed, 0x74, 0xe1, 0xff, 0xab, 0x1e, 0x3f, 0x20, 0xe1, 0xe5, 0xc0, 0x11, 0xc3, - 0x54, 0x52, 0x45, 0x38, 0xfa, 0x78, 0xd7, 0x3d, 0xef, 0x87, 0xab, 0x02, 0x20, 0x7f, 0xbe, 0x39, 0x00, 0xf2, 0x97, - 0x5b, 0x06, 0xb9, 0xff, 0xfc, 0x07, 0x47, 0x40, 0x7e, 0x68, 0x06, 0xb9, 0xf7, 0xd8, 0x4a, 0xa0, 0xa3, 0xe9, 0xac, - 0x3d, 0x67, 0xce, 0xe1, 0x8f, 0x6c, 0x88, 0x69, 0xd3, 0xd0, 0xfa, 0x2f, 0xb5, 0x78, 0x50, 0x86, 0x1b, 0x79, 0x8f, - 0x89, 0x9d, 0xcc, 0xf8, 0x15, 0x04, 0xe1, 0xfc, 0x46, 0x82, 0xbc, 0xb8, 0x1d, 0x3d, 0x38, 0xff, 0x63, 0xe9, 0x41, - 0x5f, 0xee, 0x57, 0xf4, 0xa8, 0x45, 0xdc, 0x29, 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0xf6, 0x16, 0xc3, 0xb7, - 0xc2, 0x16, 0xb9, 0x80, 0xef, 0x3e, 0xe7, 0x74, 0x40, 0x64, 0x15, 0x87, 0xb6, 0x0c, 0xc0, 0xcc, 0xf1, 0xdb, 0xb4, - 0xea, 0xe5, 0x54, 0xdc, 0x5c, 0x09, 0xe9, 0xda, 0xd9, 0x8e, 0x0e, 0xde, 0x60, 0xa2, 0x77, 0xb8, 0xcc, 0x78, 0x84, - 0xbf, 0xfc, 0x88, 0xc7, 0x3c, 0xc1, 0x4b, 0xb1, 0xf2, 0x02, 0x19, 0xe6, 0x25, 0x7f, 0x87, 0x39, 0xd5, 0xea, 0x90, - 0x60, 0x86, 0x01, 0x83, 0x57, 0x6c, 0x1c, 0x47, 0x8e, 0xed, 0xcc, 0x61, 0xc7, 0xc2, 0x98, 0xad, 0x5a, 0x42, 0x33, - 0xe5, 0x32, 0xbb, 0xb6, 0xfa, 0x7d, 0x3b, 0x39, 0x7e, 0xbf, 0x2c, 0x3c, 0x94, 0x01, 0x46, 0x5b, 0x7a, 0x00, 0x30, - 0xbe, 0x2a, 0xc9, 0x51, 0xd8, 0x57, 0x56, 0x83, 0x2d, 0xcc, 0x86, 0x8e, 0xdf, 0x05, 0x37, 0x82, 0x8a, 0xf1, 0x7b, - 0x50, 0x3f, 0x38, 0xad, 0x6d, 0x30, 0x6b, 0x8c, 0x6e, 0x7a, 0xa0, 0xe1, 0x4a, 0x18, 0x49, 0x04, 0x07, 0x1a, 0xa5, - 0x9e, 0xfe, 0x05, 0x64, 0x55, 0xb8, 0xa8, 0x78, 0x7c, 0x71, 0x20, 0xef, 0x7c, 0xdb, 0x18, 0xb9, 0xa5, 0x88, 0x7d, - 0xf5, 0xbd, 0xa9, 0x4d, 0x50, 0x17, 0xf4, 0x5b, 0x20, 0xe9, 0xdc, 0x1b, 0x35, 0x02, 0xa6, 0x5c, 0x5b, 0xd2, 0x73, - 0x08, 0x6d, 0xa1, 0x0f, 0xc6, 0xec, 0x34, 0x1e, 0x49, 0xb1, 0xee, 0x59, 0xf2, 0xaa, 0x48, 0x8b, 0xb0, 0x08, 0x3b, - 0x9e, 0xf0, 0x9d, 0xe1, 0x05, 0xb5, 0x5a, 0x98, 0x66, 0x76, 0xff, 0x5e, 0x4f, 0x43, 0x52, 0xcf, 0x56, 0xb7, 0xf1, - 0x57, 0x52, 0x1e, 0x82, 0xaf, 0xf6, 0xf7, 0xe1, 0x3d, 0xfc, 0xa5, 0x94, 0xf7, 0x86, 0xb6, 0xeb, 0x93, 0x50, 0xbc, - 0x57, 0xfd, 0x66, 0x4a, 0x94, 0x08, 0x9b, 0xa0, 0xbf, 0xbc, 0xdb, 0x2a, 0x32, 0xa9, 0xb4, 0xba, 0x3b, 0x95, 0xd2, - 0x82, 0x67, 0x43, 0x4a, 0x81, 0x00, 0xed, 0xfa, 0x3b, 0x86, 0x28, 0x3c, 0x6d, 0xe1, 0xcf, 0x9a, 0x30, 0xbc, 0x0f, - 0x0d, 0x94, 0x34, 0x7c, 0x09, 0xcd, 0xb7, 0x85, 0xe0, 0x85, 0x7e, 0x3f, 0x92, 0xa8, 0x12, 0x62, 0xaa, 0xce, 0x31, - 0x6b, 0x0e, 0x91, 0x44, 0x8e, 0x80, 0xed, 0x19, 0xf1, 0x26, 0xc1, 0xae, 0x32, 0x9a, 0xf2, 0x14, 0xfa, 0x3a, 0xfa, - 0x33, 0xce, 0xeb, 0xea, 0xbc, 0xda, 0xce, 0x59, 0x33, 0x05, 0x32, 0x7c, 0xe3, 0xa0, 0x8a, 0xae, 0x2e, 0x88, 0xcf, - 0x99, 0x89, 0x6d, 0x5c, 0x7d, 0xf0, 0x6d, 0x4d, 0xf6, 0xad, 0xb9, 0x29, 0x58, 0xc5, 0x34, 0xb4, 0x2f, 0x30, 0x65, - 0x06, 0x7f, 0x56, 0xc5, 0xea, 0x41, 0x32, 0x94, 0x9f, 0x44, 0xf8, 0xdb, 0x58, 0xe8, 0x47, 0x59, 0x6d, 0x40, 0x4e, - 0xdf, 0xab, 0x24, 0x48, 0x5f, 0x8c, 0xcb, 0x26, 0x12, 0x60, 0x2f, 0xe0, 0x2f, 0xf7, 0xab, 0xae, 0x4a, 0xc8, 0x3b, - 0x90, 0x98, 0x53, 0x30, 0x8e, 0x73, 0xba, 0x5a, 0xab, 0xf0, 0xaf, 0x45, 0x34, 0x2b, 0x52, 0xd3, 0xae, 0x64, 0xc5, - 0xc0, 0xc6, 0x22, 0x3b, 0x90, 0xc9, 0x68, 0xe6, 0x07, 0x9b, 0xcd, 0xbb, 0x8f, 0x63, 0x91, 0x87, 0x86, 0x1f, 0xb4, - 0xb7, 0x05, 0x91, 0x6d, 0x10, 0x63, 0x57, 0xe2, 0x44, 0xc6, 0x0d, 0x5e, 0x19, 0xac, 0x7e, 0x43, 0x91, 0xb9, 0xe1, - 0x6d, 0x73, 0xb5, 0xf4, 0xb8, 0xb4, 0x0e, 0xae, 0x8c, 0xdf, 0x1d, 0xb3, 0x88, 0xfb, 0x51, 0x4a, 0xb9, 0x49, 0x8e, - 0x21, 0x16, 0xbc, 0x0e, 0xdb, 0x76, 0x4b, 0x90, 0x3c, 0xc6, 0xaf, 0x70, 0x12, 0xa4, 0xf7, 0xa1, 0xb0, 0x4a, 0xd8, - 0xda, 0x9d, 0x76, 0xfb, 0x6f, 0x0e, 0xf6, 0x2c, 0xb1, 0x9b, 0x77, 0xb7, 0xe0, 0x75, 0x97, 0xdc, 0x61, 0x91, 0x9f, - 0x11, 0x8a, 0xfc, 0x0c, 0x4b, 0x24, 0x74, 0x85, 0xf6, 0x96, 0x40, 0xd3, 0xb6, 0x58, 0x3a, 0x12, 0x31, 0xbc, 0x19, - 0xb8, 0x0b, 0x31, 0x7e, 0xd4, 0x6b, 0x0b, 0xbb, 0xb5, 0x70, 0xa5, 0x6d, 0x95, 0xe1, 0xa2, 0x0c, 0x04, 0x9e, 0xaa, - 0x88, 0x1f, 0xa8, 0x75, 0xa6, 0x92, 0x5d, 0xe4, 0x50, 0x3a, 0x27, 0x75, 0xb5, 0x75, 0xb1, 0x38, 0x9e, 0x81, 0x1c, - 0x52, 0x09, 0x2a, 0xef, 0x65, 0x87, 0x5d, 0x9a, 0x0a, 0x93, 0x62, 0x57, 0x23, 0x92, 0xd3, 0x4e, 0x7f, 0x37, 0x92, - 0xf6, 0x0e, 0xee, 0xdd, 0x02, 0x36, 0x2f, 0xa8, 0x39, 0x34, 0x2a, 0xfc, 0x38, 0xdb, 0x3a, 0x63, 0xc7, 0xad, 0x68, - 0x1e, 0x57, 0xe1, 0x3f, 0xd4, 0x7e, 0xfd, 0x5d, 0xa5, 0x08, 0x65, 0x9a, 0xa5, 0x7c, 0x8c, 0x8c, 0x2c, 0x0e, 0x24, - 0x1c, 0x31, 0x68, 0x29, 0x63, 0x8b, 0x64, 0x34, 0x02, 0xf1, 0x01, 0x56, 0xe2, 0x5f, 0x15, 0x83, 0x94, 0x9a, 0xa0, - 0xb4, 0xfb, 0x7f, 0xfd, 0x5f, 0xff, 0x5b, 0x86, 0x15, 0x81, 0xac, 0x00, 0x16, 0xa6, 0xc1, 0x54, 0x27, 0x8c, 0xec, - 0x1c, 0x1c, 0xd1, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, 0x20, 0x28, 0x98, 0xb8, 0xca, 0x24, 0xeb, 0x81, 0x0b, 0x24, - 0x58, 0xe6, 0xe1, 0xbc, 0x04, 0xaf, 0x5e, 0x84, 0x2b, 0xf6, 0xbb, 0xf2, 0x56, 0x55, 0xbe, 0x30, 0x31, 0xb4, 0x91, - 0xc5, 0x6a, 0xf0, 0x5c, 0x2d, 0x93, 0x55, 0xfd, 0x82, 0x24, 0x29, 0x3c, 0x58, 0x2d, 0x8d, 0x15, 0x5a, 0xea, 0x83, - 0x90, 0x7f, 0xfb, 0xe7, 0xff, 0xfc, 0xdf, 0xd5, 0x2b, 0x9e, 0x6f, 0xfc, 0xf5, 0x9f, 0xfe, 0xe1, 0xff, 0xfe, 0x9f, - 0xff, 0x82, 0x59, 0xc2, 0xf2, 0x0c, 0x84, 0xb6, 0x92, 0x55, 0x1d, 0x80, 0x88, 0x3d, 0x65, 0x55, 0x0e, 0x47, 0x3d, - 0xdd, 0x75, 0x9f, 0x26, 0x24, 0xde, 0x94, 0xd0, 0x11, 0x5f, 0x53, 0x7a, 0x34, 0x51, 0xed, 0x1a, 0xf2, 0xc1, 0x52, - 0x5a, 0x74, 0xac, 0x6f, 0xef, 0xb4, 0xed, 0x6a, 0x79, 0xfb, 0x46, 0xdf, 0x2d, 0x5c, 0x98, 0x5b, 0x65, 0xe0, 0xf8, - 0x7a, 0xd9, 0x96, 0x2a, 0x8c, 0x85, 0x25, 0x65, 0x55, 0x6e, 0x61, 0x7c, 0x79, 0x89, 0xaf, 0x41, 0xd7, 0x28, 0xa6, - 0x55, 0xae, 0xf5, 0xe9, 0xfd, 0xb2, 0x00, 0x44, 0x27, 0xb8, 0x34, 0x22, 0x58, 0x46, 0x67, 0xa7, 0x2d, 0xb4, 0x4e, - 0x92, 0x8b, 0x92, 0x46, 0x11, 0xde, 0xcc, 0xfd, 0x47, 0x7f, 0x57, 0xfe, 0x69, 0x86, 0x56, 0x81, 0xe5, 0xcc, 0xa2, - 0x73, 0xe9, 0xe3, 0x3c, 0x68, 0xb7, 0xe7, 0xe7, 0xee, 0xb2, 0x9a, 0xc1, 0xbb, 0x6a, 0x32, 0x0a, 0xb0, 0x99, 0x03, - 0xd2, 0xa1, 0xab, 0x8e, 0xe5, 0x81, 0x59, 0xdf, 0xc6, 0xd0, 0x4f, 0x59, 0x7e, 0xb9, 0xa4, 0x70, 0x52, 0xfc, 0x1b, - 0x1e, 0x8e, 0xca, 0xc8, 0x1b, 0x94, 0x18, 0x58, 0x2c, 0x8d, 0x5e, 0x5d, 0xd1, 0x6b, 0xda, 0x59, 0xcd, 0x4d, 0x31, - 0x0f, 0x77, 0xcd, 0x63, 0xd9, 0xfb, 0x78, 0xd0, 0x3a, 0xed, 0x78, 0xd3, 0xee, 0x52, 0x0f, 0xcf, 0x79, 0x36, 0x33, - 0x4f, 0x73, 0x59, 0xc4, 0x46, 0x6c, 0xa2, 0x22, 0x96, 0xb2, 0x5e, 0x9c, 0xd4, 0x96, 0x5f, 0xe0, 0x76, 0x03, 0xda, - 0x66, 0x11, 0x0f, 0x88, 0x69, 0x7b, 0xe6, 0x79, 0x6f, 0x84, 0x27, 0xe9, 0xd9, 0xd2, 0x98, 0xab, 0x27, 0x9a, 0x62, - 0x5c, 0xb0, 0x9e, 0xf7, 0x53, 0xfa, 0xd4, 0xdd, 0x1c, 0x4a, 0x84, 0x15, 0x5e, 0xc8, 0x63, 0xd4, 0x77, 0x35, 0x7f, - 0x5c, 0x8a, 0x62, 0x70, 0x81, 0xd7, 0xd6, 0x0b, 0xb5, 0x28, 0x6a, 0x5f, 0x80, 0xb5, 0x43, 0x60, 0xda, 0xcd, 0x56, - 0x54, 0x88, 0xad, 0xde, 0x85, 0x2f, 0xb4, 0xed, 0x1d, 0xcd, 0xe7, 0xd4, 0xd0, 0x05, 0x6e, 0x24, 0x1b, 0x1a, 0x25, - 0x05, 0xa5, 0x08, 0x88, 0x13, 0x79, 0xd9, 0x46, 0xb2, 0xad, 0x78, 0x92, 0x67, 0xf5, 0xf4, 0xfb, 0xb6, 0xff, 0x1f, - 0x22, 0x28, 0x4d, 0x5d, 0x85, 0x7b, 0x00, 0x00}; + 0xc7, 0xf0, 0x84, 0x19, 0x5a, 0x16, 0x3c, 0xbf, 0xea, 0x3e, 0x04, 0x1d, 0x77, 0xda, 0xba, 0x3f, 0x6a, 0xb7, 0x3a, + 0x56, 0xa7, 0xd5, 0xf5, 0x1f, 0x5a, 0x5d, 0xf1, 0x3f, 0xc8, 0xc8, 0x6d, 0xab, 0x03, 0x4f, 0xdb, 0x16, 0xbc, 0x9f, + 0xde, 0x17, 0xe9, 0x17, 0x91, 0xbd, 0xd5, 0xdf, 0xc5, 0x5f, 0x8f, 0x04, 0x48, 0x7d, 0x69, 0x8b, 0x5f, 0xe8, 0x66, + 0x7f, 0x61, 0x96, 0x76, 0x1e, 0xad, 0x2d, 0xee, 0x3e, 0x5c, 0x5b, 0xbc, 0xfd, 0x60, 0x6d, 0xf1, 0xfd, 0x9d, 0x7a, + 0xf1, 0xd6, 0x89, 0xa8, 0xd2, 0x72, 0x21, 0xb4, 0x67, 0x11, 0x30, 0xca, 0xb9, 0xd3, 0x01, 0x38, 0xdb, 0x56, 0x0b, + 0x7f, 0x3c, 0xec, 0xba, 0xba, 0xd7, 0x31, 0xf6, 0xd2, 0x58, 0x3e, 0x7c, 0x04, 0x58, 0x3e, 0xef, 0x3e, 0x18, 0x61, + 0x3b, 0x42, 0x14, 0xfe, 0x9d, 0x6e, 0x3f, 0x1a, 0x81, 0x46, 0xb0, 0xf0, 0x1f, 0xfc, 0x99, 0xee, 0x74, 0x47, 0xe2, + 0xa5, 0x8d, 0xf5, 0x1f, 0x3a, 0x0f, 0x0b, 0x68, 0x8a, 0x7f, 0x7e, 0xd3, 0x26, 0x34, 0x1a, 0xf0, 0xe6, 0xb8, 0xf7, + 0x81, 0x46, 0x8f, 0xa6, 0x5d, 0xff, 0xcb, 0xd3, 0x87, 0xfe, 0xa3, 0x69, 0xe7, 0xe1, 0x07, 0xf1, 0x96, 0x00, 0x05, + 0xbf, 0xc4, 0x7f, 0x1f, 0xb6, 0xdb, 0xd3, 0x56, 0xc7, 0x7f, 0x74, 0xba, 0xed, 0x6f, 0x27, 0xad, 0x07, 0xfe, 0x23, + 0xfc, 0x57, 0x0d, 0x37, 0xcd, 0x66, 0xcc, 0xb6, 0x70, 0xbd, 0x1b, 0x7e, 0xaf, 0x39, 0x47, 0xf7, 0xbe, 0xb5, 0x73, + 0xff, 0xf9, 0x23, 0x58, 0xa3, 0x69, 0xa7, 0x0b, 0xff, 0x5f, 0xf5, 0xf8, 0x01, 0x09, 0x2f, 0x07, 0x8e, 0x18, 0x66, + 0xca, 0x2a, 0xc2, 0xd1, 0xb7, 0xc9, 0xee, 0x79, 0xdf, 0x5f, 0x15, 0x00, 0x61, 0xfc, 0xe6, 0x20, 0x37, 0xbf, 0x5d, + 0x04, 0x84, 0x3e, 0x9c, 0xff, 0x07, 0x46, 0x40, 0xbe, 0x6f, 0x06, 0xb9, 0xcf, 0x57, 0xf3, 0x03, 0x9b, 0xce, 0xda, + 0x6b, 0xe6, 0x1c, 0xfe, 0x85, 0x0d, 0x31, 0x2b, 0x1c, 0x5a, 0x73, 0x6e, 0xc6, 0x83, 0x32, 0xdc, 0xc8, 0xe7, 0x32, + 0xea, 0x5f, 0xf0, 0x2b, 0x08, 0x12, 0xdf, 0x4c, 0x90, 0x5f, 0x6f, 0x47, 0x8f, 0xf8, 0x0f, 0xa6, 0x47, 0xc1, 0x0d, + 0x7a, 0xd4, 0x22, 0xee, 0x14, 0x31, 0x20, 0x47, 0x7f, 0x9f, 0xde, 0x9d, 0xef, 0xf1, 0xab, 0x62, 0x5b, 0x0c, 0x4b, + 0x0a, 0x5b, 0xe4, 0x24, 0xbe, 0xfb, 0x9c, 0x13, 0x02, 0x91, 0x38, 0x1d, 0xda, 0x32, 0x08, 0x33, 0xc7, 0xcf, 0xef, + 0xaa, 0x97, 0x53, 0x71, 0x39, 0x27, 0xa4, 0x9b, 0x75, 0x3b, 0x3a, 0x80, 0x83, 0xb9, 0xec, 0xe1, 0x32, 0xe3, 0x11, + 0xfe, 0x7e, 0x27, 0x1e, 0xf3, 0x04, 0xef, 0xfd, 0xca, 0x3b, 0x72, 0x98, 0x7a, 0xfd, 0x2d, 0xa6, 0x8d, 0xab, 0x83, + 0x82, 0x19, 0x06, 0x0d, 0x5e, 0xb1, 0x71, 0x1c, 0x39, 0xb6, 0x33, 0x87, 0x5d, 0x0b, 0x63, 0xb6, 0x6a, 0x39, 0xdb, + 0x94, 0xae, 0xed, 0xda, 0xea, 0x57, 0x0a, 0xe5, 0xf8, 0x89, 0xb6, 0xf0, 0x50, 0x06, 0x19, 0x6d, 0xe9, 0x05, 0xc0, + 0xf8, 0xaa, 0x24, 0x47, 0x81, 0x5f, 0x59, 0x0e, 0xb6, 0x30, 0x1d, 0x3a, 0x7e, 0x17, 0x5c, 0x09, 0x2a, 0xc6, 0x4f, + 0x5e, 0xfd, 0xe0, 0xb4, 0xb6, 0xc1, 0xb4, 0x31, 0xba, 0xe9, 0x81, 0x86, 0x2b, 0xa1, 0x24, 0x11, 0x20, 0x68, 0x94, + 0x7a, 0xfa, 0x77, 0xac, 0x55, 0x21, 0xa3, 0xe2, 0xf1, 0xc5, 0x81, 0xbc, 0xd6, 0x6e, 0x63, 0xf4, 0x96, 0xa2, 0xf6, + 0xd5, 0x27, 0xb5, 0x36, 0x41, 0x65, 0xd0, 0x2f, 0xba, 0xa4, 0x33, 0x70, 0xd4, 0x0a, 0x98, 0x55, 0x6e, 0x49, 0xef, + 0x21, 0xb4, 0x85, 0x4e, 0x18, 0xb3, 0xd3, 0x78, 0x24, 0x45, 0xbb, 0x67, 0xc9, 0xdb, 0x30, 0x2d, 0xc2, 0x22, 0xec, + 0x78, 0xc2, 0x7f, 0x86, 0x17, 0xd4, 0x6c, 0x61, 0x9a, 0xd9, 0xfd, 0x7b, 0x3d, 0x0d, 0x49, 0x3d, 0x21, 0xdf, 0xc6, + 0xdf, 0xba, 0x79, 0x08, 0xfe, 0xda, 0xdf, 0x85, 0xf7, 0xf0, 0xf7, 0x6e, 0xde, 0x1b, 0xda, 0xae, 0x4f, 0x82, 0xf1, + 0x5e, 0xf5, 0xcb, 0x37, 0x51, 0x2a, 0x6c, 0x82, 0x0e, 0xf3, 0x6e, 0xab, 0xcc, 0xa4, 0xe2, 0xea, 0xee, 0x54, 0x8a, + 0x0b, 0x9e, 0x0d, 0x49, 0x05, 0x42, 0xb4, 0xeb, 0xef, 0x18, 0xe2, 0xf0, 0xb4, 0x85, 0x3f, 0x6b, 0x02, 0xf1, 0x3e, + 0x34, 0x50, 0x12, 0xf1, 0x25, 0x34, 0xdf, 0x16, 0xc2, 0x17, 0xfa, 0xfd, 0x48, 0xe2, 0x4a, 0x88, 0xaa, 0x3a, 0xc7, + 0xac, 0x39, 0x48, 0x12, 0xf9, 0x02, 0xb6, 0x67, 0xc4, 0x9c, 0x04, 0xbb, 0xca, 0x88, 0xca, 0x53, 0xe8, 0xeb, 0xe8, + 0x2f, 0x55, 0xaf, 0xab, 0xf3, 0x6a, 0xbb, 0x67, 0xcd, 0x14, 0xc8, 0xf0, 0x8d, 0xc3, 0x2a, 0xba, 0x9d, 0x21, 0xbe, + 0xd8, 0x26, 0xb6, 0x72, 0xf5, 0x4d, 0xbb, 0x35, 0x59, 0xc0, 0xe6, 0xa6, 0x60, 0x15, 0xd3, 0xd0, 0xbe, 0xc0, 0xf4, + 0x19, 0xfc, 0x59, 0x15, 0xab, 0x07, 0xc9, 0x50, 0x7e, 0x12, 0xe1, 0x2f, 0x9c, 0xa1, 0x1f, 0x65, 0xb5, 0x01, 0x39, + 0x7d, 0x92, 0x93, 0x20, 0x7d, 0x31, 0x2e, 0x9b, 0x48, 0x80, 0xcd, 0x80, 0xbf, 0xbf, 0xb0, 0xba, 0x0d, 0x22, 0xaf, + 0x79, 0x62, 0x6a, 0xc1, 0x38, 0xce, 0xe9, 0xf6, 0xb0, 0xc2, 0xbf, 0x16, 0xd5, 0xac, 0x48, 0x4d, 0xbb, 0x92, 0x15, + 0x03, 0x1b, 0x8b, 0xec, 0x40, 0x26, 0xc0, 0x99, 0xdf, 0xa4, 0x36, 0xaf, 0x77, 0x8e, 0x45, 0xee, 0x1b, 0x7e, 0xb3, + 0xdf, 0x16, 0x44, 0xb6, 0x41, 0x94, 0x5d, 0x89, 0x13, 0x19, 0x38, 0x78, 0x2b, 0xb2, 0xfa, 0x25, 0x4c, 0xe6, 0x86, + 0xb7, 0xcd, 0xd5, 0xd2, 0xe3, 0xd2, 0x3a, 0xb8, 0x32, 0x86, 0x77, 0xcc, 0x22, 0xee, 0x47, 0x29, 0xe5, 0x29, 0x39, + 0x86, 0x58, 0xf0, 0x3a, 0x6c, 0xdb, 0x2d, 0x41, 0xf2, 0x18, 0xbf, 0xa5, 0x4a, 0x90, 0xde, 0x87, 0x42, 0x95, 0x4b, + 0xfd, 0x7d, 0x2d, 0x15, 0x78, 0xda, 0xed, 0xbf, 0x39, 0xd8, 0xb3, 0xc4, 0xc6, 0xde, 0xdd, 0x82, 0xd7, 0x5d, 0xf2, + 0x8e, 0x45, 0xc6, 0x46, 0x28, 0x32, 0x36, 0x2c, 0x91, 0xe7, 0x25, 0x52, 0x67, 0xb7, 0x04, 0xd6, 0xb6, 0xc5, 0xd2, + 0x91, 0x08, 0xeb, 0xcd, 0xc0, 0x83, 0x88, 0xf1, 0x33, 0x66, 0x5b, 0xd8, 0xb5, 0x85, 0x0b, 0x6f, 0xab, 0xe4, 0x17, + 0x65, 0x33, 0xf0, 0x54, 0x05, 0x01, 0x41, 0xd3, 0x33, 0x95, 0x07, 0x23, 0x87, 0xd2, 0x69, 0xb1, 0xab, 0xad, 0x8b, + 0xc5, 0xf1, 0x0c, 0xc4, 0x92, 0xca, 0x5d, 0x79, 0x2f, 0x3b, 0xec, 0xd2, 0x54, 0xfd, 0xa3, 0x72, 0x5d, 0x94, 0x72, + 0xda, 0xe9, 0xef, 0x46, 0xd2, 0x06, 0xc2, 0xbd, 0x5c, 0xc0, 0x66, 0x06, 0xd5, 0x87, 0x86, 0x86, 0x1f, 0x67, 0x5b, + 0x67, 0xec, 0xb8, 0x15, 0xcd, 0xe3, 0x2a, 0x24, 0x88, 0x1a, 0xb1, 0xbf, 0xab, 0x94, 0xa3, 0x4c, 0xf5, 0x94, 0x8f, + 0x91, 0x91, 0xdc, 0x81, 0x84, 0x24, 0x86, 0x2d, 0x65, 0xbc, 0x91, 0x0c, 0x49, 0x58, 0x0c, 0x00, 0x96, 0xf8, 0x59, + 0xc5, 0x25, 0xa5, 0x66, 0x28, 0xed, 0xfe, 0x5f, 0xff, 0xf7, 0xff, 0x91, 0xa1, 0x46, 0xa0, 0x2d, 0x80, 0x85, 0xd9, + 0x31, 0xd5, 0xa9, 0x23, 0x3b, 0x07, 0xe7, 0x34, 0x1e, 0xb7, 0xa6, 0x51, 0x32, 0x01, 0x08, 0x0a, 0x26, 0xee, 0x14, + 0xc8, 0x7a, 0xe0, 0x0a, 0x09, 0x96, 0x79, 0x62, 0x2f, 0xc1, 0xab, 0x17, 0xe1, 0xb2, 0xfd, 0xae, 0xdc, 0x59, 0x95, + 0xb3, 0x4c, 0x0c, 0x6e, 0x64, 0xd2, 0x1a, 0x3c, 0x58, 0xcb, 0xa6, 0x55, 0xbf, 0x13, 0x4a, 0x0a, 0x13, 0x56, 0x4b, + 0xa5, 0x85, 0x96, 0xfa, 0x70, 0xe4, 0x5f, 0xff, 0xe9, 0xbf, 0xfc, 0x0f, 0xf5, 0x8a, 0x67, 0x1e, 0x7f, 0xfd, 0xc7, + 0xbf, 0xff, 0x7f, 0xff, 0xf7, 0xbf, 0x62, 0xa6, 0xb2, 0x3c, 0x17, 0xa1, 0xad, 0x65, 0x55, 0x87, 0x22, 0x62, 0x8f, + 0x59, 0x95, 0x13, 0x52, 0x4f, 0xb9, 0xdd, 0xa7, 0x09, 0x89, 0x41, 0x25, 0x74, 0xc4, 0xe7, 0x94, 0xa2, 0x4d, 0x54, + 0xbb, 0x86, 0x7c, 0xb0, 0x94, 0x16, 0x1d, 0xf5, 0xdb, 0x3b, 0x6d, 0xbb, 0x5a, 0xde, 0xbe, 0xd1, 0x77, 0x0b, 0x17, + 0xe6, 0x56, 0x89, 0x39, 0xbe, 0x5e, 0xb6, 0xa5, 0x0a, 0x6d, 0x61, 0x49, 0x59, 0x95, 0x5b, 0x18, 0x73, 0x5e, 0xe2, + 0x6b, 0xd0, 0x35, 0x8a, 0x69, 0x95, 0x6b, 0x7d, 0x7a, 0xbf, 0x2c, 0x00, 0xd1, 0x09, 0x2e, 0x8d, 0x08, 0xa0, 0xd1, + 0x79, 0x6a, 0x0b, 0xad, 0x95, 0xe4, 0xa2, 0xa4, 0x51, 0x84, 0x87, 0x73, 0xff, 0xd1, 0xdf, 0x96, 0x7f, 0x9e, 0xa1, + 0x95, 0x60, 0x39, 0xb3, 0xe8, 0x5c, 0xfa, 0x3d, 0x0f, 0xda, 0xed, 0xf9, 0xb9, 0xbb, 0xac, 0x66, 0xf0, 0xae, 0x9a, + 0x8c, 0x82, 0x6e, 0xe6, 0x80, 0x74, 0x10, 0xab, 0xe3, 0x7b, 0x60, 0xea, 0xb7, 0x31, 0x1c, 0x54, 0x96, 0x7f, 0x5a, + 0x52, 0x88, 0x29, 0xfe, 0x0d, 0x0f, 0x4c, 0x65, 0x34, 0x0e, 0x4a, 0x0c, 0x2c, 0x96, 0x46, 0xaf, 0xae, 0xe8, 0x35, + 0xed, 0xac, 0xa6, 0xac, 0x98, 0x07, 0xbe, 0xe6, 0x51, 0xed, 0x7d, 0x3c, 0x7c, 0x9d, 0x76, 0xbc, 0x69, 0x77, 0xa9, + 0x87, 0xe7, 0x3c, 0x9b, 0x99, 0x27, 0xbc, 0x2c, 0x62, 0x23, 0x36, 0x51, 0x51, 0x4c, 0x59, 0x2f, 0x4e, 0x6f, 0xcb, + 0x2f, 0x70, 0xbb, 0x01, 0x6d, 0xb3, 0x88, 0x07, 0xc4, 0xb4, 0x3d, 0xf3, 0x0c, 0x38, 0xc2, 0xd3, 0xf5, 0x6c, 0x69, + 0xcc, 0xd5, 0x13, 0x4d, 0x31, 0x56, 0x58, 0x4f, 0x07, 0x2a, 0x7d, 0xea, 0x6e, 0x0e, 0x25, 0x42, 0x0d, 0xbf, 0xca, + 0xa3, 0xd5, 0x77, 0x35, 0x1f, 0x5d, 0x8a, 0x66, 0x70, 0x8b, 0xd7, 0xd6, 0x0b, 0x35, 0x29, 0x6a, 0x3f, 0x80, 0xf5, + 0x43, 0x60, 0xda, 0xcd, 0x56, 0x54, 0x88, 0xad, 0xde, 0x85, 0xbf, 0x6a, 0x7b, 0x3c, 0x9a, 0xcf, 0xa9, 0xa1, 0x0b, + 0xdc, 0x48, 0x76, 0x35, 0x4a, 0x0a, 0x4a, 0x1b, 0x10, 0xa7, 0xf4, 0xb2, 0x8d, 0x64, 0x5b, 0xf1, 0x24, 0xcf, 0xef, + 0xe9, 0x57, 0x8c, 0xff, 0x7f, 0x2a, 0xa5, 0xd0, 0x17, 0x78, 0x7c, 0x00, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 30ac959e43..00b2e20015 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2,12 +2,12 @@ #include "web_server.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" -#include "esphome/core/entity_base.h" -#include "esphome/core/util.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" #include "StreamString.h" @@ -83,6 +83,13 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) { return match; } +WebServer::WebServer(web_server_base::WebServerBase *base) + : base_(base), entities_iterator_(ListEntitiesIterator(this)) { +#ifdef USE_ESP32 + to_schedule_lock_ = xSemaphoreCreateMutex(); +#endif +} + 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; } @@ -97,7 +104,8 @@ void WebServer::setup() { // Configure reconnect timeout and send config client->send(json::build_json([this](JsonObject root) { - root["title"] = App.get_name(); + root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root["comment"] = App.get_comment(); root["ota"] = this->allow_ota_; root["lang"] = "en"; }).c_str(), @@ -120,7 +128,25 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } -void WebServer::loop() { this->entities_iterator_.advance(); } +void WebServer::loop() { +#ifdef USE_ESP32 + if (xSemaphoreTake(this->to_schedule_lock_, 0L)) { + std::function fn; + if (!to_schedule_.empty()) { + // scheduler execute things out of order which may lead to incorrect state + // this->defer(std::move(to_schedule_.front())); + // let's execute it directly from the loop + fn = std::move(to_schedule_.front()); + to_schedule_.pop_front(); + } + xSemaphoreGive(this->to_schedule_lock_); + if (fn) { + fn(); + } + } +#endif + this->entities_iterator_.advance(); +} void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); @@ -402,6 +428,9 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + if (start_config == DETAIL_ALL) { + root["assumed_state"] = obj->assumed_state(); + } }); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -413,13 +442,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM std::string data = this->switch_json(obj, obj->state, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle(); }); + this->schedule_([obj]() { obj->toggle(); }); request->send(200); } else if (match.method == "turn_on") { - this->defer([obj]() { obj->turn_on(); }); + this->schedule_([obj]() { obj->turn_on(); }); request->send(200); } else if (match.method == "turn_off") { - this->defer([obj]() { obj->turn_off(); }); + this->schedule_([obj]() { obj->turn_off(); }); request->send(200); } else { request->send(404); @@ -441,7 +470,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_POST && match.method == "press") { - this->defer([obj]() { obj->press(); }); + this->schedule_([obj]() { obj->press(); }); request->send(200); return; } else { @@ -497,7 +526,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc std::string data = this->fan_json(obj, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle().perform(); }); + this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); @@ -531,10 +560,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else if (match.method == "turn_off") { - this->defer([obj]() { obj->turn_off().perform(); }); + this->schedule_([obj]() { obj->turn_off().perform(); }); request->send(200); } else { request->send(404); @@ -558,7 +587,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa std::string data = this->light_json(obj, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle().perform(); }); + this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); @@ -590,7 +619,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa call.set_effect(effect); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else if (match.method == "turn_off") { auto call = obj->turn_off(); @@ -598,7 +627,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; call.set_transition_length(length); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else { request->send(404); @@ -663,7 +692,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->hasParam("tilt")) call.set_tilt(request->getParam("tilt")->value().toFloat()); - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -708,7 +737,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM call.set_value(*value_f); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -765,7 +794,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -833,7 +862,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url call.set_target_temperature(*value_f); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -841,13 +870,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } // Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P)((mode_s)), 15) +#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P) ((mode_s)), 15) std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); - int8_t accuracy = traits.get_temperature_accuracy_decimals(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char __buf[16]; if (start_config == DETAIL_ALL) { @@ -884,9 +914,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf bool has_state = false; root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), accuracy); - root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), accuracy); - root["step"] = traits.get_visual_temperature_step(); + root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); + root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root["step"] = traits.get_visual_target_temperature_step(); if (traits.get_supports_action()) { root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); root["state"] = root["action"]; @@ -909,20 +939,20 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } if (traits.get_supports_current_temperature()) { if (!std::isnan(obj->current_temperature)) { - root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, accuracy); + root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); } else { root["current_temperature"] = "NA"; } } if (traits.get_supports_two_point_target_temperature()) { - root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, accuracy); - root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, accuracy); + root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); if (!has_state) { - root["state"] = - value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, accuracy); + root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy); } } else { - root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, accuracy); + root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); if (!has_state) root["state"] = root["target_temperature"]; } @@ -949,13 +979,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat std::string data = this->lock_json(obj, obj->state, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { - this->defer([obj]() { obj->lock(); }); + this->schedule_([obj]() { obj->lock(); }); request->send(200); } else if (match.method == "unlock") { - this->defer([obj]() { obj->unlock(); }); + this->schedule_([obj]() { obj->unlock(); }); request->send(200); } else if (match.method == "open") { - this->defer([obj]() { obj->open(); }); + this->schedule_([obj]() { obj->open(); }); request->send(200); } else { request->send(404); @@ -1154,6 +1184,16 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { bool WebServer::isRequestHandlerTrivial() { return false; } +void WebServer::schedule_(std::function &&f) { +#ifdef USE_ESP32 + xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY); + to_schedule_.push_back(std::move(f)); + xSemaphoreGive(this->to_schedule_lock_); +#else + this->defer(std::move(f)); +#endif +} + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 78d0597e61..f4122ef754 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -9,7 +9,11 @@ #include "esphome/core/controller.h" #include - +#ifdef USE_ESP32 +#include +#include +#include +#endif namespace esphome { namespace web_server { @@ -34,7 +38,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {} + WebServer(web_server_base::WebServerBase *base); /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -220,6 +224,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool isRequestHandlerTrivial() override; protected: + void schedule_(std::function &&f); friend ListEntitiesIterator; web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; @@ -230,6 +235,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *js_include_{nullptr}; bool include_internal_{false}; bool allow_ota_{true}; +#ifdef USE_ESP32 + std::deque> to_schedule_; + SemaphoreHandle_t to_schedule_lock_; +#endif }; } // namespace web_server diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index bc37337ca5..f6d3a84f89 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -4,6 +4,8 @@ #include #include +#include + #include "esphome/core/component.h" #include @@ -81,6 +83,7 @@ class WebServerBase : public Component { return; } this->server_ = std::make_shared(this->port_); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); for (auto *handler : this->handlers_) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index f354ab070d..225423b4db 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -78,7 +78,7 @@ void WhirlpoolClimate::transmit_state() { // 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; + remote_state[3] |= (uint8_t) (temp - this->temperature_min_()) << 4; // Fan speed switch (this->fan_mode.value()) { diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py new file mode 100644 index 0000000000..7b05c43198 --- /dev/null +++ b/esphome/components/wiegand/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import key_provider +from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["key_provider"] + +MULTI_CONF = True + +wiegand_ns = cg.esphome_ns.namespace("wiegand") + +Wiegand = wiegand_ns.class_("Wiegand", key_provider.KeyProvider, cg.Component) +WiegandTagTrigger = wiegand_ns.class_( + "WiegandTagTrigger", automation.Trigger.template(cg.std_string) +) +WiegandRawTrigger = wiegand_ns.class_( + "WiegandRawTrigger", automation.Trigger.template(cg.uint8, cg.uint64) +) +WiegandKeyTrigger = wiegand_ns.class_( + "WiegandKeyTrigger", automation.Trigger.template(cg.uint8) +) + +CONF_D0 = "d0" +CONF_D1 = "d1" +CONF_ON_KEY = "on_key" +CONF_ON_RAW = "on_raw" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wiegand), + cv.Required(CONF_D0): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_D1): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandTagTrigger), + } + ), + cv.Optional(CONF_ON_RAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandRawTrigger), + } + ), + cv.Optional(CONF_ON_KEY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandKeyTrigger), + } + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + pin = await cg.gpio_pin_expression(config[CONF_D0]) + cg.add(var.set_d0_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_D1]) + cg.add(var.set_d1_pin(pin)) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_tag_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_raw_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.uint8, "bits"), (cg.uint64, "value")], conf + ) + + for conf in config.get(CONF_ON_KEY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_key_trigger(trigger)) + await automation.build_automation(trigger, [(cg.uint8, "x")], conf) diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp new file mode 100644 index 0000000000..c4e834c85a --- /dev/null +++ b/esphome/components/wiegand/wiegand.cpp @@ -0,0 +1,117 @@ +#include "wiegand.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace wiegand { + +static const char *const TAG = "wiegand"; +static const char *const KEYS = "0123456789*#"; + +void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { + if (arg->d0.digital_read()) + return; + arg->count++; + arg->value <<= 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { + if (arg->d1.digital_read()) + return; + arg->count++; + arg->value = (arg->value << 1) | 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void Wiegand::setup() { + this->d0_pin_->setup(); + this->store_.d0 = this->d0_pin_->to_isr(); + this->d1_pin_->setup(); + this->store_.d1 = this->d1_pin_->to_isr(); + this->d0_pin_->attach_interrupt(WiegandStore::d0_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->d1_pin_->attach_interrupt(WiegandStore::d1_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); +} + +bool check_eparity(uint64_t value, int start, int length) { + int parity = 0; + uint64_t mask = 1LL << start; + for (int i = 0; i < length; i++, mask <<= 1) { + if (value & mask) + parity++; + } + return !(parity & 1); +} + +bool check_oparity(uint64_t value, int start, int length) { + int parity = 0; + uint64_t mask = 1LL << start; + for (int i = 0; i < length; i++, mask <<= 1) { + if (value & mask) + parity++; + } + return parity & 1; +} + +void Wiegand::loop() { + if (this->store_.done) + return; + if (millis() - this->store_.last_bit_time < 100) + return; + uint8_t count = this->store_.count; + uint64_t value = this->store_.value; + this->store_.count = 0; + this->store_.value = 0; + this->store_.done = true; + ESP_LOGV(TAG, "received %d-bit value: %llx", count, value); + for (auto *trigger : this->raw_triggers_) + trigger->trigger(count, value); + if (count == 26) { + std::string tag = to_string((value >> 1) & 0xffffff); + ESP_LOGD(TAG, "received 26-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 13, 13) || !check_oparity(value, 0, 13)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 34) { + std::string tag = to_string((value >> 1) & 0xffffffff); + ESP_LOGD(TAG, "received 34-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 17, 17) || !check_oparity(value, 0, 17)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 37) { + std::string tag = to_string((value >> 1) & 0x7ffffffff); + ESP_LOGD(TAG, "received 37-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 18, 19) || !check_oparity(value, 0, 19)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 4) { + for (auto *trigger : this->key_triggers_) + trigger->trigger(value); + if (value < 12) { + uint8_t key = KEYS[value]; + this->send_key_(key); + } + } else { + ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value); + } +} + +void Wiegand::dump_config() { + ESP_LOGCONFIG(TAG, "Wiegand reader:"); + LOG_PIN(" D0 pin: ", this->d0_pin_); + LOG_PIN(" D1 pin: ", this->d1_pin_); +} + +} // namespace wiegand +} // namespace esphome diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h new file mode 100644 index 0000000000..994631a3a3 --- /dev/null +++ b/esphome/components/wiegand/wiegand.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace wiegand { + +class Wiegand; + +struct WiegandStore { + ISRInternalGPIOPin d0; + ISRInternalGPIOPin d1; + volatile uint64_t value{0}; + volatile uint32_t last_bit_time{0}; + volatile bool done{true}; + volatile uint8_t count{0}; + + static void d0_gpio_intr(WiegandStore *arg); + static void d1_gpio_intr(WiegandStore *arg); +}; + +class WiegandTagTrigger : public Trigger {}; + +class WiegandRawTrigger : public Trigger {}; + +class WiegandKeyTrigger : public Trigger {}; + +class Wiegand : public key_provider::KeyProvider, public Component { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + + void set_d0_pin(InternalGPIOPin *pin) { this->d0_pin_ = pin; }; + void set_d1_pin(InternalGPIOPin *pin) { this->d1_pin_ = pin; }; + void register_tag_trigger(WiegandTagTrigger *trig) { this->tag_triggers_.push_back(trig); } + void register_raw_trigger(WiegandRawTrigger *trig) { this->raw_triggers_.push_back(trig); } + void register_key_trigger(WiegandKeyTrigger *trig) { this->key_triggers_.push_back(trig); } + + protected: + InternalGPIOPin *d0_pin_; + InternalGPIOPin *d1_pin_; + WiegandStore store_{}; + std::vector tag_triggers_; + std::vector raw_triggers_; + std::vector key_triggers_; +}; + +} // namespace wiegand +} // namespace esphome diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 846a8e1303..c9da07795c 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -252,6 +252,7 @@ def _validate(config): CONF_OUTPUT_POWER = "output_power" +CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -267,7 +268,7 @@ CONFIG_SCHEMA = cv.All( CONF_REBOOT_TIMEOUT, default="15min" ): cv.positive_time_period_milliseconds, cv.SplitDefault( - CONF_POWER_SAVE_MODE, esp8266="none", esp32="light" + CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", rp2040="light" ): 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, @@ -280,6 +281,7 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All( cv.boolean, cv.only_with_esp_idf ), + cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional("enable_mdns"): cv.invalid( "This option has been removed. Please use the [disabled] option under the " "new mdns component instead." @@ -332,8 +334,7 @@ def manual_ip(config): ) -def wifi_network(config, static_ip): - ap = cg.variable(config[CONF_ID], WiFiAP()) +def wifi_network(config, ap, static_ip): if CONF_SSID in config: cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: @@ -360,19 +361,27 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) - for network in config.get(CONF_NETWORKS, []): + def add_sta(ap, network): ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) - cg.add(var.add_sta(wifi_network(network, ip_config))) + cg.add(var.add_sta(wifi_network(network, ap, ip_config))) + + for network in config.get(CONF_NETWORKS, []): + cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] - ip_config = conf.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) - cg.add(var.set_ap(wifi_network(conf, ip_config))) + ip_config = conf.get(CONF_MANUAL_IP) + cg.with_local_variable( + conf[CONF_ID], + WiFiAP(), + lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), + ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) 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])) + cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) @@ -380,6 +389,8 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFi", None) + elif CORE.is_rp2040: + cg.add_library("WiFi", None) if CORE.is_esp32 and CORE.using_esp_idf: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 37f5276c8f..9f047dd5ed 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,4 +1,5 @@ #include "wifi_component.h" +#include #if defined(USE_ESP32) || defined(USE_ESP_IDF) #include @@ -35,10 +36,12 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up WiFi..."); + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); this->last_connected_ = millis(); this->wifi_pre_setup_(); - uint32_t hash = fnv1_hash(App.get_compilation_time()); + uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; + this->pref_ = global_preferences->make_preference(hash, true); SavedWifiSettings save{}; @@ -368,7 +371,7 @@ void WiFiComponent::print_connect_params_() { if (this->selected_ap_.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); } - ESP_LOGCONFIG(TAG, " Channel: %d", wifi_channel_()); + ESP_LOGCONFIG(TAG, " Channel: %" PRId32, wifi_channel_()); ESP_LOGCONFIG(TAG, " Subnet: %s", wifi_subnet_mask_().str().c_str()); ESP_LOGCONFIG(TAG, " Gateway: %s", wifi_gateway_ip_().str().c_str()); ESP_LOGCONFIG(TAG, " DNS1: %s", wifi_dns_ip_(0).str().c_str()); @@ -382,7 +385,7 @@ void WiFiComponent::print_connect_params_() { void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan..."); - this->wifi_scan_start_(); + this->wifi_scan_start_(this->passive_scan_); this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } @@ -612,6 +615,8 @@ bool WiFiComponent::is_connected() { } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } + std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { char buf[20]; sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); @@ -706,6 +711,8 @@ int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } +bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; } + WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace wifi diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6c5202ed7a..3f81b94cce 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -5,7 +5,9 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" + #include +#include #ifdef USE_ESP32_FRAMEWORK_ARDUINO #include @@ -24,6 +26,16 @@ extern "C" { #endif #endif +#ifdef USE_RP2040 +extern "C" { +#include "cyw43.h" +#include "cyw43_country.h" +#include "pico/cyw43_arch.h" +} + +#include +#endif + namespace esphome { namespace wifi { @@ -138,6 +150,8 @@ class WiFiScanResult { float get_priority() const { return priority_; } void set_priority(float priority) { priority_ = priority; } + bool operator==(const WiFiScanResult &rhs) const; + protected: bool matches_{false}; bssid_t bssid_; @@ -203,6 +217,8 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_output_power(float output_power) { output_power_ = output_power; } + void set_passive_scan(bool passive); + void save_wifi_sta(const std::string &ssid, const std::string &password); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -280,7 +296,7 @@ class WiFiComponent : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_(); - bool wifi_scan_start_(); + bool wifi_scan_start_(bool passive); bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); bool wifi_disconnect_(); @@ -310,6 +326,11 @@ class WiFiComponent : public Component { void wifi_process_event_(IDFWiFiEvent *data); #endif +#ifdef USE_RP2040 + static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); + void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); +#endif + std::string use_address_; std::vector sta_; std::vector sta_priorities_; @@ -330,6 +351,7 @@ class WiFiComponent : public Component { bool scan_done_{false}; bool ap_setup_{false}; optional output_power_; + bool passive_scan_{false}; ESPPreferenceObject pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index e893712287..f35f5dfc43 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -4,19 +4,19 @@ #include -#include #include +#include #ifdef USE_WIFI_WPA2_EAP #include #endif -#include "lwip/err.h" -#include "lwip/dns.h" #include "lwip/apps/sntp.h" +#include "lwip/dns.h" +#include "lwip/err.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include "esphome/core/application.h" #include "esphome/core/util.h" namespace esphome { @@ -128,13 +128,23 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } ip_addr_t dns; +#if LWIP_IPV6 dns.type = IPADDR_TYPE_V4; +#endif if (uint32_t(manual_ip->dns1) != 0) { +#if LWIP_IPV6 dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); +#else + dns.addr = static_cast(manual_ip->dns1); +#endif dns_setserver(0, &dns); } if (uint32_t(manual_ip->dns2) != 0) { +#if LWIP_IPV6 dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); +#else + dns.addr = static_cast(manual_ip->dns2); +#endif dns_setserver(1, &dns); } @@ -266,8 +276,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); } } - esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); - err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + err = esp_wifi_sta_wpa2_ent_enable(); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); } @@ -609,13 +618,13 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { } return WiFiSTAConnectStatus::IDLE; } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) return false; // need to use WiFi because of WiFiScanClass allocations :( - int16_t err = WiFi.scanNetworks(true, true, false, 200); + int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); return false; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index de4253fe41..8b38297b17 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -601,7 +601,7 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { return WiFiSTAConnectStatus::IDLE; } } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { static bool first_scan = false; // enable STA @@ -615,13 +615,21 @@ bool WiFiComponent::wifi_scan_start_() { config.channel = 0; config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) - config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; if (first_scan) { - config.scan_time.active.min = 100; - config.scan_time.active.max = 200; + if (passive) { + config.scan_time.passive = 200; + } else { + config.scan_time.active.min = 100; + config.scan_time.active.max = 200; + } } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + if (passive) { + config.scan_time.passive = 500; + } else { + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; + } } #endif first_scan = false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2883164495..1c70f33040 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -11,11 +11,13 @@ #include #include +#include #include #include #ifdef USE_WIFI_WPA2_EAP #include #endif +#include "dhcpserver/dhcpserver.h" #include "lwip/err.h" #include "lwip/dns.h" @@ -31,7 +33,7 @@ namespace wifi { static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static xQueueHandle s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -413,17 +415,17 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (!this->wifi_mode_(true, {})) return false; - tcpip_adapter_dhcp_status_t dhcp_status; - esp_err_t err = tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); + esp_netif_dhcp_status_t dhcp_status; + esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcpc_get_status failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err)); return false; } if (!manual_ip.has_value()) { - // Use DHCP client - if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { - err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); + // No manual IP is set; use DHCP client + if (dhcp_status != ESP_NETIF_DHCP_STARTED) { + err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); } @@ -432,33 +434,29 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); + esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw info.ip.addr = static_cast(manual_ip->static_ip); info.gw.addr = static_cast(manual_ip->gateway); info.netmask.addr = static_cast(manual_ip->subnet); - - err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); + err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "tcpip_adapter_dhcpc_stop failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_dhcpc_stop failed: %s", esp_err_to_name(err)); return false; } - - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); + err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_set_ip_info failed: %s", esp_err_to_name(err)); return false; } - ip_addr_t dns; - dns.type = IPADDR_TYPE_V4; + esp_netif_dns_info_t dns; if (uint32_t(manual_ip->dns1) != 0) { - dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); - dns_setserver(0, &dns); + dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns1); + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns); } if (uint32_t(manual_ip->dns2) != 0) { - dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); - dns_setserver(1, &dns); + dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns2); + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns); } return true; @@ -467,10 +465,10 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { network::IPAddress WiFiComponent::wifi_sta_ip() { if (!this->has_sta()) return {}; - tcpip_adapter_ip_info_t ip; - esp_err_t err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_get_ip_info failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); return false; } return {ip.ip.addr}; @@ -590,9 +588,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { ESP_LOGV(TAG, "Event: WiFi STA start"); // apply hostname - err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); if (err != ERR_OK) { - ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); } s_sta_started = true; @@ -639,8 +637,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; -#ifdef LWIP_IPV6_AUTOCONFIG - tcpip_adapter_create_ip6_linklocal(TCPIP_ADAPTER_IF_STA); +#if LWIP_IPV6_AUTOCONFIG + esp_netif_create_ip6_linklocal(s_sta_netif); #endif ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); @@ -656,7 +654,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { const auto &it = data->data.sta_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Event: WiFi Scan Done status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); scan_result_.clear(); this->scan_done_ = true; @@ -725,7 +723,7 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { } return WiFiSTAConnectStatus::IDLE; } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -735,9 +733,13 @@ bool WiFiComponent::wifi_scan_start_() { config.bssid = nullptr; config.channel = 0; config.show_hidden = true; - config.scan_type = WIFI_SCAN_TYPE_ACTIVE; - config.scan_time.active.min = 100; - config.scan_time.active.max = 300; + config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + if (passive) { + config.scan_time.passive = 300; + } else { + config.scan_time.active.min = 100; + config.scan_time.active.max = 300; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { @@ -755,8 +757,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (!this->wifi_mode_({}, true)) return false; - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); + esp_netif_ip_info_t info; if (manual_ip.has_value()) { info.ip.addr = static_cast(manual_ip->static_ip); info.gw.addr = static_cast(manual_ip->gateway); @@ -766,17 +767,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { info.gw.addr = static_cast(network::IPAddress(192, 168, 4, 1)); info.netmask.addr = static_cast(network::IPAddress(255, 255, 255, 0)); } - tcpip_adapter_dhcp_status_t dhcp_status; - tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); - err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP); + esp_netif_dhcp_status_t dhcp_status; + esp_netif_dhcps_get_status(s_sta_netif, &dhcp_status); + err = esp_netif_dhcps_stop(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_stop failed! %d", err); return false; } - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err); + ESP_LOGV(TAG, "esp_netif_set_ip_info failed! %d", err); return false; } @@ -789,17 +790,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { start_address[3] += 100; lease.end_ip.addr = static_cast(start_address); ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); - err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + err = esp_netif_dhcps_option(s_sta_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_option failed! %d", err); return false; } - err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + err = esp_netif_dhcps_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_start failed! %d", err); return false; } @@ -845,8 +846,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(s_sta_netif, &ip); return {ip.ip.addr}; } bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } @@ -912,7 +913,11 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { const ip_addr_t *dns_ip = dns_getserver(num); +#if LWIP_IPV6 return {dns_ip->u_addr.ip4.addr}; +#else + return {dns_ip->addr}; +#endif } } // namespace wifi diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp new file mode 100644 index 0000000000..489ebc3699 --- /dev/null +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -0,0 +1,193 @@ + +#include "wifi_component.h" + +#ifdef USE_RP2040 + +#include "lwip/dns.h" +#include "lwip/err.h" +#include "lwip/netif.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace wifi { + +static const char *const TAG = "wifi_pico_w"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + if (sta.has_value()) { + if (sta.value()) { + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE); + } + } + if (ap.has_value()) { + if (ap.value()) { + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE); + } + } + return true; +} + +bool WiFiComponent::wifi_apply_power_save_() { + uint32_t pm; + switch (this->power_save_) { + case WIFI_POWER_SAVE_NONE: + pm = CYW43_PERFORMANCE_PM; + break; + case WIFI_POWER_SAVE_LIGHT: + pm = CYW43_DEFAULT_PM; + break; + case WIFI_POWER_SAVE_HIGH: + pm = CYW43_AGGRESSIVE_PM; + break; + } + int ret = cyw43_wifi_pm(&cyw43_state, pm); + return ret == 0; +} + +// TODO: The driver doesnt seem to have an API for this +bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } + +bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) + return false; + + auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); + if (ret != WL_CONNECTED) + return false; + + return true; +} + +bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } + +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + if (!manual_ip.has_value()) { + return true; + } + + IPAddress ip_address = IPAddress(manual_ip->static_ip); + IPAddress gateway = IPAddress(manual_ip->gateway); + IPAddress subnet = IPAddress(manual_ip->subnet); + + IPAddress dns = IPAddress(manual_ip->dns1); + + WiFi.config(ip_address, dns, gateway, subnet); + return true; +} + +bool WiFiComponent::wifi_apply_hostname_() { + WiFi.setHostname(App.get_name().c_str()); + return true; +} +const char *get_auth_mode_str(uint8_t mode) { + // TODO: + return "UNKNOWN"; +} +const char *get_disconnect_reason_str(uint8_t reason) { + // TODO: + return "UNKNOWN"; +} + +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { + int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + switch (status) { + case CYW43_LINK_JOIN: + case CYW43_LINK_NOIP: + return WiFiSTAConnectStatus::CONNECTING; + case CYW43_LINK_UP: + return WiFiSTAConnectStatus::CONNECTED; + case CYW43_LINK_FAIL: + case CYW43_LINK_BADAUTH: + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + case CYW43_LINK_NONET: + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + } + return WiFiSTAConnectStatus::IDLE; +} + +int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + global_wifi_component->wifi_scan_result(env, result); + return 0; +} + +void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + bssid_t bssid; + std::copy(result->bssid, result->bssid + 6, bssid.begin()); + std::string ssid(reinterpret_cast(result->ssid)); + WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty()); + if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { + this->scan_result_.push_back(res); + } +} + +bool WiFiComponent::wifi_scan_start_(bool passive) { + this->scan_result_.clear(); + this->scan_done_ = false; + cyw43_wifi_scan_options_t scan_options = {0}; + scan_options.scan_type = passive ? 1 : 0; + int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); + if (err) { + ESP_LOGV(TAG, "cyw43_wifi_scan failed!"); + } + return err == 0; + return true; +} + +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + // TODO: + return false; +} + +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + if (!this->wifi_mode_({}, true)) + return false; + + WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); + + return true; +} +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.localIP()}; } + +bool WiFiComponent::wifi_disconnect_() { + int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); + return err == 0; +} + +bssid_t WiFiComponent::wifi_bssid() { + bssid_t bssid{}; + uint8_t raw_bssid[6]; + WiFi.BSSID(raw_bssid); + for (size_t i = 0; i < bssid.size(); i++) + bssid[i] = raw_bssid[i]; + return bssid; +} +std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); } + +network::IPAddress WiFiComponent::wifi_sta_ip() { return {WiFi.localIP()}; } +network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } +network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } +network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { + const ip_addr_t *dns_ip = dns_getserver(num); + return {dns_ip->addr}; +} + +void WiFiComponent::wifi_loop_() { + if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { + this->scan_done_ = true; + ESP_LOGV(TAG, "Scan done!"); + } +} + +void WiFiComponent::wifi_pre_setup_() {} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wl_134/__init__.py b/esphome/components/wl_134/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wl_134/text_sensor.py b/esphome/components/wl_134/text_sensor.py new file mode 100644 index 0000000000..1373df77f4 --- /dev/null +++ b/esphome/components/wl_134/text_sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, uart +from esphome.const import ( + ICON_FINGERPRINT, +) + +CODEOWNERS = ["@hobbypunk90"] +DEPENDENCIES = ["uart"] +CONF_RESET = "reset" + +wl134_ns = cg.esphome_ns.namespace("wl_134") +Wl134Component = wl134_ns.class_( + "Wl134Component", text_sensor.TextSensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema( + Wl134Component, + icon=ICON_FINGERPRINT, + ) + .extend({cv.Optional(CONF_RESET, default=False): cv.boolean}) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_do_reset(config[CONF_RESET])) + await uart.register_uart_device(var, config) diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp new file mode 100644 index 0000000000..3ffa0c63ce --- /dev/null +++ b/esphome/components/wl_134/wl_134.cpp @@ -0,0 +1,111 @@ +#include "wl_134.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wl_134 { + +static const char *const TAG = "wl_134.sensor"; +static const uint8_t ASCII_CR = 0x0D; +static const uint8_t ASCII_NBSP = 0xFF; +static const int MAX_DATA_LENGTH_BYTES = 6; + +void Wl134Component::setup() { this->publish_state(""); } + +void Wl134Component::loop() { + while (this->available() >= RFID134_PACKET_SIZE) { + Wl134Component::Rfid134Error error = this->read_packet_(); + if (error != RFID134_ERROR_NONE) { + ESP_LOGW(TAG, "Error: %d", error); + } + } +} + +Wl134Component::Rfid134Error Wl134Component::read_packet_() { + uint8_t packet[RFID134_PACKET_SIZE]; + packet[RFID134_PACKET_START_CODE] = this->read(); + + // check for the first byte being the packet start code + if (packet[RFID134_PACKET_START_CODE] != 0x02) { + // just out of sync, ignore until we are synced up + return RFID134_ERROR_NONE; + } + + if (!this->read_array(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_SIZE - 1)) { + return RFID134_ERROR_PACKET_SIZE; + } + + if (packet[RFID134_PACKET_END_CODE] != 0x03) { + return RFID134_ERROR_PACKET_END_CODE_MISSMATCH; + } + + // calculate checksum + uint8_t checksum = 0; + for (uint8_t i = RFID134_PACKET_ID; i < RFID134_PACKET_CHECKSUM; i++) { + checksum = checksum ^ packet[i]; + } + + // test checksum + if (checksum != packet[RFID134_PACKET_CHECKSUM]) { + return RFID134_ERROR_PACKET_CHECKSUM; + } + + if (static_cast(~checksum) != static_cast(packet[RFID134_PACKET_CHECKSUM_INVERT])) { + return RFID134_ERROR_PACKET_CHECKSUM_INVERT; + } + + Rfid134Reading reading; + + // convert packet into the reading struct + reading.id = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_ID]), RFID134_PACKET_COUNTRY - RFID134_PACKET_ID); + reading.country = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_COUNTRY]), + RFID134_PACKET_DATA_FLAG - RFID134_PACKET_COUNTRY); + reading.isData = packet[RFID134_PACKET_DATA_FLAG] == '1'; + reading.isAnimal = packet[RFID134_PACKET_ANIMAL_FLAG] == '1'; + reading.reserved0 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED0]), + RFID134_PACKET_RESERVED1 - RFID134_PACKET_RESERVED0); + reading.reserved1 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED1]), + RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); + + ESP_LOGV(TAG, "Tag id: %012lld", reading.id); + ESP_LOGV(TAG, "Country: %03d", reading.country); + ESP_LOGV(TAG, "isData: %s", reading.isData ? "true" : "false"); + ESP_LOGV(TAG, "isAnimal: %s", reading.isAnimal ? "true" : "false"); + ESP_LOGV(TAG, "Reserved0: %d", reading.reserved0); + ESP_LOGV(TAG, "Reserved1: %d", reading.reserved1); + + char buf[20]; + sprintf(buf, "%03d%012lld", reading.country, reading.id); + this->publish_state(buf); + if (this->do_reset_) { + this->set_timeout(1000, [this]() { this->publish_state(""); }); + } + + return RFID134_ERROR_NONE; +} + +uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size) { + uint64_t value = 0; + uint8_t i = text_size; + do { + i--; + + uint8_t digit = text[i]; + if (digit >= 'A') { + digit = digit - 'A' + 10; + } else { + digit = digit - '0'; + } + value = (value << 4) + digit; + } while (i != 0); + + return value; +} + +void Wl134Component::dump_config() { + ESP_LOGCONFIG(TAG, "WL-134 Sensor:"); + LOG_TEXT_SENSOR("", "Tag", this); + // As specified in the sensor's data sheet + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); +} +} // namespace wl_134 +} // namespace esphome diff --git a/esphome/components/wl_134/wl_134.h b/esphome/components/wl_134/wl_134.h new file mode 100644 index 0000000000..c0a90de17d --- /dev/null +++ b/esphome/components/wl_134/wl_134.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace wl_134 { + +class Wl134Component : public text_sensor::TextSensor, public Component, public uart::UARTDevice { + public: + enum Rfid134Error { + RFID134_ERROR_NONE, + + // from library + RFID134_ERROR_PACKET_SIZE = 0x81, + RFID134_ERROR_PACKET_END_CODE_MISSMATCH, + RFID134_ERROR_PACKET_CHECKSUM, + RFID134_ERROR_PACKET_CHECKSUM_INVERT + }; + + struct Rfid134Reading { + uint16_t country; + uint64_t id; + bool isData; + bool isAnimal; + uint16_t reserved0; + uint32_t reserved1; + }; + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void setup() override; + void loop() override; + void dump_config() override; + + void set_do_reset(bool do_reset) { this->do_reset_ = do_reset; } + + private: + enum DfMp3Packet { + RFID134_PACKET_START_CODE, + RFID134_PACKET_ID = 1, + RFID134_PACKET_COUNTRY = 11, + RFID134_PACKET_DATA_FLAG = 15, + RFID134_PACKET_ANIMAL_FLAG = 16, + RFID134_PACKET_RESERVED0 = 17, + RFID134_PACKET_RESERVED1 = 21, + RFID134_PACKET_CHECKSUM = 27, + RFID134_PACKET_CHECKSUM_INVERT = 28, + RFID134_PACKET_END_CODE = 29, + RFID134_PACKET_SIZE + }; + + bool do_reset_; + + Rfid134Error read_packet_(); + uint64_t hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size); +}; + +} // namespace wl_134 +} // namespace esphome diff --git a/esphome/components/x9c/__init__.py b/esphome/components/x9c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/x9c/output.py b/esphome/components/x9c/output.py new file mode 100644 index 0000000000..44e9d729b3 --- /dev/null +++ b/esphome/components/x9c/output.py @@ -0,0 +1,46 @@ +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_CS_PIN, + CONF_INC_PIN, + CONF_UD_PIN, + CONF_INITIAL_VALUE, +) + +CODEOWNERS = ["@EtienneMD"] + +x9c_ns = cg.esphome_ns.namespace("x9c") + +X9cOutput = x9c_ns.class_("X9cOutput", output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(X9cOutput), + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_INC_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_UD_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_INITIAL_VALUE, default=1.0): cv.float_range( + min=0.01, max=1.0 + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + cs_pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(cs_pin)) + inc_pin = await cg.gpio_pin_expression(config[CONF_INC_PIN]) + cg.add(var.set_inc_pin(inc_pin)) + ud_pin = await cg.gpio_pin_expression(config[CONF_UD_PIN]) + cg.add(var.set_ud_pin(ud_pin)) + + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp new file mode 100644 index 0000000000..ff7777e71f --- /dev/null +++ b/esphome/components/x9c/x9c.cpp @@ -0,0 +1,72 @@ +#include "x9c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace x9c { + +static const char *const TAG = "x9c.output"; + +void X9cOutput::trim_value(int change_amount) { + if (change_amount > 0) { // Set change direction + this->ud_pin_->digital_write(true); + } else { + this->ud_pin_->digital_write(false); + } + + this->inc_pin_->digital_write(true); + this->cs_pin_->digital_write(false); // Select chip + + for (int i = 0; i < abs(change_amount); i++) { // Move wiper + this->inc_pin_->digital_write(true); + delayMicroseconds(1); + this->inc_pin_->digital_write(false); + delayMicroseconds(1); + } + + delayMicroseconds(100); // Let value settle + + this->inc_pin_->digital_write(false); + this->cs_pin_->digital_write(true); // Deselect chip safely (no save) +} + +void X9cOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up X9C Potentiometer with initial value of %f", this->initial_value_); + + this->inc_pin_->get_pin(); + this->inc_pin_->setup(); + this->inc_pin_->digital_write(false); + + this->cs_pin_->get_pin(); + this->cs_pin_->setup(); + this->cs_pin_->digital_write(true); + + this->ud_pin_->get_pin(); + this->ud_pin_->setup(); + + if (this->initial_value_ <= 0.50) { + this->trim_value(-101); // Set min value (beyond 0) + this->trim_value((int) (this->initial_value_ * 100)); + } else { + this->trim_value(101); // Set max value (beyond 100) + this->trim_value((int) (this->initial_value_ * 100) - 100); + } + this->pot_value_ = this->initial_value_; + this->write_state(this->initial_value_); +} + +void X9cOutput::write_state(float state) { + this->trim_value((int) ((state - this->pot_value_) * 100)); + this->pot_value_ = state; +} + +void X9cOutput::dump_config() { + ESP_LOGCONFIG(TAG, "X9C Potentiometer Output:"); + LOG_PIN(" Chip Select Pin: ", this->cs_pin_); + LOG_PIN(" Increment Pin: ", this->inc_pin_); + LOG_PIN(" Up/Down Pin: ", this->ud_pin_); + ESP_LOGCONFIG(TAG, " Initial Value: %f", this->initial_value_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace x9c +} // namespace esphome diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h new file mode 100644 index 0000000000..924460c841 --- /dev/null +++ b/esphome/components/x9c/x9c.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace x9c { + +class X9cOutput : public output::FloatOutput, public Component { + public: + void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; } + void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; } + void set_ud_pin(InternalGPIOPin *pin) { ud_pin_ = pin; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + + void setup() override; + void dump_config() override; + + void trim_value(int change_amount); + + protected: + void write_state(float state) override; + InternalGPIOPin *cs_pin_; + InternalGPIOPin *inc_pin_; + InternalGPIOPin *ud_pin_; + float initial_value_; + float pot_value_; +}; + +} // namespace x9c +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 95d97defe2..95faea0446 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -239,12 +239,12 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c } uint8_t mac_reverse[6] = {0}; - mac_reverse[5] = (uint8_t)(address >> 40); - mac_reverse[4] = (uint8_t)(address >> 32); - mac_reverse[3] = (uint8_t)(address >> 24); - mac_reverse[2] = (uint8_t)(address >> 16); - mac_reverse[1] = (uint8_t)(address >> 8); - mac_reverse[0] = (uint8_t)(address >> 0); + mac_reverse[5] = (uint8_t) (address >> 40); + mac_reverse[4] = (uint8_t) (address >> 32); + mac_reverse[3] = (uint8_t) (address >> 24); + mac_reverse[2] = (uint8_t) (address >> 16); + mac_reverse[1] = (uint8_t) (address >> 8); + mac_reverse[0] = (uint8_t) (address >> 0); XiaomiAESVector vector{.key = {0}, .plaintext = {0}, diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 399bef83b8..c1086605d1 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -3,6 +3,8 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/component.h" +#include + #ifdef USE_ESP32 namespace esphome { diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index cff61f153b..4523bbc82b 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + #ifdef USE_ESP32 namespace esphome { diff --git a/esphome/components/xpt2046/__init__.py b/esphome/components/xpt2046/__init__.py index 3de89a6425..3b8a925bb2 100644 --- a/esphome/components/xpt2046/__init__.py +++ b/esphome/components/xpt2046/__init__.py @@ -1,129 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation -from esphome import pins -from esphome.components import spi -from esphome.const import CONF_ID, CONF_ON_STATE, CONF_THRESHOLD, CONF_TRIGGER_ID -CODEOWNERS = ["@numo68"] -AUTO_LOAD = ["binary_sensor"] -DEPENDENCIES = ["spi"] -MULTI_CONF = True - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" -CONF_SWAP_X_Y = "swap_x_y" -CONF_IRQ_PIN = "irq_pin" - -xpt2046_ns = cg.esphome_ns.namespace("xpt2046") -CONF_XPT2046_ID = "xpt2046_id" - -XPT2046Component = xpt2046_ns.class_( - "XPT2046Component", cg.PollingComponent, spi.SPIDevice +CONFIG_SCHEMA = cv.invalid( + "This component sould now be used as platform of the Touchscreen component." ) - -XPT2046OnStateTrigger = xpt2046_ns.class_( - "XPT2046OnStateTrigger", automation.Trigger.template(cg.int_, cg.int_, cg.bool_) -) - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_IRQ_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_DIMENSION_X, default=100): cv.positive_not_null_int, - cv.Optional(CONF_DIMENSION_Y, default=100): cv.positive_not_null_int, - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - XPT2046OnStateTrigger - ), - } - ), - } - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), - validate_xpt2046, -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await spi.register_spi_device(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_dimensions(config[CONF_DIMENSION_X], config[CONF_DIMENSION_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_SWAP_X_Y in config: - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - - if CONF_IRQ_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) - cg.add(var.set_irq_pin(pin)) - - for conf in config.get(CONF_ON_STATE, []): - await automation.build_automation( - var.get_on_state_trigger(), - [(cg.int_, "x"), (cg.int_, "y"), (cg.bool_, "touched")], - conf, - ) diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py index 6ec09a2295..5a6cfe4919 100644 --- a/esphome/components/xpt2046/binary_sensor.py +++ b/esphome/components/xpt2046/binary_sensor.py @@ -1,55 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import binary_sensor -from . import ( - xpt2046_ns, - XPT2046Component, - CONF_XPT2046_ID, -) - -CONF_X_MIN = "x_min" -CONF_X_MAX = "x_max" -CONF_Y_MIN = "y_min" -CONF_Y_MAX = "y_max" - -DEPENDENCIES = ["xpt2046"] -XPT2046Button = xpt2046_ns.class_("XPT2046Button", binary_sensor.BinarySensor) - - -def validate_xpt2046_button(config): - if cv.int_(config[CONF_X_MAX]) < cv.int_(config[CONF_X_MIN]) or cv.int_( - config[CONF_Y_MAX] - ) < cv.int_(config[CONF_Y_MIN]): - raise cv.Invalid("x_max is less than x_min or y_max is less than y_min") - - return config - - -CONFIG_SCHEMA = cv.All( - binary_sensor.binary_sensor_schema(XPT2046Button).extend( - { - cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component), - cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095), - cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095), - } - ), - validate_xpt2046_button, -) - - -async def to_code(config): - var = await binary_sensor.new_binary_sensor(config) - hub = await cg.get_variable(config[CONF_XPT2046_ID]) - cg.add( - var.set_area( - config[CONF_X_MIN], - config[CONF_X_MAX], - config[CONF_Y_MIN], - config[CONF_Y_MAX], - ) - ) - - cg.add(hub.register_button(var)) +CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py new file mode 100644 index 0000000000..e45b723179 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen.py @@ -0,0 +1,117 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import spi, touchscreen +from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", + touchscreen.Touchscreen, + cg.PollingComponent, + spi.SPIDevice, +) + +CONF_REPORT_INTERVAL = "report_interval" +CONF_CALIBRATION_X_MIN = "calibration_x_min" +CONF_CALIBRATION_X_MAX = "calibration_x_max" +CONF_CALIBRATION_Y_MIN = "calibration_y_min" +CONF_CALIBRATION_Y_MAX = "calibration_y_max" +CONF_SWAP_X_Y = "swap_x_y" + +# obsolete Keys +CONF_DIMENSION_X = "dimension_x" +CONF_DIMENSION_Y = "dimension_y" +CONF_IRQ_PIN = "irq_pin" + + +def validate_xpt2046(config): + if ( + abs( + cv.int_(config[CONF_CALIBRATION_X_MAX]) + - cv.int_(config[CONF_CALIBRATION_X_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration X values difference < 1000") + + if ( + abs( + cv.int_(config[CONF_CALIBRATION_Y_MAX]) + - cv.int_(config[CONF_CALIBRATION_Y_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration Y values difference < 1000") + + return config + + +def report_interval(value): + if value == "never": + return 4294967295 # uint32_t max + return cv.positive_time_period_milliseconds(value) + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, + cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, + # obsolete Keys + cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), + cv.Optional(CONF_DIMENSION_X): cv.invalid( + "This key is now obsolete, please remove it" + ), + cv.Optional(CONF_DIMENSION_Y): cv.invalid( + "This key is now obsolete, please remove it" + ), + }, + ) + .extend(cv.polling_component_schema("50ms")) + .extend(spi.spi_device_schema()), +).add_extra(validate_xpt2046) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + await touchscreen.register_touchscreen(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) + cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) + cg.add( + var.set_calibration( + config[CONF_CALIBRATION_X_MIN], + config[CONF_CALIBRATION_X_MAX], + config[CONF_CALIBRATION_Y_MIN], + config[CONF_CALIBRATION_Y_MAX], + ) + ) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp index aaadeea52e..6c7c55a995 100644 --- a/esphome/components/xpt2046/xpt2046.cpp +++ b/esphome/components/xpt2046/xpt2046.cpp @@ -9,118 +9,126 @@ namespace xpt2046 { static const char *const TAG = "xpt2046"; +void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } + void XPT2046Component::setup() { if (this->irq_pin_ != nullptr) { // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state // while the channels are read and wiring it as an interrupt is not straightforward and would // need careful masking. A GPIO poll is cheap so we'll just use that. + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); } spi_setup(); read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin } void XPT2046Component::loop() { - if (this->irq_pin_ != nullptr) { - // Force immediate update if a falling edge (= touched is seen) Ignore if still active - // (that would mean that we missed the release because of a too long update interval) - bool val = this->irq_pin_->digital_read(); - if (!val && this->last_irq_ && !this->touched) { - ESP_LOGD(TAG, "Falling penirq edge, forcing update"); - update(); - } - this->last_irq_ = val; + if ((this->irq_pin_ != nullptr) && (this->store_.touch || this->touched)) { + this->store_.touch = false; + check_touch_(); } } void XPT2046Component::update() { + if (this->irq_pin_ == nullptr) + check_touch_(); +} + +void XPT2046Component::check_touch_() { int16_t data[6]; bool touch = false; uint32_t now = millis(); - this->z_raw = 0; + enable(); - // In case the penirq pin is present only do the SPI transaction if it reports a touch (is low). - // The touch has to be also confirmed with checking the pressure over threshold - if (this->irq_pin_ == nullptr || !this->irq_pin_->digital_read()) { - enable(); + int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - int16_t z1 = read_adc_(0xB1 /* Z1 */); - int16_t z2 = read_adc_(0xC1 /* Z2 */); + this->z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - this->z_raw = z1 + 4095 - z2; - - touch = (this->z_raw >= this->threshold_); - if (touch) { - read_adc_(0x91 /* Y */); // dummy Y measure, 1st is always noisy - data[0] = read_adc_(0xD1 /* X */); - data[1] = read_adc_(0x91 /* Y */); // make 3 x-y measurements - data[2] = read_adc_(0xD1 /* X */); - data[3] = read_adc_(0x91 /* Y */); - data[4] = read_adc_(0xD1 /* X */); - } - - data[5] = read_adc_(0x90 /* Y */); // Last Y touch power down - - disable(); + touch = (this->z_raw >= this->threshold_); + if (touch) { + read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy + data[0] = read_adc_(0x91 /* Y */); + data[1] = read_adc_(0xD1 /* X */); // make 3 x-y measurements + data[2] = read_adc_(0x91 /* Y */); + data[3] = read_adc_(0xD1 /* X */); + data[4] = read_adc_(0x91 /* Y */); } - if (touch) { - this->x_raw = best_two_avg(data[0], data[2], data[4]); - this->y_raw = best_two_avg(data[1], data[3], data[5]); - } else { - this->x_raw = this->y_raw = 0; - } + data[5] = read_adc_(0xD0 /* X */); // Last X touch power down - ESP_LOGV(TAG, "Update [x, y] = [%d, %d], z = %d%s", this->x_raw, this->y_raw, this->z_raw, (touch ? " touched" : "")); + disable(); if (touch) { - // Normalize raw data according to calibration min and max + this->x_raw = best_two_avg(data[1], data[3], data[5]); + this->y_raw = best_two_avg(data[0], data[2], data[4]); - int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - int16_t y_val = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); + ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); + + TouchPoint touchpoint; + + touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); + touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); if (this->swap_x_y_) { - std::swap(x_val, y_val); + std::swap(touchpoint.x, touchpoint.y); } if (this->invert_x_) { - x_val = 0x7fff - x_val; + touchpoint.x = 0xfff - touchpoint.x; } if (this->invert_y_) { - y_val = 0x7fff - y_val; + touchpoint.y = 0xfff - touchpoint.y; } - x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff); - y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff); + switch (static_cast(this->display_->get_rotation())) { + case ROTATE_0_DEGREES: + break; + case ROTATE_90_DEGREES: + std::swap(touchpoint.x, touchpoint.y); + touchpoint.y = 0xfff - touchpoint.y; + break; + case ROTATE_180_DEGREES: + touchpoint.x = 0xfff - touchpoint.x; + touchpoint.y = 0xfff - touchpoint.y; + break; + case ROTATE_270_DEGREES: + std::swap(touchpoint.x, touchpoint.y); + touchpoint.x = 0xfff - touchpoint.x; + break; + } + + touchpoint.x = (int16_t) ((int) touchpoint.x * this->display_->get_width() / 0xfff); + touchpoint.y = (int16_t) ((int) touchpoint.y * this->display_->get_height() / 0xfff); if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGD(TAG, "Raw [x, y] = [%d, %d], transformed = [%d, %d]", this->x_raw, this->y_raw, x_val, y_val); + ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - this->x = x_val; - this->y = y_val; + this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); + + this->x = touchpoint.x; + this->y = touchpoint.y; this->touched = true; this->last_pos_ms_ = now; - - this->on_state_trigger_->process(this->x, this->y, true); - for (auto *button : this->buttons_) - button->touch(this->x, this->y); } - } else { - if (this->touched) { - ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y); + } - this->touched = false; - - this->on_state_trigger_->process(this->x, this->y, false); - for (auto *button : this->buttons_) - button->release(); - } + if (!touch && this->touched) { + this->x_raw = this->y_raw = this->z_raw = 0; + ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); + this->touched = false; + for (auto *listener : this->touch_listeners_) + listener->release(); } } -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { +void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT this->x_raw_min_ = std::min(x_min, x_max); this->x_raw_max_ = std::max(x_min, x_max); this->y_raw_min_ = std::min(y_min, y_max); @@ -137,11 +145,11 @@ void XPT2046Component::dump_config() { ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - ESP_LOGCONFIG(TAG, " X dim: %d", this->x_dim_); - ESP_LOGCONFIG(TAG, " Y dim: %d", this->y_dim_); - if (this->swap_x_y_) { - ESP_LOGCONFIG(TAG, " Swap X/Y"); - } + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); ESP_LOGCONFIG(TAG, " Report interval: %u", this->report_millis_); @@ -150,8 +158,8 @@ void XPT2046Component::dump_config() { float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { - int16_t da, db, dc; +int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT + int16_t da, db, dc; // NOLINT int16_t reta = 0; da = (x > y) ? x - y : y - x; @@ -175,43 +183,24 @@ int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_va if (val <= min_val) { ret = 0; } else if (val >= max_val) { - ret = 0x7fff; + ret = 0xfff; } else { - ret = (int16_t)((int) 0x7fff * (val - min_val) / (max_val - min_val)); + ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); } return ret; } -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT uint8_t data[2]; write_byte(ctrl); + delay(1); data[0] = read_byte(); data[1] = read_byte(); return ((data[0] << 8) | data[1]) >> 3; } -void XPT2046OnStateTrigger::process(int x, int y, bool touched) { this->trigger(x, y, touched); } - -void XPT2046Button::touch(int16_t x, int16_t y) { - bool touched = (x >= this->x_min_ && x <= this->x_max_ && y >= this->y_min_ && y <= this->y_max_); - - if (touched) { - this->publish_state(true); - this->state_ = true; - } else { - release(); - } -} - -void XPT2046Button::release() { - if (this->state_) { - this->publish_state(false); - this->state_ = false; - } -} - } // namespace xpt2046 } // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h index e7270f7d7d..e7d9caba21 100644 --- a/esphome/components/xpt2046/xpt2046.h +++ b/esphome/components/xpt2046/xpt2046.h @@ -3,42 +3,29 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/spi/spi.h" -#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace xpt2046 { -class XPT2046OnStateTrigger : public Trigger { - public: - void process(int x, int y, bool touched); +using namespace touchscreen; + +struct XPT2046TouchscreenStore { + volatile bool touch; + static void gpio_intr(XPT2046TouchscreenStore *store); }; -class XPT2046Button : public binary_sensor::BinarySensor { - public: - /// Set the touch screen area where the button will detect the touch. - void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { - this->x_min_ = x_min; - this->x_max_ = x_max; - this->y_min_ = y_min; - this->y_max_ = y_max; - } - - void touch(int16_t x, int16_t y); - void release(); - - protected: - int16_t x_min_, x_max_, y_min_, y_max_; - bool state_{false}; -}; - -class XPT2046Component : public PollingComponent, +class XPT2046Component : public Touchscreen, + public PollingComponent, public spi::SPIDevice { public: /// Set the logical touch screen dimensions. void set_dimensions(int16_t x, int16_t y) { - this->x_dim_ = x; - this->y_dim_ = y; + this->display_width_ = x; + this->display_height_ = y; } /// Set the coordinates for the touch screen edges. void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); @@ -47,14 +34,12 @@ class XPT2046Component : public PollingComponent, /// Set the interval to report the touch point perodically. void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } + uint32_t get_report_interval() { return this->report_millis_; } + /// Set the threshold for the touch detection. void set_threshold(int16_t threshold) { this->threshold_ = threshold; } /// Set the pin used to detect the touch. - void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; } - /// Get an access to the on_state automation trigger - XPT2046OnStateTrigger *get_on_state_trigger() const { return this->on_state_trigger_; } - /// Register a virtual button to the component. - void register_button(XPT2046Button *button) { this->buttons_.push_back(button); } + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } void setup() override; void dump_config() override; @@ -103,21 +88,19 @@ class XPT2046Component : public PollingComponent, static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); int16_t read_adc_(uint8_t ctrl); + void check_touch_(); int16_t threshold_; int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - int16_t x_dim_, y_dim_; + bool invert_x_, invert_y_; bool swap_x_y_; uint32_t report_millis_; uint32_t last_pos_ms_{0}; - GPIOPin *irq_pin_{nullptr}; - bool last_irq_{true}; - - XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()}; - std::vector buttons_{}; + InternalGPIOPin *irq_pin_{nullptr}; + XPT2046TouchscreenStore store_; }; } // namespace xpt2046 diff --git a/esphome/config.py b/esphome/config.py index 56fe75a4c7..b04de020e0 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -4,7 +4,8 @@ import heapq import logging import re -# pylint: disable=unused-import, wrong-import-order +from typing import Optional, Union + from contextlib import contextmanager import voluptuous as vol @@ -13,6 +14,7 @@ from esphome import core, yaml_util, loader import esphome.core.config as core_config from esphome.const import ( CONF_ESPHOME, + CONF_ID, CONF_PLATFORM, CONF_PACKAGES, CONF_SUBSTITUTIONS, @@ -23,7 +25,7 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from typing import List, Optional, Tuple, Union +from esphome.config_helpers import Extend from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid @@ -50,10 +52,10 @@ def iter_components(config): yield p_name, platform, p_config -ConfigPath = List[Union[str, int]] +ConfigPath = list[Union[str, int]] -def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool +def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: if len(path) < len(other): return False return path[: len(other)] == other @@ -67,7 +69,7 @@ class _ValidationStepTask: self.step = step @property - def _cmp_tuple(self) -> Tuple[float, int]: + def _cmp_tuple(self) -> tuple[float, int]: return (-self.priority, self.id_number) def __eq__(self, other): @@ -84,21 +86,20 @@ class Config(OrderedDict, fv.FinalValidateConfig): def __init__(self): super().__init__() # A list of voluptuous errors - self.errors = [] # type: List[vol.Invalid] + self.errors: 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, str]] + self.output_paths: list[tuple[ConfigPath, str]] = [] # A list of components ids with the config path - self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + self.declare_ids: list[tuple[core.ID, ConfigPath]] = [] self._data = {} # Store pending validation tasks (in heap order) - self._validation_tasks: List[_ValidationStepTask] = [] + self._validation_tasks: list[_ValidationStepTask] = [] # ID to ensure stable order for keys with equal priority self._validation_tasks_id = 0 - def add_error(self, error): - # type: (vol.Invalid) -> None + def add_error(self, error: vol.Invalid) -> None: if isinstance(error, vol.MultipleInvalid): for err in error.errors: self.add_error(err) @@ -132,20 +133,16 @@ class Config(OrderedDict, fv.FinalValidateConfig): e.prepend(path) self.add_error(e) - def add_str_error(self, message, path): - # type: (str, ConfigPath) -> None + def add_str_error(self, message: str, path: ConfigPath) -> None: self.add_error(vol.Invalid(message, path)) - def add_output_path(self, path, domain): - # type: (ConfigPath, str) -> None + def add_output_path(self, path: ConfigPath, domain: str) -> None: self.output_paths.append((path, domain)) - def remove_output_path(self, path, domain): - # type: (ConfigPath, str) -> None + def remove_output_path(self, path: ConfigPath, domain: str) -> None: self.output_paths.remove((path, domain)) - def is_in_error_path(self, path): - # type: (ConfigPath) -> bool + def is_in_error_path(self, path: ConfigPath) -> bool: for err in self.errors: if _path_begins_with(err.path, path): return True @@ -157,23 +154,27 @@ class Config(OrderedDict, fv.FinalValidateConfig): conf = conf[key] conf[path[-1]] = value - def get_error_for_path(self, path): - # type: (ConfigPath) -> Optional[vol.Invalid] + def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]: for err in self.errors: if self.get_deepest_path(err.path) == path: self.errors.remove(err) return err return None - def get_deepest_document_range_for_path(self, path): - # type: (ConfigPath) -> Optional[ESPHomeDataBase] + def get_deepest_document_range_for_path( + self, path: ConfigPath, get_key: bool = False + ) -> Optional[ESPHomeDataBase]: data = self doc_range = None - for item_index in path: + for index, path_item in enumerate(path): try: - if item_index in data: - doc_range = [x for x in data.keys() if x == item_index][0].esp_range - data = data[item_index] + if path_item in data: + key_data = [x for x in data.keys() if x == path_item][0] + if isinstance(key_data, ESPHomeDataBase): + doc_range = key_data.esp_range + if get_key and index == len(path) - 1: + return doc_range + data = data[path_item] except (KeyError, IndexError, TypeError, AttributeError): return doc_range if isinstance(data, core.ID): @@ -203,8 +204,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): return {} return data - def get_deepest_path(self, path): - # type: (ConfigPath) -> ConfigPath + def get_deepest_path(self, path: ConfigPath) -> ConfigPath: """Return the path that is the deepest reachable by following path.""" data = self part = [] @@ -281,7 +281,7 @@ class ConfigValidationStep(abc.ABC): class LoadValidationStep(ConfigValidationStep): """Load step, this step is called once for each domain config fragment. - Responsibilties: + Responsibilities: - Load component code - Ensure all AUTO_LOADs are added - Set output paths of result @@ -336,6 +336,13 @@ class LoadValidationStep(ConfigValidationStep): continue p_name = p_config.get("platform") if p_name is None: + p_id = p_config.get(CONF_ID) + if isinstance(p_id, Extend): + result.add_str_error( + f"Source for extension of ID '{p_id.value}' was not found.", + path + [CONF_ID], + ) + continue result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one @@ -528,7 +535,7 @@ class IDPassValidationStep(ConfigValidationStep): # because the component that did not validate doesn't have any IDs set return - searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + searching_ids: list[tuple[core.ID, ConfigPath]] = [] for id, path in iter_ids(result): if id.is_declaration: if id.id is not None: @@ -738,6 +745,10 @@ def validate_config(config, command_line_substitutions) -> Config: result.add_validation_step(LoadValidationStep(key, config[key])) result.run_validation_steps() + if result.errors: + # do not try to validate further as we don't know what the target is + return result + for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) @@ -772,8 +783,7 @@ def _get_parent_name(path, config): return path[-1] -def _format_vol_invalid(ex, config): - # type: (vol.Invalid, Config) -> str +def _format_vol_invalid(ex: vol.Invalid, config: Config) -> str: message = "" paren = _get_parent_name(ex.path[:-1], config) @@ -854,8 +864,9 @@ def _print_on_next_line(obj): return False -def dump_dict(config, path, at_root=True): - # type: (Config, ConfigPath, bool) -> Tuple[str, bool] +def dump_dict( + config: Config, path: ConfigPath, at_root: bool = True +) -> tuple[str, bool]: conf = config.get_nested_item(path) ret = "" multiline = False diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 39b57e441b..e1d63775bb 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,12 +1,28 @@ import json import os +from esphome.const import CONF_ID from esphome.core import CORE from esphome.helpers import read_file -def read_config_file(path): - # type: (str) -> str +class Extend: + def __init__(self, value): + self.value = value + + def __str__(self): + return f"!extend {self.value}" + + def __eq__(self, b): + """ + Check if two Extend objects contain the same ID. + + Only used in unit tests. + """ + return isinstance(b, Extend) and self.value == b.value + + +def read_config_file(path: str) -> str: if CORE.vscode and ( not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) ): @@ -27,7 +43,6 @@ def read_config_file(path): def merge_config(full_old, full_new): def merge(old, new): - # pylint: disable=no-else-return if isinstance(new, dict): if not isinstance(old, dict): return new @@ -35,11 +50,29 @@ def merge_config(full_old, full_new): for k, v in new.items(): res[k] = merge(old[k], v) if k in old else v return res - elif isinstance(new, list): + if isinstance(new, list): if not isinstance(old, list): return new - return old + new - elif new is None: + res = old.copy() + ids = { + v[CONF_ID]: i + for i, v in enumerate(res) + if CONF_ID in v and isinstance(v[CONF_ID], str) + } + for v in new: + if CONF_ID in v: + new_id = v[CONF_ID] + if isinstance(new_id, Extend): + new_id = new_id.value + if new_id in ids: + v[CONF_ID] = new_id + res[ids[new_id]] = merge(res[ids[new_id]], v) + continue + else: + ids[new_id] = len(res) + res.append(v) + return res + if new is None: return old return new diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 0ff0ba83d9..4b822b46c9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -13,6 +13,7 @@ import voluptuous as vol from esphome import core import esphome.codegen as cg +from esphome.config_helpers import Extend from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, @@ -38,12 +39,20 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE, + CONF_REF, + CONF_URL, + CONF_PATH, + CONF_USERNAME, + CONF_PASSWORD, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_NONE, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + TYPE_GIT, + TYPE_LOCAL, ) from esphome.core import ( CORE, @@ -202,6 +211,9 @@ RESERVED_IDS = [ "open", "setup", "loop", + "uart0", + "uart1", + "uart2", ] @@ -489,6 +501,8 @@ def declare_id(type): if value is None: return core.ID(None, is_declaration=True, type=type) + if isinstance(value, Extend): + raise Invalid(f"Source for extension of ID '{value.value}' was not found.") return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator @@ -547,6 +561,7 @@ def only_with_framework(frameworks): only_on_esp32 = only_on("esp32") only_on_esp8266 = only_on("esp8266") +only_on_rp2040 = only_on("rp2040") only_with_arduino = only_with_framework("arduino") only_with_esp_idf = only_with_framework("esp-idf") @@ -1051,9 +1066,8 @@ def mqtt_qos(value): def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" - # pylint: disable=unsupported-membership-test + def validator(value): - # pylint: disable=unsupported-membership-test if comp not in CORE.loaded_integrations: raise Invalid(f"This option requires component {comp}") return value @@ -1086,7 +1100,7 @@ def possibly_negative_percentage(value): if isinstance(value, str): try: if value.endswith("%"): - has_percent_sign = False + has_percent_sign = True value = float(value[:-1].rstrip()) / 100.0 else: value = float(value) @@ -1239,7 +1253,7 @@ def enum(mapping, **kwargs): return validator -LAMBDA_ENTITY_ID_PROG = re.compile(r"id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)") +LAMBDA_ENTITY_ID_PROG = re.compile(r"\Wid\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)") def lambda_(value): @@ -1440,6 +1454,7 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + rp2040=vol.UNDEFINED, ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1449,6 +1464,7 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._rp2040_default = vol.default_factory(rp2040) @property def default(self): @@ -1458,6 +1474,8 @@ class SplitDefault(Optional): return self._esp32_arduino_default if CORE.is_esp32 and CORE.using_esp_idf: return self._esp32_idf_default + if CORE.is_rp2040: + return self._rp2040_default raise NotImplementedError @default.setter @@ -1476,7 +1494,6 @@ class OnlyWith(Optional): @property def default(self): - # pylint: disable=unsupported-membership-test if self._component in CORE.loaded_integrations: return self._default return vol.UNDEFINED @@ -1497,6 +1514,8 @@ def _entity_base_validator(config): config[CONF_NAME] = id.id config[CONF_INTERNAL] = True return config + if config[CONF_NAME] is None: + config[CONF_NAME] = "" return config @@ -1556,6 +1575,23 @@ def validate_registry_entry(name, registry): return validator +def none(value): + if value in ("none", "None"): + return None + if boolean(value) is False: + return None + raise Invalid("Must be none") + + +def requires_friendly_name(message): + def validate(value): + if CORE.friendly_name is None: + raise Invalid(message) + return value + + return validate + + def validate_registry(name, registry): return ensure_list(validate_registry_entry(name, registry)) @@ -1615,7 +1651,15 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( ENTITY_BASE_SCHEMA = Schema( { - Optional(CONF_NAME): string, + Optional(CONF_NAME): Any( + All( + none, + requires_friendly_name( + "Name cannot be None when esphome->friendly_name is not set!" + ), + ), + string, + ), Optional(CONF_INTERNAL): boolean, Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, Optional(CONF_ICON): icon, @@ -1674,7 +1718,7 @@ def source_refresh(value: str): if value.lower() == "always": return source_refresh("0s") if value.lower() == "never": - return source_refresh("1000y") + return source_refresh("365250d") return positive_time_period_seconds(value) @@ -1689,7 +1733,7 @@ class Version: @classmethod def parse(cls, value: str) -> "Version": - match = re.match(r"(\d+).(\d+).(\d+)", value) + match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) @@ -1703,7 +1747,7 @@ def version_number(value): try: return str(Version.parse(value)) except ValueError as e: - raise Invalid("Not a version number") from e + raise Invalid("Not a valid version number") from e def platformio_version_constraint(value): @@ -1730,6 +1774,7 @@ def require_framework_version( esp_idf=None, esp32_arduino=None, esp8266_arduino=None, + rp2040_arduino=None, max_version=False, extra_message=None, ): @@ -1757,8 +1802,23 @@ def require_framework_version( msg += f". {extra_message}" raise Invalid(msg) required = esp8266_arduino + elif CORE.is_rp2040 and framework == "arduino": + if rp2040_arduino is None: + msg = "This feature is incompatible with RP2040" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + required = rp2040_arduino else: - raise NotImplementedError + raise Invalid( + f""" + Internal Error: require_framework_version does not support this platform configuration + platform: {core_data[KEY_TARGET_PLATFORM]} + framework: {framework} + + Please report this issue on GitHub -> https://github.com/esphome/issues/issues/new?template=bug_report.yml. + """ + ) if max_version: if core_data[KEY_FRAMEWORK_VERSION] > required: @@ -1797,3 +1857,59 @@ def suppress_invalid(): yield except vol.Invalid: pass + + +GIT_SCHEMA = { + Required(CONF_URL): url, + Optional(CONF_REF): git_ref, + Optional(CONF_USERNAME): string, + Optional(CONF_PASSWORD): string, +} +LOCAL_SCHEMA = { + Required(CONF_PATH): directory, +} + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise Invalid("Shorthand only for strings") + try: + return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except Invalid: + pass + # Regex for GitHub repo name with optional branch/tag + # Note: git allows other branch/tag names as well, but never seen them used before + m = re.match( + r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", + value, + ) + if m is None: + raise Invalid( + "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" + ) + if m.group(4): + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: "https://github.com/esphome/esphome.git", + CONF_REF: f"pull/{m.group(4)}/head", + } + else: + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + } + if m.group(3): + conf[CONF_REF] = m.group(3) + + return SOURCE_SCHEMA(conf) + + +SOURCE_SCHEMA = Any( + validate_source_shorthand, + typed_schema( + { + TYPE_GIT: Schema(GIT_SCHEMA), + TYPE_LOCAL: Schema(LOCAL_SCHEMA), + } + ), +) diff --git a/esphome/const.py b/esphome/const.py index 8245ed26f7..2f66b47b8e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,17 +1,18 @@ """Constants used by esphome.""" -__version__ = "2022.9.0-dev" +__version__ = "2023.5.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" +PLATFORM_RP2040 = "rp2040" -TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266] +TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} -SECRETS_FILES = {"secrets.yaml", "secrets.yml"} +SECRETS_FILES = ("secrets.yaml", "secrets.yml") CONF_ABOVE = "above" @@ -23,7 +24,9 @@ CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" +CONF_ACTUAL_GAIN = "actual_gain" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" @@ -114,6 +117,7 @@ CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment" CONF_COMMIT = "commit" +CONF_COMPILE_PROCESS_LIMIT = "compile_process_limit" CONF_COMPONENT_ID = "component_id" CONF_COMPONENTS = "components" CONF_CONDITION = "condition" @@ -137,6 +141,7 @@ CONF_CURRENT = "current" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" +CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" CONF_CUSTOM_FAN_MODES = "custom_fan_modes" CONF_CUSTOM_PRESET = "custom_preset" @@ -163,6 +168,7 @@ CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" CONF_DELAY = "delay" CONF_DELIMITER = "delimiter" CONF_DELTA = "delta" +CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" @@ -172,6 +178,7 @@ CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" +CONF_DISABLE_CRC = "disable_crc" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCONNECT_DELAY = "disconnect_delay" CONF_DISCOVERY = "discovery" @@ -192,6 +199,7 @@ CONF_DUMMY_RECEIVER_ID = "dummy_receiver_id" CONF_DUMP = "dump" CONF_DURATION = "duration" CONF_EAP = "eap" +CONF_EC = "ec" CONF_ECHO_PIN = "echo_pin" CONF_ECO2 = "eco2" CONF_EFFECT = "effect" @@ -206,6 +214,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" @@ -213,6 +222,7 @@ CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" +CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input" CONF_EXTERNAL_COMPONENTS = "external_components" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" @@ -228,6 +238,7 @@ CONF_FAN_MODE_MEDIUM_ACTION = "fan_mode_medium_action" CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" +CONF_FAN_MODE_QUIET_ACTION = "fan_mode_quiet_action" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" CONF_FAN_ONLY_ACTION = "fan_only_action" CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer" @@ -255,6 +266,7 @@ CONF_FRAGMENTATION = "fragmentation" CONF_FRAMEWORK = "framework" CONF_FREE = "free" CONF_FREQUENCY = "frequency" +CONF_FRIENDLY_NAME = "friendly_name" CONF_FROM = "from" CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" @@ -302,6 +314,7 @@ CONF_ILLUMINANCE = "illuminance" CONF_IMPEDANCE = "impedance" CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy" CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" +CONF_INC_PIN = "inc_pin" CONF_INCLUDE_INTERNAL = "include_internal" CONF_INCLUDES = "includes" CONF_INDEX = "index" @@ -318,6 +331,7 @@ CONF_INTERNAL = "internal" CONF_INTERNAL_FILTER = "internal_filter" CONF_INTERNAL_FILTER_MODE = "internal_filter_mode" CONF_INTERRUPT = "interrupt" +CONF_INTERRUPT_PIN = "interrupt_pin" CONF_INTERVAL = "interval" CONF_INVALID_COOLDOWN = "invalid_cooldown" CONF_INVERT = "invert" @@ -333,6 +347,7 @@ CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" CONF_LATITUDE = "latitude" +CONF_LED = "led" CONF_LEGEND = "legend" CONF_LENGTH = "length" CONF_LEVEL = "level" @@ -379,6 +394,7 @@ CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" +CONF_MICROPHONE = "microphone" CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" CONF_MIN_COOLING_RUN_TIME = "min_cooling_run_time" CONF_MIN_FAN_MODE_SWITCHING_TIME = "min_fan_mode_switching_time" @@ -393,6 +409,7 @@ CONF_MIN_POWER = "min_power" CONF_MIN_RANGE = "min_range" CONF_MIN_TEMPERATURE = "min_temperature" CONF_MIN_VALUE = "min_value" +CONF_MIN_VERSION = "min_version" CONF_MINUTE = "minute" CONF_MINUTES = "minutes" CONF_MISO_PIN = "miso_pin" @@ -432,6 +449,7 @@ CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" CONF_ON_CONNECT = "on_connect" +CONF_ON_CONTROL = "on_control" CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" @@ -455,6 +473,7 @@ CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" CONF_ON_TIME_SYNC = "on_time_sync" +CONF_ON_TIMEOUT = "on_timeout" CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" @@ -485,12 +504,14 @@ CONF_PACKAGES = "packages" CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" +CONF_PARAMETERS = "parameters" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_PERIOD = "period" +CONF_PH = "ph" CONF_PHASE_ANGLE = "phase_angle" CONF_PHASE_BALANCER = "phase_balancer" CONF_PIN = "pin" @@ -533,8 +554,10 @@ CONF_POWER_SAVE_MODE = "power_save_mode" CONF_POWER_SUPPLY = "power_supply" CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" +CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" CONF_PRESET_SLEEP = "preset_sleep" +CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" CONF_PRIORITY = "priority" CONF_PROJECT = "project" @@ -558,6 +581,7 @@ CONF_RAW_DATA_ID = "raw_data_id" CONF_RC_CODE_1 = "rc_code_1" CONF_RC_CODE_2 = "rc_code_2" CONF_REACTIVE_POWER = "reactive_power" +CONF_READ_PIN = "read_pin" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" @@ -651,6 +675,7 @@ CONF_STATE_CLASS = "state_class" CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" CONF_STATUS = "status" +CONF_STB_PIN = "stb_pin" CONF_STEP = "step" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" @@ -730,6 +755,7 @@ CONF_TX_POWER = "tx_power" CONF_TYPE = "type" CONF_TYPE_ID = "type_id" CONF_UART_ID = "uart_id" +CONF_UD_PIN = "ud_pin" CONF_UID = "uid" CONF_UNIQUE = "unique" CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" @@ -769,10 +795,14 @@ CONF_WILL_MESSAGE = "will_message" CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees" CONF_WIND_SPEED = "wind_speed" CONF_WINDOW_SIZE = "window_size" +CONF_WRITE_PIN = "write_pin" CONF_X_GRID = "x_grid" CONF_Y_GRID = "y_grid" CONF_ZERO = "zero" +TYPE_GIT = "git" +TYPE_LOCAL = "local" + ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE" ENV_QUICKWIZARD = "ESPHOME_QUICKWIZARD" @@ -792,6 +822,7 @@ ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BUG = "mdi:bug" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" +ICON_CHIP = "mdi:chip" ICON_COUNTER = "mdi:counter" ICON_CURRENT_AC = "mdi:current-ac" ICON_DATABASE = "mdi:database" @@ -807,6 +838,7 @@ ICON_GRAIN = "mdi:grain" ICON_KEY_PLUS = "mdi:key-plus" ICON_LIGHTBULB = "mdi:lightbulb" ICON_MAGNET = "mdi:magnet" +ICON_MEMORY = "mdi:memory" ICON_MOLECULE_CO2 = "mdi:molecule-co2" ICON_MOTION_SENSOR = "mdi:motion-sensor" ICON_NEW_BOX = "mdi:new-box" @@ -830,6 +862,7 @@ ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal" ICON_THERMOMETER = "mdi:thermometer" ICON_TIMELAPSE = "mdi:timelapse" ICON_TIMER = "mdi:timer-outline" +ICON_WATER = "mdi:water" ICON_WATER_PERCENT = "mdi:water-percent" ICON_WEATHER_SUNSET = "mdi:weather-sunset" ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down" @@ -841,6 +874,7 @@ UNIT_AMPERE = "A" UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_BYTES = "B" UNIT_CELSIUS = "°C" +UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" @@ -850,8 +884,10 @@ UNIT_DEGREE_PER_SECOND = "°/s" UNIT_DEGREES = "°" UNIT_EMPTY = "" UNIT_G = "G" +UNIT_GRAMS_PER_CUBIC_METER = "g/m³" UNIT_HECTOPASCAL = "hPa" UNIT_HERTZ = "Hz" +UNIT_HOUR = "h" UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" @@ -869,12 +905,14 @@ UNIT_MICROSIEMENS_PER_CENTIMETER = "µS/cm" UNIT_MICROTESLA = "µT" UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" UNIT_MILLISECOND = "ms" +UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm" UNIT_MINUTE = "min" UNIT_OHM = "Ω" UNIT_PARTS_PER_BILLION = "ppb" UNIT_PARTS_PER_MILLION = "ppm" UNIT_PASCAL = "Pa" UNIT_PERCENT = "%" +UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" UNIT_SECOND = "s" @@ -886,72 +924,89 @@ UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" -# device classes of binary_sensor component +# device classes +DEVICE_CLASS_APPARENT_POWER = "apparent_power" +DEVICE_CLASS_AQI = "aqi" +DEVICE_CLASS_ATMOSPHERIC_PRESSURE = "atmospheric_pressure" +DEVICE_CLASS_AWNING = "awning" +DEVICE_CLASS_BATTERY = "battery" DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" +DEVICE_CLASS_BLIND = "blind" +DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_CONNECTIVITY = "connectivity" +DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_CURTAIN = "curtain" +DEVICE_CLASS_DAMPER = "damper" +DEVICE_CLASS_DATA_RATE = "data_rate" +DEVICE_CLASS_DATA_SIZE = "data_size" +DEVICE_CLASS_DATE = "date" +DEVICE_CLASS_DISTANCE = "distance" DEVICE_CLASS_DOOR = "door" +DEVICE_CLASS_DURATION = "duration" +DEVICE_CLASS_EMPTY = "" +DEVICE_CLASS_ENERGY = "energy" +DEVICE_CLASS_ENERGY_STORAGE = "energy_storage" +DEVICE_CLASS_FREQUENCY = "frequency" +DEVICE_CLASS_GARAGE = "garage" DEVICE_CLASS_GARAGE_DOOR = "garage_door" +DEVICE_CLASS_GAS = "gas" +DEVICE_CLASS_GATE = "gate" DEVICE_CLASS_HEAT = "heat" +DEVICE_CLASS_HUMIDITY = "humidity" +DEVICE_CLASS_ILLUMINANCE = "illuminance" +DEVICE_CLASS_IRRADIANCE = "irradiance" DEVICE_CLASS_LIGHT = "light" DEVICE_CLASS_LOCK = "lock" DEVICE_CLASS_MOISTURE = "moisture" +DEVICE_CLASS_MONETARY = "monetary" DEVICE_CLASS_MOTION = "motion" DEVICE_CLASS_MOVING = "moving" -DEVICE_CLASS_OCCUPANCY = "occupancy" -DEVICE_CLASS_OPENING = "opening" -DEVICE_CLASS_PLUG = "plug" -DEVICE_CLASS_PRESENCE = "presence" -DEVICE_CLASS_PROBLEM = "problem" -DEVICE_CLASS_RUNNING = "running" -DEVICE_CLASS_SAFETY = "safety" -DEVICE_CLASS_SMOKE = "smoke" -DEVICE_CLASS_SOUND = "sound" -DEVICE_CLASS_TAMPER = "tamper" -DEVICE_CLASS_VIBRATION = "vibration" -DEVICE_CLASS_WINDOW = "window" -# device classes of both binary_sensor and sensor component -DEVICE_CLASS_EMPTY = "" -DEVICE_CLASS_BATTERY = "battery" -DEVICE_CLASS_GAS = "gas" -DEVICE_CLASS_POWER = "power" -# device classes of sensor component -DEVICE_CLASS_APPARENT_POWER = "apparent_power" -DEVICE_CLASS_AQI = "aqi" -DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" -DEVICE_CLASS_CURRENT = "current" -DEVICE_CLASS_DATE = "date" -DEVICE_CLASS_DURATION = "duration" -DEVICE_CLASS_ENERGY = "energy" -DEVICE_CLASS_FREQUENCY = "frequency" -DEVICE_CLASS_HUMIDITY = "humidity" -DEVICE_CLASS_ILLUMINANCE = "illuminance" -DEVICE_CLASS_MONETARY = "monetary" DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OCCUPANCY = "occupancy" +DEVICE_CLASS_OPENING = "opening" +DEVICE_CLASS_OUTLET = "outlet" DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" DEVICE_CLASS_PM25 = "pm25" +DEVICE_CLASS_POWER = "power" DEVICE_CLASS_POWER_FACTOR = "power_factor" +DEVICE_CLASS_PRECIPITATION = "precipitation" +DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity" +DEVICE_CLASS_PRESENCE = "presence" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_PROBLEM = "problem" DEVICE_CLASS_REACTIVE_POWER = "reactive_power" +DEVICE_CLASS_RESTART = "restart" +DEVICE_CLASS_RUNNING = "running" +DEVICE_CLASS_SAFETY = "safety" +DEVICE_CLASS_SHADE = "shade" +DEVICE_CLASS_SHUTTER = "shutter" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SMOKE = "smoke" +DEVICE_CLASS_SOUND = "sound" +DEVICE_CLASS_SOUND_PRESSURE = "sound_pressure" +DEVICE_CLASS_SPEED = "speed" DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" +DEVICE_CLASS_SWITCH = "switch" +DEVICE_CLASS_TAMPER = "tamper" DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_UPDATE = "update" +DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" -# device classes of both binary_sensor and button component -DEVICE_CLASS_UPDATE = "update" -# device classes of button component -DEVICE_CLASS_RESTART = "restart" -# device classes of switch component -DEVICE_CLASS_OUTLET = "outlet" -DEVICE_CLASS_SWITCH = "switch" - +DEVICE_CLASS_VOLUME = "volume" +DEVICE_CLASS_VOLUME_STORAGE = "volume_storage" +DEVICE_CLASS_WATER = "water" +DEVICE_CLASS_WEIGHT = "weight" +DEVICE_CLASS_WINDOW = "window" +DEVICE_CLASS_WIND_SPEED = "wind_speed" # state classes STATE_CLASS_NONE = "" @@ -969,6 +1024,9 @@ KEY_CORE = "core" KEY_TARGET_PLATFORM = "target_platform" KEY_TARGET_FRAMEWORK = "target_framework" KEY_FRAMEWORK_VERSION = "framework_version" +KEY_NAME = "name" +KEY_VARIANT = "variant" +KEY_PAST_SAFE_MODE = "past_safe_mode" # Entity categories ENTITY_CATEGORY_NONE = "" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 3ee94efd64..1866f9c9f5 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -2,7 +2,7 @@ import logging import math import os import re -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Optional, Union from esphome.const import ( CONF_COMMENT, @@ -409,6 +409,9 @@ class Define: return self.as_tuple == other.as_tuple return NotImplemented + def __str__(self): + return f"{self.name}={self.value}" + class Library: def __init__(self, name, version, repository=None): @@ -443,7 +446,7 @@ class Library: return NotImplemented -# pylint: disable=too-many-instance-attributes,too-many-public-methods +# pylint: disable=too-many-public-methods class EsphomeCore: def __init__(self): # True if command is run from dashboard @@ -453,6 +456,8 @@ class EsphomeCore: self.ace = False # The name of the node self.name: Optional[str] = None + # The friendly name of the node + self.friendly_name: Optional[str] = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} @@ -469,19 +474,19 @@ 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: Dict[str, "MockObj"] = {} + self.variables: dict[str, "MockObj"] = {} # A list of statements that go in the main setup() block - self.main_statements: List["Statement"] = [] + self.main_statements: list["Statement"] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements: List["Statement"] = [] + self.global_statements: list["Statement"] = [] # A set of platformio libraries to add to the project - self.libraries: List[Library] = [] + self.libraries: list[Library] = [] # A set of build flags to set in the platformio project - self.build_flags: Set[str] = set() + self.build_flags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines: Set["Define"] = set() + self.defines: set["Define"] = set() # A map of all platformio options to apply - self.platformio_options: Dict[str, Union[str, List[str]]] = {} + self.platformio_options: dict[str, Union[str, list[str]]] = {} # 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 @@ -492,6 +497,7 @@ class EsphomeCore: def reset(self): self.dashboard = False self.name = None + self.friendly_name = None self.data = {} self.config_path = None self.build_path = None @@ -553,7 +559,6 @@ class EsphomeCore: return os.path.basename(self.config_path) def relative_config_path(self, *path): - # pylint: disable=no-value-for-parameter path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.config_dir, path_) @@ -561,7 +566,6 @@ class EsphomeCore: return self.relative_config_path(".esphome", *path) def relative_build_path(self, *path): - # pylint: disable=no-value-for-parameter path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.build_path, path_) @@ -594,6 +598,10 @@ class EsphomeCore: def is_esp32(self): return self.target_platform == "esp32" + @property + def is_rp2040(self): + return self.target_platform == "rp2040" + @property def target_framework(self): return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK] @@ -648,7 +656,15 @@ class EsphomeCore: f"Library {library} must be instance of Library, not {type(library)}" ) for other in self.libraries[:]: - if other.name != library.name or other.name is None or library.name is None: + if other.name is None or library.name is None: + continue + library_name = ( + library.name if "/" not in library.name else library.name.split("/")[1] + ) + other_name = ( + other.name if "/" not in other.name else other.name.split("/")[1] + ) + if other_name != library_name: continue if other.repository is not None: if library.repository is None or other.repository == library.repository: @@ -701,7 +717,7 @@ class EsphomeCore: _LOGGER.debug("Adding define: %s", define) return define - def add_platformio_option(self, key: str, value: Union[str, List[str]]) -> None: + def add_platformio_option(self, key: str, value: Union[str, list[str]]) -> None: new_val = value old_val = self.platformio_options.get(key) if isinstance(old_val, list): @@ -734,7 +750,7 @@ class EsphomeCore: _LOGGER.debug("Waiting for variable %s", id) yield - async def get_variable_with_full_id(self, id: ID) -> Tuple[ID, "MockObj"]: + async def get_variable_with_full_id(self, id: ID) -> tuple[ID, "MockObj"]: if not isinstance(id, ID): raise ValueError(f"ID {id!r} must be of type ID!") return await _FakeAwaitable(self._get_variable_with_full_id_generator(id)) diff --git a/esphome/core/application.h b/esphome/core/application.h index 6376987f66..0992a4df39 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -2,11 +2,11 @@ #include #include -#include "esphome/core/defines.h" -#include "esphome/core/preferences.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" #ifdef USE_BINARY_SENSOR @@ -53,14 +53,22 @@ namespace esphome { class Application { public: - void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &comment, + const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { this->name_ = name + "-" + get_mac_address().substr(6); + if (friendly_name.empty()) { + this->friendly_name_ = ""; + } else { + this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6); + } } else { this->name_ = name; + this->friendly_name_ = friendly_name; } + this->comment_ = comment; this->compilation_time_ = compilation_time; } @@ -131,9 +139,14 @@ class Application { /// Make a loop iteration. Call this in your loop() function. void loop(); - /// Get the name of this Application set by set_name(). + /// Get the name of this Application set by pre_setup(). const std::string &get_name() const { return this->name_; } + /// Get the friendly name of this Application set by pre_setup(). + const std::string &get_friendly_name() const { return this->friendly_name_; } + /// Get the comment of this Application set by pre_setup(). + const std::string &get_comment() const { return this->comment_; } + bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } const std::string &get_compilation_time() const { return this->compilation_time_; } @@ -338,6 +351,8 @@ class Application { #endif std::string name_; + std::string friendly_name_; + std::string comment_; std::string compilation_time_; bool name_add_mac_suffix_; uint32_t last_loop_{0}; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 92bc32247b..84c754e081 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -176,7 +176,7 @@ template class Action { return this->next_->is_running(); } - Action *next_ = nullptr; + Action *next_{nullptr}; /// The number of instances of this sequence in the list of actions /// that is currently being executed. diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 6ac6e596c1..daa09b912e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -3,6 +3,8 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include + namespace esphome { template class AndCondition : public Condition { @@ -233,22 +235,21 @@ template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) - void add_then(const std::vector *> &actions) { + void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { - this->iteration_++; - if (this->iteration_ == this->count_.value(x...)) + this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { + iteration++; + if (iteration >= this->count_.value(x...)) this->play_next_tuple_(this->var_); else - this->then_.play_tuple(this->var_); + this->then_.play(iteration, x...); })); } void play_complex(Ts... x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - this->iteration_ = 0; - this->then_.play_tuple(this->var_); + this->then_.play(0, x...); } void play(Ts... x) override { /* ignore - see play_complex */ @@ -257,8 +258,7 @@ template class RepeatAction : public Action { void stop() override { this->then_.stop(); } protected: - uint32_t iteration_; - ActionList then_; + ActionList then_; std::tuple var_; }; @@ -317,7 +317,7 @@ template class UpdateComponentAction : public Action { UpdateComponentAction(PollingComponent *component) : component_(component) {} void play(Ts... x) override { - if (this->component_->is_failed()) + if (!this->component_->is_ready()) return; this->component_->update(); } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5cb063cbec..49ef8ecde7 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float ETHERNET = 250.0f; const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; @@ -56,7 +57,7 @@ bool Component::cancel_interval(const std::string &name) { // NOLINT } void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&f, float backoff_increase_factor) { // NOLINT + std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } @@ -129,11 +130,15 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) { // N void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, "", interval, std::move(f)); } -void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, +void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } +bool Component::is_ready() { + return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || + (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; +} bool Component::can_proceed() { return true; } bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; } diff --git a/esphome/core/component.h b/esphome/core/component.h index e394736653..7382f1c617 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -29,6 +29,7 @@ extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +extern const float ETHERNET; /// For components that should be initialized after WiFi and before API is connected. extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. @@ -118,6 +119,8 @@ class Component { bool is_failed(); + bool is_ready(); + virtual bool can_proceed(); bool status_has_warning(); @@ -184,26 +187,39 @@ class Component { /** Set an retry function with a unique name. Empty name means no cancelling possible. * - * This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms. - * f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased - * by backoff_increase_factor for each iteration. Default is doubling the time between iterations - * Can be cancelled via cancel_retry(). + * This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if + * it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f + * again in the future. + * + * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is + * increased by multipling by `backoff_increase_factor` each time. If no backoff_increase_factor is + * supplied (default = 1.0), the wait time will stay constant. + * + * The retry function f needs to accept a single argument: the number of attempts remaining. On the + * final retry of f, this value will be 0. + * + * This retry function can also be cancelled by name via cancel_retry(). * * IMPORTANT: Do not rely on this having correct timing. This is only called from * loop() and therefore can be significantly delayed. * + * REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead. + * + * REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly + * if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows. + * * @param name The identifier for this retry function. * @param initial_wait_time The time in ms before f is called again - * @param max_attempts The maximum number of retries + * @param max_attempts The maximum number of executions * @param f The function (or lambda) that should be called - * @param backoff_increase_factor time between retries is increased by this factor on every retry + * @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first * @see cancel_retry() */ - void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT - std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT + std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT - void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT - float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT + float backoff_increase_factor = 1.0f); // NOLINT /** Cancel a retry function. * @@ -254,7 +270,7 @@ class Component { uint32_t component_state_{0x0000}; ///< State of this component. float setup_priority_override_{NAN}; - const char *component_source_ = nullptr; + const char *component_source_{nullptr}; }; /** This class simplifies creating components that periodically check a state. diff --git a/esphome/core/config.py b/esphome/core/config.py index f1337be04b..ef6553026e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,4 +1,5 @@ import logging +import multiprocessing import os import re @@ -11,11 +12,14 @@ from esphome.const import ( CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, CONF_COMMENT, + CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, CONF_FRAMEWORK, CONF_INCLUDES, CONF_LIBRARIES, + CONF_MIN_VERSION, CONF_NAME, + CONF_FRIENDLY_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, @@ -30,6 +34,7 @@ from esphome.const import ( KEY_CORE, TARGET_PLATFORMS, PLATFORM_ESP8266, + __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files @@ -96,11 +101,31 @@ def valid_project_name(value: str): return value +def validate_version(value: str): + min_version = cv.Version.parse(value) + current_version = cv.Version.parse(ESPHOME_VERSION) + if current_version < min_version: + raise cv.Invalid( + f"Your ESPHome version is too old. Please update to at least {min_version}" + ) + return value + + +if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: + _compile_process_limit_default = min( + int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), + multiprocessing.cpu_count(), + ) +else: + _compile_process_limit_default = cv.UNDEFINED + + CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -136,6 +161,12 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( + cv.version_number, validate_version + ), + cv.Optional( + CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default + ): cv.int_range(min=1, max=multiprocessing.cpu_count()), } ), validate_hostname, @@ -163,6 +194,7 @@ def preload_core_config(config, result): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) CORE.name = conf[CONF_NAME] + CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME) CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: @@ -179,7 +211,11 @@ def preload_core_config(config, result): ] if not has_oldstyle and not newstyle_found: - raise cv.Invalid("Platform missing for core options!", [CONF_ESPHOME]) + raise cv.Invalid( + "Platform missing. You must include one of the available platform keys: " + + ", ".join(TARGET_PLATFORMS), + [CONF_ESPHOME], + ) if has_oldstyle and newstyle_found: raise cv.Invalid( f"Please remove the `platform` key from the [esphome] block. You're already using the new style with the [{conf[CONF_PLATFORM]}] block", @@ -313,6 +349,8 @@ async def to_code(config): cg.add( cg.App.pre_setup( config[CONF_NAME], + config[CONF_FRIENDLY_NAME], + config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], ) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 90676c421e..77d41e5b58 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -25,6 +25,7 @@ #define USE_FAN #define USE_GRAPH #define USE_HOMEASSISTANT_TIME +#define USE_JSON #define USE_LIGHT #define USE_LOCK #define USE_LOGGER @@ -32,6 +33,7 @@ #define USE_MEDIA_PLAYER #define USE_MQTT #define USE_NUMBER +#define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK #define USE_POWER_SUPPLY @@ -49,7 +51,6 @@ // Arduino-specific feature flags #ifdef USE_ARDUINO #define USE_CAPTIVE_PORTAL -#define USE_JSON #define USE_NEXTION_TFT_UPLOAD #define USE_PROMETHEUS #define USE_WEBSERVER @@ -67,19 +68,19 @@ #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_SERVER #define USE_ESP32_CAMERA -#define USE_ESP32_IGNORE_EFUSE_MAC_CRC #define USE_IMPROV #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_WIFI_11KV_SUPPORT #define USE_BLUETOOTH_PROXY +#define USE_VOICE_ASSISTANT #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 6) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5) #define USE_ETHERNET #endif #ifdef USE_ESP_IDF -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(4, 3, 0) +#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(4, 4, 2) #endif #endif @@ -90,15 +91,16 @@ #define USE_ESP8266_PREFERENCES_FLASH #define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_SOCKET_IMPL_LWIP_TCP -#endif - -// Disabled feature flags -//#define USE_BSEC // Requires a library with proprietary license. - -#define USE_DASHBOARD_IMPORT // Dummy firmware payload for shelly_dimmer #define USE_SHD_FIRMWARE_MAJOR_VERSION 56 #define USE_SHD_FIRMWARE_MINOR_VERSION 5 #define USE_SHD_FIRMWARE_DATA \ {} + +#endif + +// Disabled feature flags +//#define USE_BSEC // Requires a library with proprietary license. + +#define USE_DASHBOARD_IMPORT diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index a9e1414018..1e2ccc35b5 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,17 +1,21 @@ #include "esphome/core/entity_base.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" namespace esphome { static const char *const TAG = "entity_base"; -EntityBase::EntityBase(std::string name) : name_(std::move(name)) { this->calc_object_id_(); } - // Entity Name -const std::string &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const std::string &name) { - this->name_ = name; - this->calc_object_id_(); +const StringRef &EntityBase::get_name() const { return this->name_; } +void EntityBase::set_name(const char *name) { + this->name_ = StringRef(name); + if (this->name_.empty()) { + this->name_ = StringRef(App.get_friendly_name()); + this->has_own_name_ = false; + } else { + this->has_own_name_ = true; + } } // Entity Internal @@ -23,22 +27,61 @@ bool EntityBase::is_disabled_by_default() const { return this->disabled_by_defau void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } // Entity Icon -const std::string &EntityBase::get_icon() const { return this->icon_; } -void EntityBase::set_icon(const std::string &name) { this->icon_ = name; } +std::string EntityBase::get_icon() const { + if (this->icon_c_str_ == nullptr) { + return ""; + } + return this->icon_c_str_; +} +void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; } // Entity Category EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; } void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; } // Entity Object ID -const std::string &EntityBase::get_object_id() { return this->object_id_; } +std::string EntityBase::get_object_id() const { + // Check if `App.get_friendly_name()` is constant or dynamic. + if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + // `App.get_friendly_name()` is dynamic. + return str_sanitize(str_snake_case(App.get_friendly_name())); + } else { + // `App.get_friendly_name()` is constant. + if (this->object_id_c_str_ == nullptr) { + return ""; + } + return this->object_id_c_str_; + } +} +void EntityBase::set_object_id(const char *object_id) { + this->object_id_c_str_ = object_id; + this->calc_object_id_(); +} // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { - this->object_id_ = str_sanitize(str_snake_case(this->name_)); - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(this->object_id_); + // Check if `App.get_friendly_name()` is constant or dynamic. + if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + // `App.get_friendly_name()` is dynamic. + const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(object_id); + } else { + // `App.get_friendly_name()` is constant. + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(this->object_id_c_str_); + } } + uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } +std::string EntityBase_DeviceClass::get_device_class() { + if (this->device_class_ == nullptr) { + return ""; + } + return this->device_class_; +} + +void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } + } // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index a9eedfd07e..d717674450 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -2,6 +2,7 @@ #include #include +#include "string_ref.h" namespace esphome { @@ -14,15 +15,16 @@ enum EntityCategory : uint8_t { // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: - EntityBase() : EntityBase("") {} - explicit EntityBase(std::string name); - // Get/set the name of this Entity - const std::string &get_name() const; - void set_name(const std::string &name); + const StringRef &get_name() const; + void set_name(const char *name); - // Get the sanitized name of this Entity as an ID. Caching it internally. - const std::string &get_object_id(); + // Get whether this Entity has its own name or it should use the device friendly_name. + bool has_own_name() const { return this->has_own_name_; } + + // Get the sanitized name of this Entity as an ID. + std::string get_object_id() const; + void set_object_id(const char *object_id); // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); @@ -42,8 +44,8 @@ class EntityBase { void set_entity_category(EntityCategory entity_category); // Get/set this entity's icon - const std::string &get_icon() const; - void set_icon(const std::string &name); + std::string get_icon() const; + void set_icon(const char *icon); protected: /// The hash_base() function has been deprecated. It is kept in this @@ -51,13 +53,25 @@ class EntityBase { virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); - std::string name_; - std::string object_id_; - std::string icon_; + StringRef name_; + const char *object_id_c_str_{nullptr}; + const char *icon_c_str_{nullptr}; uint32_t object_id_hash_; + bool has_own_name_{false}; bool internal_{false}; bool disabled_by_default_{false}; EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; }; +class EntityBase_DeviceClass { + public: + /// Get the device class, using the manual override if set. + std::string get_device_class(); + /// Manually set the device class. + void set_device_class(const char *device_class); + + protected: + const char *device_class_{nullptr}; ///< Device class override +}; + } // namespace esphome diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index b953a95664..1b6f2ba1e6 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -73,7 +73,7 @@ class ISRInternalGPIOPin { void pin_mode(gpio::Flags flags); protected: - void *arg_ = nullptr; + void *arg_{nullptr}; }; class InternalGPIOPin : public GPIOPin { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index d82e452c3d..7e8ba41987 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -18,9 +18,17 @@ #elif defined(USE_ESP32_FRAMEWORK_ARDUINO) #include #elif defined(USE_ESP_IDF) +#include "esp_mac.h" +#include "esp_random.h" #include "esp_system.h" #include #include +#elif defined(USE_RP2040) +#if defined(USE_WIFI) +#include +#endif +#include +#include #endif #ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC @@ -62,6 +70,21 @@ uint8_t crc8(uint8_t *data, uint8_t len) { } return crc; } +uint16_t crc16(const 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; +} uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { @@ -76,6 +99,13 @@ uint32_t random_uint32() { return esp_random(); #elif defined(USE_ESP8266) return os_random(); +#elif defined(USE_RP2040) + uint32_t result = 0; + for (uint8_t i = 0; i < 32; i++) { + result <<= 1; + result |= rosc_hw->randombit; + } + return result; #else #error "No random source available for this configuration." #endif @@ -87,6 +117,16 @@ bool random_bytes(uint8_t *data, size_t len) { return true; #elif defined(USE_ESP8266) return os_get_random(data, len) == 0; +#elif defined(USE_RP2040) + while (len-- != 0) { + uint8_t result = 0; + for (uint8_t i = 0; i < 8; i++) { + result <<= 1; + result |= rosc_hw->randombit; + } + *data++ = result; + } + return true; #else #error "No random source available for this configuration." #endif @@ -355,15 +395,30 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green } // System APIs +#if defined(USE_ESP8266) || defined(USE_RP2040) +// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. +Mutex::Mutex() {} +void Mutex::lock() {} +bool Mutex::try_lock() { return true; } +void Mutex::unlock() {} +#elif defined(USE_ESP32) +Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } +void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } +bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } +void Mutex::unlock() { xSemaphoreGive(this->handle_); } +#endif #if defined(USE_ESP8266) -IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } -IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } +IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } +IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } #elif defined(USE_ESP32) // only affects the executing core // so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } +#elif defined(USE_RP2040) +IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } +IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } #endif uint8_t HighFrequencyLoopRequester::num_requests = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -381,7 +436,7 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } -void get_mac_address_raw(uint8_t *mac) { +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(USE_ESP32) #if defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC) // On some devices, the MAC address that is burnt into EFuse does not @@ -394,6 +449,8 @@ void get_mac_address_raw(uint8_t *mac) { #endif #elif defined(USE_ESP8266) wifi_get_macaddr(STATION_IF, mac); +#elif defined(USE_RP2040) && defined(USE_WIFI) + WiFi.macAddress(mac); #endif } std::string get_mac_address() { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6bed743010..05a7eaa4cc 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -14,6 +14,11 @@ #include #endif +#if defined(USE_ESP32) +#include +#include +#endif + #define HOT __attribute__((hot)) #define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg))) #define ALWAYS_INLINE __attribute__((always_inline)) @@ -149,6 +154,9 @@ template T remap(U value, U min, U max, T min_out, T max /// Calculate a CRC-8 checksum of \p data with size \p len. uint8_t crc8(uint8_t *data, uint8_t len); +/// Calculate a CRC-16 checksum of \p data with size \p len. +uint16_t crc16(const uint8_t *data, uint8_t len); + /// Calculate a FNV-1 hash of \p str. uint32_t fnv1_hash(const std::string &str); @@ -387,6 +395,9 @@ template::value, int> = 0> std::stri val = convert_big_endian(val); return format_hex(reinterpret_cast(&val), sizeof(T)); } +template std::string format_hex(const std::array &data) { + return format_hex(data.data(), data.size()); +} /// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. std::string format_hex_pretty(const uint8_t *data, size_t length); @@ -513,6 +524,39 @@ template class Parented { /// @name System APIs ///@{ +/** Mutex implementation, with API based on the unavailable std::mutex. + * + * @note This mutex is non-recursive, so take care not to try to obtain the mutex while it is already taken. + */ +class Mutex { + public: + Mutex(); + Mutex(const Mutex &) = delete; + void lock(); + bool try_lock(); + void unlock(); + + Mutex &operator=(const Mutex &) = delete; + + private: +#if defined(USE_ESP32) + SemaphoreHandle_t handle_; +#endif +}; + +/** Helper class that wraps a mutex with a RAII-style API. + * + * This behaves like std::lock_guard: as long as the object is alive, the mutex is held. + */ +class LockGuard { + public: + LockGuard(Mutex &mutex) : mutex_(mutex) { mutex_.lock(); } + ~LockGuard() { mutex_.unlock(); } + + private: + Mutex &mutex_; +}; + /** Helper class to disable interrupts. * * This behaves like std::lock_guard: as long as the object is alive, all interrupts are disabled. @@ -539,8 +583,8 @@ class InterruptLock { ~InterruptLock(); protected: -#ifdef USE_ESP8266 - uint32_t xt_state_; +#if defined(USE_ESP8266) || defined(USE_RP2040) + uint32_t state_; #endif }; @@ -565,7 +609,7 @@ class HighFrequencyLoopRequester { }; /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). -void get_mac_address_raw(uint8_t *mac); +void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) /// Get the device MAC address as a string, in lowercase hex notation. std::string get_mac_address(); @@ -609,7 +653,7 @@ template class ExternalRAMAllocator { size_t size = n * sizeof(T); T *ptr = nullptr; #ifdef USE_ESP32 - ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)); + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); #endif if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0) ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) diff --git a/esphome/core/log.h b/esphome/core/log.h index b1b1cf9115..6775aa5ac5 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -144,19 +144,12 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif #define ESP_LOGE(tag, ...) esph_log_e(tag, __VA_ARGS__) -#define LOG_E(tag, ...) ESP_LOGE(tag, __VA__ARGS__) #define ESP_LOGW(tag, ...) esph_log_w(tag, __VA_ARGS__) -#define LOG_W(tag, ...) ESP_LOGW(tag, __VA__ARGS__) #define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__) -#define LOG_I(tag, ...) ESP_LOGI(tag, __VA__ARGS__) #define ESP_LOGD(tag, ...) esph_log_d(tag, __VA_ARGS__) -#define LOG_D(tag, ...) ESP_LOGD(tag, __VA__ARGS__) #define ESP_LOGCONFIG(tag, ...) esph_log_config(tag, __VA_ARGS__) -#define LOG_CONFIG(tag, ...) ESP_LOGCONFIG(tag, __VA__ARGS__) #define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__) -#define LOG_V(tag, ...) ESP_LOGV(tag, __VA__ARGS__) #define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__) -#define LOG_VV(tag, ...) ESP_LOGVV(tag, __VA__ARGS__) #define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" #define BYTE_TO_BINARY(byte) \ @@ -174,7 +167,7 @@ struct LogString; #include #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0) -#define LOG_STR_ARG(s) ((PGM_P)(s)) +#define LOG_STR_ARG(s) ((PGM_P) (s)) #else // Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a // local buffer first. String length is limited to 63 characters. @@ -183,7 +176,7 @@ struct LogString; ({ \ char __buf[64]; \ __buf[63] = '\0'; \ - strncpy_P(__buf, (PGM_P)(s), 63); \ + strncpy_P(__buf, (PGM_P) (s), 63); \ __buf; \ }) #endif diff --git a/esphome/core/macros.h b/esphome/core/macros.h index 70ceaf58f4..ee53d20ad1 100644 --- a/esphome/core/macros.h +++ b/esphome/core/macros.h @@ -1,4 +1,4 @@ #pragma once -// Helper macro to define a version code, whos evalue can be compared against other version codes. +// Helper macro to define a version code, whose value can be compared against other version codes. #define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch)) diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 2b13061a59..6d2dd967e9 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -46,6 +46,14 @@ class ESPPreferences { */ virtual bool sync() = 0; + /** + * Forget all unsaved changes and re-initialize the permanent preferences storage. + * Usually followed by a restart which moves the system to "factory" conditions + * + * @return true if operation is successful. + */ + virtual bool reset() = 0; + template::value, bool> = true> ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { return this->make_preference(sizeof(T), type, in_flash); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index cc4074b94d..7c76c8490b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -3,6 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/hal.h" #include +#include namespace esphome { @@ -13,6 +14,11 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to +// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, +// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to +// avoid the main thread modifying the list while it is being accessed. + void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func) { const uint32_t now = this->millis_(); @@ -74,7 +80,7 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam } struct RetryArgs { - std::function func; + std::function func; uint8_t retry_countdown; uint32_t current_interval; Component *component; @@ -84,15 +90,18 @@ struct RetryArgs { }; static void retry_handler(const std::shared_ptr &args) { - RetryResult retry_result = args->func(); - if (retry_result == RetryResult::DONE || --args->retry_countdown <= 0) + RetryResult const retry_result = args->func(--args->retry_countdown); + if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; - args->current_interval *= args->backoff_increase_factor; + // second execution of `func` happens after `initial_wait_time` args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); + // backoff_increase_factor applied to third & later executions + args->current_interval *= args->backoff_increase_factor; } void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function func, float backoff_increase_factor) { + uint8_t max_attempts, std::function func, + float backoff_increase_factor) { if (!name.empty()) this->cancel_retry(component, name); @@ -102,6 +111,13 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); + if (backoff_increase_factor < 0.0001) { + ESP_LOGE(TAG, + "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead", + name.c_str(), backoff_increase_factor); + backoff_increase_factor = 1; + } + auto args = std::make_shared(); args->func = std::move(func); args->retry_countdown = max_attempts; @@ -111,7 +127,8 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin args->backoff_increase_factor = backoff_increase_factor; args->scheduler = this; - this->set_timeout(component, args->name, initial_wait_time, [args]() { retry_handler(args); }); + // First execution of `func` immediately + this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); }); } bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { return this->cancel_timeout(component, "retry$" + name); @@ -139,35 +156,47 @@ void HOT Scheduler::call() { std::vector> old_items; ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); while (!this->empty_()) { + this->lock_.lock(); auto item = std::move(this->items_[0]); + this->pop_raw_(); + this->lock_.unlock(); + ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(), 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); + + { + LockGuard guard{this->lock_}; + this->items_ = std::move(old_items); + } } #endif // ESPHOME_DEBUG_SCHEDULER auto to_remove_was = to_remove_; - auto items_was = items_.size(); + auto items_was = this->items_.size(); // If we have too many items to remove if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { std::vector> valid_items; while (!this->empty_()) { + LockGuard guard{this->lock_}; auto item = std::move(this->items_[0]); this->pop_raw_(); valid_items.push_back(std::move(item)); } - this->items_ = std::move(valid_items); + + { + LockGuard guard{this->lock_}; + 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()); + ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this", + to_remove_was, to_remove_, items_was, items_.size()); to_remove_ = 0; } } @@ -187,6 +216,7 @@ void HOT Scheduler::call() { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; this->pop_raw_(); continue; } @@ -206,6 +236,8 @@ void HOT Scheduler::call() { } { + this->lock_.lock(); + // new scope, item from before might have been moved in the vector auto item = std::move(this->items_[0]); @@ -213,6 +245,8 @@ void HOT Scheduler::call() { // during the function call and know if we were cancelled. this->pop_raw_(); + this->lock_.unlock(); + if (item->remove) { // We were removed/cancelled in the function call, stop to_remove_--; @@ -235,6 +269,7 @@ void HOT Scheduler::call() { this->process_to_add(); } void HOT Scheduler::process_to_add() { + LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { if (it->remove) { continue; @@ -252,15 +287,24 @@ void HOT Scheduler::cleanup_() { return; to_remove_--; - this->pop_raw_(); + + { + LockGuard guard{this->lock_}; + this->pop_raw_(); + } } } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->items_.pop_back(); } -void HOT Scheduler::push_(std::unique_ptr item) { this->to_add_.push_back(std::move(item)); } +void HOT Scheduler::push_(std::unique_ptr item) { + LockGuard guard{this->lock_}; + this->to_add_.push_back(std::move(item)); +} bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + // obtain lock because this function iterates and can be called from non-loop task context + LockGuard guard{this->lock_}; bool ret = false; for (auto &it : this->items_) { if (it->component == component && it->name == name && it->type == type && !it->remove) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 111bee1df2..44a58f37f5 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -1,9 +1,11 @@ #pragma once -#include "esphome/core/component.h" #include #include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + namespace esphome { class Component; @@ -16,7 +18,7 @@ class Scheduler { bool cancel_interval(Component *component, const std::string &name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor = 1.0f); + std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); optional next_schedule_in(); @@ -71,6 +73,7 @@ class Scheduler { return this->items_.empty(); } + Mutex lock_; std::vector> items_; std::vector> to_add_; uint32_t last_millis_{0}; diff --git a/esphome/core/string_ref.cpp b/esphome/core/string_ref.cpp new file mode 100644 index 0000000000..ce1e33cbb7 --- /dev/null +++ b/esphome/core/string_ref.cpp @@ -0,0 +1,12 @@ +#include "string_ref.h" + +namespace esphome { + +#ifdef USE_JSON + +// NOLINTNEXTLINE(readability-identifier-naming) +void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } + +#endif // USE_JSON + +} // namespace esphome diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h new file mode 100644 index 0000000000..5940a7ee65 --- /dev/null +++ b/esphome/core/string_ref.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include "esphome/core/defines.h" + +#ifdef USE_JSON +#include "esphome/components/json/json_util.h" +#endif // USE_JSON + +namespace esphome { + +/** + * StringRef is a reference to a string owned by something else. So it behaves like simple string, but it does not own + * pointer. When it is default constructed, it has empty string. You can freely copy or move around this struct, but + * never free its pointer. str() function can be used to export the content as std::string. StringRef is adopted from + * + */ +class StringRef { + public: + using traits_type = std::char_traits; + using value_type = traits_type::char_type; + using allocator_type = std::allocator; + using size_type = std::allocator_traits::size_type; + using difference_type = std::allocator_traits::difference_type; + using const_reference = const value_type &; + using const_pointer = const value_type *; + using const_iterator = const_pointer; + using const_reverse_iterator = std::reverse_iterator; + + constexpr StringRef() : base_(""), len_(0) {} + explicit StringRef(const std::string &s) : base_(s.c_str()), len_(s.size()) {} + explicit StringRef(const char *s) : base_(s), len_(strlen(s)) {} + constexpr StringRef(const char *s, size_t n) : base_(s), len_(n) {} + template + constexpr StringRef(const CharT *s, size_t n) : base_(reinterpret_cast(s)), len_(n) {} + template + StringRef(InputIt first, InputIt last) + : base_(reinterpret_cast(&*first)), len_(std::distance(first, last)) {} + template + StringRef(InputIt *first, InputIt *last) + : base_(reinterpret_cast(first)), len_(std::distance(first, last)) {} + template constexpr static StringRef from_lit(const CharT (&s)[N]) { + return StringRef{s, N - 1}; + } + static StringRef from_maybe_nullptr(const char *s) { + if (s == nullptr) { + return StringRef(); + } + + return StringRef(s); + } + + constexpr const_iterator begin() const { return base_; }; + constexpr const_iterator cbegin() const { return base_; }; + + constexpr const_iterator end() const { return base_ + len_; }; + constexpr const_iterator cend() const { return base_ + len_; }; + + const_reverse_iterator rbegin() const { return const_reverse_iterator{base_ + len_}; } + const_reverse_iterator crbegin() const { return const_reverse_iterator{base_ + len_}; } + + const_reverse_iterator rend() const { return const_reverse_iterator{base_}; } + const_reverse_iterator crend() const { return const_reverse_iterator{base_}; } + + constexpr const char *c_str() const { return base_; } + constexpr size_type size() const { return len_; } + constexpr bool empty() const { return len_ == 0; } + constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + + std::string str() const { return std::string(base_, len_); } + const uint8_t *byte() const { return reinterpret_cast(base_); } + + operator std::string() const { return str(); } + + private: + const char *base_; + size_type len_; +}; + +inline bool operator==(const StringRef &lhs, const StringRef &rhs) { + return lhs.size() == rhs.size() && std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const StringRef &lhs, const std::string &rhs) { + return lhs.size() == rhs.size() && std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const std::string &lhs, const StringRef &rhs) { return rhs == lhs; } + +inline bool operator==(const StringRef &lhs, const char *rhs) { + return lhs.size() == strlen(rhs) && std::equal(std::begin(lhs), std::end(lhs), rhs); +} + +inline bool operator==(const char *lhs, const StringRef &rhs) { return rhs == lhs; } + +inline bool operator!=(const StringRef &lhs, const StringRef &rhs) { return !(lhs == rhs); } + +inline bool operator!=(const StringRef &lhs, const std::string &rhs) { return !(lhs == rhs); } + +inline bool operator!=(const std::string &lhs, const StringRef &rhs) { return !(rhs == lhs); } + +inline bool operator!=(const StringRef &lhs, const char *rhs) { return !(lhs == rhs); } + +inline bool operator!=(const char *lhs, const StringRef &rhs) { return !(rhs == lhs); } + +inline bool operator<(const StringRef &lhs, const StringRef &rhs) { + return std::lexicographical_compare(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs)); +} + +inline std::string &operator+=(std::string &lhs, const StringRef &rhs) { + lhs.append(rhs.c_str(), rhs.size()); + return lhs; +} + +inline std::string operator+(const char *lhs, const StringRef &rhs) { + auto str = std::string(lhs); + str.append(rhs.c_str(), rhs.size()); + return str; +} + +inline std::string operator+(const StringRef &lhs, const char *rhs) { + auto str = lhs.str(); + str.append(rhs); + return str; +} + +#ifdef USE_JSON +// NOLINTNEXTLINE(readability-identifier-naming) +void convertToJson(const StringRef &src, JsonVariant dst); +#endif // USE_JSON + +} // namespace esphome diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 58f79c6b36..5f391dc7ad 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -48,7 +48,8 @@ import heapq import inspect import logging import types -from typing import Any, Awaitable, Callable, Generator, Iterator, List, Tuple +from typing import Any, Callable +from collections.abc import Awaitable, Generator, Iterator _LOGGER = logging.getLogger(__name__) @@ -177,7 +178,7 @@ class _Task: return _Task(priority, self.id_number, self.iterator, self.original_function) @property - def _cmp_tuple(self) -> Tuple[float, int]: + def _cmp_tuple(self) -> tuple[float, int]: return (-self.priority, self.id_number) def __eq__(self, other): @@ -194,7 +195,7 @@ class FakeEventLoop: """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence.""" def __init__(self): - self._pending_tasks: List[_Task] = [] + self._pending_tasks: list[_Task] = [] self._task_counter = 0 def add_job(self, func, *args, **kwargs): diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 42828450e8..789bd58e5c 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -2,28 +2,26 @@ import abc import inspect import math import re -from esphome.yaml_util import ESPHomeDataBase +from collections.abc import Generator, Sequence +from typing import Any, Callable, Optional, Union -# pylint: disable=unused-import, wrong-import-order -from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence - -from esphome.core import ( # noqa +from esphome.core import ( CORE, - HexInt, ID, + Define, + EnumValue, + HexInt, Lambda, + Library, TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, - coroutine, - Library, - Define, - EnumValue, ) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.util import OrderedDict +from esphome.yaml_util import ESPHomeDataBase class Expression(abc.ABC): @@ -44,9 +42,9 @@ SafeExpType = Union[ int, float, TimePeriod, - Type[bool], - Type[int], - Type[float], + type[bool], + type[int], + type[float], Sequence[Any], ] @@ -140,7 +138,7 @@ class CallExpression(Expression): class StructInitializer(Expression): __slots__ = ("base", "args") - def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]): + 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): @@ -176,10 +174,9 @@ class ArrayInitializer(Expression): if not self.args: return "{}" if self.multiline: - cpp = "{\n" - for arg in self.args: - cpp += f" {arg},\n" - cpp += "}" + cpp = "{\n " + cpp += ",\n ".join(str(arg) for arg in self.args) + cpp += ",\n}" else: cpp = f"{{{', '.join(str(arg) for arg in self.args)}}}" return cpp @@ -200,7 +197,7 @@ class ParameterListExpression(Expression): __slots__ = ("parameters",) def __init__( - self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]] + self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]] ): self.parameters = [] for parameter in parameters: @@ -468,7 +465,9 @@ def statement(expression: Union[Expression, Statement]) -> Statement: return ExpressionStatement(expression) -def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": +def variable( + id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True +) -> "MockObj": """Declare a new variable, not pointer type, in the code generation. :param id_: The ID used to declare the variable. @@ -485,10 +484,37 @@ def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": id_.type = type_ assignment = AssignmentExpression(id_.type, "", id_, rhs) CORE.add(assignment) - CORE.register_variable(id_, obj) + if register: + CORE.register_variable(id_, obj) return obj +def with_local_variable( + id_: ID, rhs: SafeExpType, callback: Callable[["MockObj"], None], *args +) -> None: + """Declare a new variable, not pointer type, in the code generation, within a scoped block + The variable is only usable within the callback + The callback cannot be async. + + :param id_: The ID used to declare the variable. + :param rhs: The expression to place on the right hand side of the assignment. + :param callback: The function to invoke that will receive the temporary variable + :param args: args to pass to the callback in addition to the temporary variable + + """ + + # throw if the callback is async: + assert not inspect.iscoroutinefunction( + callback + ), "with_local_variable() callback cannot be async!" + + CORE.add(RawStatement("{")) # output opening curly brace + obj = variable(id_, rhs, None, True) + # invoke user-provided callback to generate code with this local variable + callback(obj, *args) + CORE.add(RawStatement("}")) # output closing curly brace + + def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare and define a new variable, not pointer type, in the code generation. @@ -590,7 +616,7 @@ def add_define(name: str, value: SafeExpType = None): CORE.add_define(Define(name, safe_exp(value))) -def add_platformio_option(key: str, value: Union[str, List[str]]): +def add_platformio_option(key: str, value: Union[str, list[str]]): CORE.add_platformio_option(key, value) @@ -607,7 +633,7 @@ async def get_variable(id_: ID) -> "MockObj": return await CORE.get_variable(id_) -async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]: +async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. @@ -622,7 +648,7 @@ async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]: async def process_lambda( value: Lambda, - parameters: List[Tuple[SafeExpType, str]], + parameters: list[tuple[SafeExpType, str]], capture: str = "=", return_type: SafeExpType = None, ) -> Generator[LambdaExpression, None, None]: @@ -676,7 +702,7 @@ def is_template(value): async def templatable( value: Any, - args: List[Tuple[SafeExpType, str]], + args: list[tuple[SafeExpType, str]], output_type: Optional[SafeExpType], to_exp: Any = None, ): @@ -724,7 +750,7 @@ class MockObj(Expression): attr = attr[1:] return MockObj(f"{self.base}{self.op}{attr}", next_op) - def __call__(self, *args): # type: (SafeExpType) -> MockObj + def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) return MockObj(call, self.op) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 9127f88e39..cc53f491f5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,14 +9,18 @@ from esphome.const import ( CONF_SETUP_PRIORITY, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, + CONF_OTA, + CONF_SAFE_MODE, + KEY_PAST_SAFE_MODE, ) -# pylint: disable=unused-import from esphome.core import coroutine, ID, CORE -from esphome.types import ConfigType +from esphome.coroutine import FakeAwaitable +from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App from esphome.util import Registry, RegistryEntry +from esphome.helpers import snake_case, sanitize _LOGGER = logging.getLogger(__name__) @@ -98,6 +102,10 @@ async def register_parented(var, value): async def setup_entity(var, config): """Set up generic properties of an Entity""" add(var.set_name(config[CONF_NAME])) + if not config[CONF_NAME]: + add(var.set_object_id(sanitize(snake_case(CORE.friendly_name)))) + else: + add(var.set_object_id(sanitize(snake_case(config[CONF_NAME])))) add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) @@ -107,8 +115,10 @@ async def setup_entity(var, config): add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) -def extract_registry_entry_config(registry, full_config): - # type: (Registry, ConfigType) -> RegistryEntry +def extract_registry_entry_config( + registry: Registry, + full_config: ConfigType, +) -> tuple[RegistryEntry, ConfigFragmentType]: key, config = next((k, v) for k, v in full_config.items() if k in registry) return registry[key], config @@ -126,3 +136,19 @@ async def build_registry_list(registry, config): action = await build_registry_entry(registry, conf) actions.append(action) return actions + + +async def past_safe_mode(): + safe_mode_enabled = ( + CONF_OTA in CORE.config and CORE.config[CONF_OTA][CONF_SAFE_MODE] + ) + if not safe_mode_enabled: + return + + def _safe_mode_generator(): + while True: + if CORE.data.get(CONF_OTA, {}).get(KEY_PAST_SAFE_MODE, False): + return + yield + + return await FakeAwaitable(_safe_mode_generator()) diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index aafe765111..7d0e386b66 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -14,6 +14,7 @@ uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") uint64 = global_ns.namespace("uint64_t") +int16 = global_ns.namespace("int16_t") int32 = global_ns.namespace("int32_t") int64 = global_ns.namespace("int64_t") size_t = global_ns.namespace("size_t") diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index ca2767639d..1a50592a2d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,3 @@ -# pylint: disable=wrong-import-position - import base64 import codecs import collections @@ -10,11 +8,12 @@ import json import logging import multiprocessing import os -from pathlib import Path import secrets import shutil import subprocess import threading +from pathlib import Path +from typing import Optional import tornado import tornado.concurrent @@ -22,14 +21,15 @@ import tornado.gen import tornado.httpserver import tornado.ioloop import tornado.iostream -from tornado.log import access_log import tornado.netutil import tornado.process import tornado.web import tornado.websocket +import yaml +from tornado.log import access_log from esphome import const, platformio_api, util, yaml_util -from esphome.helpers import mkdir_p, get_bool_env, run_system_command +from esphome.helpers import get_bool_env, mkdir_p, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, StorageJSON, @@ -37,14 +37,11 @@ from esphome.storage_json import ( ext_storage_path, trash_storage_path, ) -from esphome.util import shlex_quote, get_serial_ports -from .util import password_hash - -# pylint: disable=unused-import, wrong-import-order -from typing import Optional # noqa - +from esphome.util import get_serial_ports, shlex_quote from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf +from .util import friendly_name_slugify, password_hash + _LOGGER = logging.getLogger(__name__) ENV_DEV = "ESPHOME_DASHBOARD_DEV" @@ -58,6 +55,7 @@ class DashboardSettings: self.using_password = False self.on_ha_addon = False self.cookie_secret = None + self.absolute_config_dir = None def parse_args(self, args): self.on_ha_addon = args.ha_addon @@ -68,6 +66,7 @@ class DashboardSettings: if self.using_password: self.password_hash = password_hash(password) self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() @property def relative_url(self): @@ -97,7 +96,10 @@ class DashboardSettings: return hmac.compare_digest(self.password_hash, password_hash(password)) def rel_path(self, *args): - return os.path.join(self.config_dir, *args) + joined_path = os.path.join(self.config_dir, *args) + # Raises ValueError if not relative to ESPHome config folder + Path(joined_path).resolve().relative_to(self.absolute_config_dir) + return joined_path def list_yaml_files(self): return util.list_yaml_files([self.config_dir]) @@ -189,7 +191,6 @@ def websocket_method(name): return wrap -# pylint: disable=abstract-method, arguments-differ @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): @@ -285,8 +286,11 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket): class EsphomeRenameHandler(EsphomeCommandWebSocket): + old_name: str + def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) + self.old_name = json_message["configuration"] return [ "esphome", "--dashboard", @@ -295,8 +299,30 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): json_message["newName"], ] + def _proc_on_exit(self, returncode): + super()._proc_on_exit(returncode) + + if returncode != 0: + return + + # Remove the old ping result from the cache + PING_RESULT.pop(self.old_name, None) + class EsphomeUploadHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + config_file = settings.rel_path(json_message["configuration"]) + return [ + "esphome", + "--dashboard", + "upload", + config_file, + "--device", + json_message["port"], + ] + + +class EsphomeRunHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) return [ @@ -312,7 +338,11 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket): class EsphomeCompileHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "compile", config_file] + command = ["esphome", "--dashboard", "compile"] + if json_message.get("only_generate", False): + command.append("--only-generate") + command.append(config_file) + return command class EsphomeValidateHandler(EsphomeCommandWebSocket): @@ -378,12 +408,24 @@ class WizardRequestHandler(BaseHandler): for k, v in json.loads(self.request.body.decode()).items() if k in ("name", "platform", "board", "ssid", "psk", "password") } + if not kwargs["name"]: + self.set_status(422) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Name is required"})) + return + + kwargs["friendly_name"] = kwargs["name"] + kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) + kwargs["ota_password"] = secrets.token_hex(16) noise_psk = secrets.token_bytes(32) kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() - destination = settings.rel_path(f"{kwargs['name']}.yaml") + filename = f"{kwargs['name']}.yaml" + destination = settings.rel_path(filename) wizard.wizard_write(path=destination, **kwargs) self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) self.finish() @@ -395,18 +437,41 @@ class ImportRequestHandler(BaseHandler): args = json.loads(self.request.body.decode()) try: name = args["name"] + friendly_name = args.get("friendly_name") + encryption = args.get("encryption", False) + + imported_device = next( + (res for res in IMPORT_RESULT.values() if res.device_name == name), None + ) + + if imported_device is not None: + network = imported_device.network + if friendly_name is None: + friendly_name = imported_device.friendly_name + else: + network = const.CONF_WIFI + import_config( settings.rel_path(f"{name}.yaml"), name, + friendly_name, args["project_name"], args["package_import_url"], + network, + encryption, ) except FileExistsError: self.set_status(500) self.write("File already exists") return + except ValueError: + self.set_status(422) + self.write("Invalid package url") + return self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": f"{name}.yaml"})) self.finish() @@ -416,21 +481,27 @@ class DownloadBinaryRequestHandler(BaseHandler): def get(self, configuration=None): type = self.get_argument("type", "firmware.bin") - if type == "firmware.bin": - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return + storage_path = ext_storage_path(settings.config_dir, configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + if storage_json.target_platform.lower() == const.PLATFORM_RP2040: + filename = f"{storage_json.name}.uf2" + path = storage_json.firmware_bin_path.replace( + "firmware.bin", "firmware.uf2" + ) + + elif storage_json.target_platform.lower() == const.PLATFORM_ESP8266: + filename = f"{storage_json.name}.bin" + path = storage_json.firmware_bin_path + + elif type == "firmware.bin": filename = f"{storage_json.name}.bin" path = storage_json.firmware_bin_path elif type == "firmware-factory.bin": - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return filename = f"{storage_json.name}-factory.bin" path = storage_json.firmware_bin_path.replace( "firmware.bin", "firmware-factory.bin" @@ -474,35 +545,11 @@ class DownloadBinaryRequestHandler(BaseHandler): self.finish() -class ManifestRequestHandler(BaseHandler): +class EsphomeVersionHandler(BaseHandler): @authenticated - @bind_config - def get(self, configuration=None): - args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(404 if rc == 2 else 500) - return - - idedata = platformio_api.IDEData(json.loads(stdout)) - - firmware_offset = "0x10000" if idedata.extra_flash_images else "0x0" - flash_images = [ - { - "path": f"./download.bin?configuration={configuration}&type=firmware.bin", - "offset": firmware_offset, - } - ] + [ - { - "path": f"./download.bin?configuration={configuration}&type={os.path.basename(image.path)}", - "offset": image.offset, - } - for image in idedata.extra_flash_images - ] - + def get(self): self.set_header("Content-Type", "application/json") - self.write(json.dumps(flash_images)) + self.write(json.dumps({"version": const.__version__})) self.finish() @@ -522,7 +569,7 @@ class DashboardEntry: return os.path.basename(self.path) @property - def storage(self): # type: () -> Optional[StorageJSON] + def storage(self) -> Optional[StorageJSON]: if not self._loaded_storage: self._storage = StorageJSON.load( ext_storage_path(settings.config_dir, self.filename) @@ -548,6 +595,12 @@ class DashboardEntry: return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name + @property + def friendly_name(self): + if self.storage is None: + return self.name + return self.storage.friendly_name + @property def comment(self): if self.storage is None: @@ -595,6 +648,7 @@ class ListDevicesHandler(BaseHandler): "configured": [ { "name": entry.name, + "friendly_name": entry.friendly_name, "configuration": entry.filename, "loaded_integrations": entry.loaded_integrations, "deployed_version": entry.update_old, @@ -610,9 +664,11 @@ class ListDevicesHandler(BaseHandler): "importable": [ { "name": res.device_name, + "friendly_name": res.friendly_name, "package_import_url": res.package_import_url, "project_name": res.project_name, "project_version": res.project_version, + "network": res.network, } for res in IMPORT_RESULT.values() if res.device_name not in configured @@ -644,6 +700,65 @@ def _ping_func(filename, address): return filename, rc == 0 +class PrometheusServiceDiscoveryHandler(BaseHandler): + @authenticated + def get(self): + entries = _list_dashboard_entries() + self.set_header("content-type", "application/json") + sd = [] + for entry in entries: + if entry.web_port is None: + continue + labels = { + "__meta_name": entry.name, + "__meta_esp_platform": entry.target_platform, + "__meta_esphome_version": entry.storage.esphome_version, + } + for integration in entry.storage.loaded_integrations: + labels[f"__meta_integration_{integration}"] = "true" + sd.append( + { + "targets": [ + f"{entry.address}:{entry.web_port}", + ], + "labels": labels, + } + ) + self.write(json.dumps(sd)) + + +class BoardsRequestHandler(BaseHandler): + @authenticated + def get(self, platform: str): + from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS + from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS + from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS + + platform_to_boards = { + "esp32": ESP32_BOARDS, + "esp8266": ESP8266_BOARDS, + "rp2040": RP2040_BOARDS, + } + # filter all ESP32 variants by requested platform + if platform.startswith("esp32"): + boards = { + k: v + for k, v in platform_to_boards["esp32"].items() + if v[const.KEY_VARIANT] == platform.upper() + } + else: + boards = platform_to_boards[platform] + + # map to a {board_name: board_title} dict + platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} + # sort by board title + boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) + output = [{"items": dict(boards_items)}] + + self.set_header("content-type", "application/json") + self.write(json.dumps(output)) + + class MDNSStatusThread(threading.Thread): def run(self): global IMPORT_RESULT @@ -747,7 +862,6 @@ class EditRequestHandler(BaseHandler): filename = settings.rel_path(configuration) content = "" if os.path.isfile(filename): - # pylint: disable=no-value-for-parameter with open(file=filename, encoding="utf-8") as f: content = f.read() self.write(content) @@ -755,7 +869,6 @@ class EditRequestHandler(BaseHandler): @authenticated @bind_config def post(self, configuration=None): - # pylint: disable=no-value-for-parameter with open(file=settings.rel_path(configuration), mode="wb") as f: f.write(self.request.body) self.set_status(200) @@ -780,6 +893,9 @@ class DeleteRequestHandler(BaseHandler): if build_folder is not None: shutil.rmtree(build_folder, os.path.join(trash_path, name)) + # Remove the old ping result from the cache + PING_RESULT.pop(configuration, None) + class UndoDeleteRequestHandler(BaseHandler): @authenticated @@ -790,7 +906,7 @@ class UndoDeleteRequestHandler(BaseHandler): shutil.move(os.path.join(trash_path, configuration), config_file) -PING_RESULT = {} # type: dict +PING_RESULT: dict = {} IMPORT_RESULT = {} STOP_EVENT = threading.Event() PING_REQUEST = threading.Event() @@ -799,7 +915,7 @@ PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): def get(self): if is_authenticated(self): - self.redirect("/") + self.redirect("./") else: self.render_login_page() @@ -816,14 +932,17 @@ class LoginHandler(BaseHandler): import requests headers = { - "Authentication": f"Bearer {os.getenv('SUPERVISOR_TOKEN')}", + "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), } + data = { "username": self.get_argument("username", ""), "password": self.get_argument("password", ""), } try: - req = requests.post("http://supervisor/auth", headers=headers, data=data) + req = requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) if req.status_code == 200: self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -841,7 +960,7 @@ class LoginHandler(BaseHandler): password = self.get_argument("password", "") if settings.check_password(username, password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") + self.redirect("./") return error_str = ( "Invalid username or password" if settings.username else "Invalid password" @@ -866,7 +985,6 @@ class LogoutHandler(BaseHandler): class SecretKeysRequestHandler(BaseHandler): @authenticated def get(self): - filename = None for secret_filename in const.SECRETS_FILES: @@ -885,6 +1003,43 @@ class SecretKeysRequestHandler(BaseHandler): self.write(json.dumps(secret_keys)) +class SafeLoaderIgnoreUnknown(yaml.SafeLoader): + def ignore_unknown(self, node): + return f"{node.tag} {node.value}" + + def construct_yaml_binary(self, node) -> str: + return super().construct_yaml_binary(node).decode("ascii") + + +SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) +SafeLoaderIgnoreUnknown.add_constructor( + "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary +) + + +class JsonConfigRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + filename = settings.rel_path(configuration) + if not os.path.isfile(filename): + self.send_error(404) + return + + args = ["esphome", "config", settings.rel_path(configuration), "--show-secrets"] + + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(422) + return + + data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + self.finish() + + def get_base_frontend_path(): if ENV_DEV not in os.environ: import esphome_dashboard @@ -903,7 +1058,7 @@ def get_static_path(*args): return os.path.join(get_base_frontend_path(), "static", *args) -@functools.lru_cache(maxsize=None) +@functools.cache def get_static_file_url(name): base = f"./static/{name}" @@ -969,6 +1124,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}logout", LogoutHandler), (f"{rel}logs", EsphomeLogsHandler), (f"{rel}upload", EsphomeUploadHandler), + (f"{rel}run", EsphomeRunHandler), (f"{rel}compile", EsphomeCompileHandler), (f"{rel}validate", EsphomeValidateHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), @@ -979,7 +1135,6 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}info", InfoRequestHandler), (f"{rel}edit", EditRequestHandler), (f"{rel}download.bin", DownloadBinaryRequestHandler), - (f"{rel}manifest.json", ManifestRequestHandler), (f"{rel}serial-ports", SerialPortRequestHandler), (f"{rel}ping", PingRequestHandler), (f"{rel}delete", DeleteRequestHandler), @@ -989,7 +1144,11 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}devices", ListDevicesHandler), (f"{rel}import", ImportRequestHandler), (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}json-config", JsonConfigRequestHandler), (f"{rel}rename", EsphomeRenameHandler), + (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), + (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), + (f"{rel}version", EsphomeVersionHandler), ], **app_settings, ) diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py index 3e3864aa17..a2ad530b74 100644 --- a/esphome/dashboard/util.py +++ b/esphome/dashboard/util.py @@ -1,4 +1,7 @@ import hashlib +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS def password_hash(password: str) -> bytes: @@ -7,3 +10,23 @@ def password_hash(password: str) -> bytes: Note this is not meant for secure storage, but for securely comparing passwords. """ return hashlib.sha256(password.encode()).digest() + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 96dd2fd651..5e9d2207b0 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Any +from typing import Any import contextvars from esphome.types import ConfigFragmentType, ID, ConfigPathType @@ -9,7 +9,7 @@ import esphome.config_validation as cv class FinalValidateConfig(ABC): @property @abstractmethod - def data(self) -> Dict[str, Any]: + def data(self) -> dict[str, Any]: """A dictionary that can be used by post validation functions to store global data during the validation phase. Each component should store its data under a unique key diff --git a/esphome/git.py b/esphome/git.py index 64c8d6a6b7..a607325b73 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -1,18 +1,20 @@ -from pathlib import Path -import subprocess import hashlib import logging +import re +import subprocess import urllib.parse - +from dataclasses import dataclass from datetime import datetime +from pathlib import Path +from typing import Callable, Optional -from esphome.core import CORE, TimePeriodSeconds import esphome.config_validation as cv +from esphome.core import CORE, TimePeriodSeconds _LOGGER = logging.getLogger(__name__) -def run_git_command(cmd, cwd=None): +def run_git_command(cmd, cwd=None) -> str: try: ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) except FileNotFoundError as err: @@ -28,6 +30,8 @@ def run_git_command(cmd, cwd=None): raise cv.Invalid(lines[-1][len("fatal: ") :]) raise cv.Invalid(err_str) + return ret.stdout.decode("utf-8").strip() + def _compute_destination_path(key: str, domain: str) -> Path: base_dir = Path(CORE.config_dir) / ".esphome" / domain @@ -40,11 +44,11 @@ def clone_or_update( *, url: str, ref: str = None, - refresh: TimePeriodSeconds, + refresh: Optional[TimePeriodSeconds], domain: str, username: str = None, password: str = None, -) -> Path: +) -> tuple[Path, Optional[Callable[[], None]]]: key = f"{url}@{ref}" if username is not None and password is not None: @@ -77,7 +81,8 @@ def clone_or_update( if not file_timestamp.exists(): file_timestamp = Path(repo_dir / ".git" / "HEAD") age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) - if age.total_seconds() > refresh.total_seconds: + if refresh is None or age.total_seconds() > refresh.total_seconds: + old_sha = run_git_command(["git", "rev-parse", "HEAD"], str(repo_dir)) _LOGGER.info("Updating %s", key) _LOGGER.debug("Location: %s", repo_dir) # Stash local changes (if any) @@ -92,4 +97,64 @@ def clone_or_update( # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) - return repo_dir + def revert(): + _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) + run_git_command(["git", "reset", "--hard", old_sha], str(repo_dir)) + + return repo_dir, revert + + return repo_dir, None + + +GIT_DOMAINS = { + "github": "github.com", + "gitlab": "gitlab.com", +} + + +@dataclass(frozen=True) +class GitFile: + domain: str + owner: str + repo: str + filename: str + ref: str = None + query: str = None + + @property + def git_url(self) -> str: + return f"https://{self.domain}/{self.owner}/{self.repo}.git" + + @property + def raw_url(self) -> str: + if self.ref is None: + raise ValueError("URL has no ref") + if self.domain == "github.com": + return f"https://raw.githubusercontent.com/{self.owner}/{self.repo}/{self.ref}/{self.filename}" + if self.domain == "gitlab.com": + return f"https://gitlab.com/{self.owner}/{self.repo}/-/raw/{self.ref}/{self.filename}" + raise NotImplementedError(f"Git domain {self.domain} not supported") + + @classmethod + def from_shorthand(cls, shorthand): + """Parse a git shorthand URL into its components.""" + if not isinstance(shorthand, str): + raise ValueError("Git shorthand must be a string") + m = re.match( + r"(?P[a-zA-Z0-9\-]+)://(?P[a-zA-Z0-9\-]+)/(?P[a-zA-Z0-9\-\._]+)/(?P[a-zA-Z0-9\-_.\./]+)(?:@(?P[a-zA-Z0-9\-_.\./]+))?(?:\?(?P[a-zA-Z0-9\-_.\./]+))?", + shorthand, + ) + if m is None: + raise ValueError( + "URL is not in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!" + ) + if m.group("domain") not in GIT_DOMAINS: + raise ValueError(f"Unknown git domain {m.group('domain')}") + return cls( + domain=GIT_DOMAINS[m.group("domain")], + owner=m.group("owner"), + repo=m.group("repo"), + filename=m.group("filename"), + ref=m.group("ref"), + query=m.group("query"), + ) diff --git a/esphome/helpers.py b/esphome/helpers.py index e958aca78e..884f640d7b 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -6,6 +6,8 @@ import os from pathlib import Path from typing import Union import tempfile +from urllib.parse import urlparse +import re _LOGGER = logging.getLogger(__name__) @@ -40,7 +42,7 @@ def indent(text, padding=" "): # From https://stackoverflow.com/a/14945195/8924614 def cpp_string_escape(string, encoding="utf-8"): - def _should_escape(byte): # type: (int) -> bool + def _should_escape(byte: int) -> bool: if not 32 <= byte < 127: return True if byte in (ord("\\"), ord('"')): @@ -134,7 +136,8 @@ def resolve_ip_address(host): errs.append(str(err)) try: - return socket.gethostbyname(host) + host_url = host if (urlparse(host).scheme != "") else "http://" + host + return socket.gethostbyname(urlparse(host_url).hostname) except OSError as err: errs.append(str(err)) raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err @@ -332,3 +335,13 @@ def add_class_to_obj(value, cls): if type(value) is type_: # pylint: disable=unidiomatic-typecheck return add_class_to_obj(func(value), cls) raise + + +def snake_case(value): + """Same behaviour as `helpers.cpp` method `str_snake_case`.""" + return value.replace(" ", "_").lower() + + +def sanitize(value): + """Same behaviour as `helpers.cpp` method `str_sanitize`.""" + return re.sub("[^-_0-9a-zA-Z]", r"", value) diff --git a/esphome/loader.py b/esphome/loader.py index 05d2e5a213..cd21e5a509 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, List, Optional, Any, ContextManager +from typing import Callable, Optional, Any, ContextManager from types import ModuleType import importlib import importlib.util @@ -62,19 +62,22 @@ class ComponentManifest: return getattr(self.module, "to_code", None) @property - def dependencies(self) -> List[str]: + def dependencies(self) -> list[str]: return getattr(self.module, "DEPENDENCIES", []) @property - def conflicts_with(self) -> List[str]: + def conflicts_with(self) -> list[str]: return getattr(self.module, "CONFLICTS_WITH", []) @property - def auto_load(self) -> List[str]: - return getattr(self.module, "AUTO_LOAD", []) + def auto_load(self) -> list[str]: + al = getattr(self.module, "AUTO_LOAD", []) + if callable(al): + return al() + return al @property - def codeowners(self) -> List[str]: + def codeowners(self) -> list[str]: return getattr(self.module, "CODEOWNERS", []) @property @@ -87,7 +90,7 @@ class ComponentManifest: return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) @property - def resources(self) -> List[FileResource]: + def resources(self) -> list[FileResource]: """Return a list of all file resources defined in the package of this component. This will return all cpp source files that are located in the same folder as the @@ -106,7 +109,7 @@ class ComponentManifest: class ComponentMetaFinder(importlib.abc.MetaPathFinder): def __init__( - self, components_path: Path, allowed_components: Optional[List[str]] = None + self, components_path: Path, allowed_components: Optional[list[str]] = None ) -> None: self._allowed_components = allowed_components self._finders = [] @@ -117,7 +120,7 @@ class ComponentMetaFinder(importlib.abc.MetaPathFinder): continue self._finders.append(finder) - def find_spec(self, fullname: str, path: Optional[List[str]], target=None): + def find_spec(self, fullname: str, path: Optional[list[str]], target=None): if not fullname.startswith("esphome.components."): return None parts = fullname.split(".") @@ -144,7 +147,7 @@ def clear_component_meta_finders(): def install_meta_finder( - components_path: Path, allowed_components: Optional[List[str]] = None + components_path: Path, allowed_components: Optional[list[str]] = None ): sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) @@ -167,10 +170,10 @@ def _lookup_module(domain): except Exception: # pylint: disable=broad-except _LOGGER.error("Unable to load component %s:", domain, exc_info=True) return None - else: - manif = ComponentManifest(module) - _COMPONENT_CACHE[domain] = manif - return manif + + manif = ComponentManifest(module) + _COMPONENT_CACHE[domain] = manif + return manif def get_component(domain): diff --git a/esphome/pins.py b/esphome/pins.py index 2b3adce86d..2ac4cd4b54 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -49,6 +49,11 @@ def _set_mode(value, default_mode): CONF_INPUT: True, CONF_PULLDOWN: True, }, + "INPUT_OUTPUT_OPEN_DRAIN": { + CONF_INPUT: True, + CONF_OUTPUT: True, + CONF_OPEN_DRAIN: True, + }, } if mode.upper() not in PIN_MODES: raise cv.Invalid(f"Unknown pin mode {mode}", [CONF_MODE]) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c4bf3d3f1a..c46a3fc767 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import json -from typing import List, Union +from typing import Union from pathlib import Path import logging @@ -8,7 +8,7 @@ import os import re import subprocess -from esphome.const import KEY_CORE +from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError from esphome.util import run_external_command, run_external_process @@ -37,7 +37,6 @@ def patch_structhash(): if not isdir(build_dir): makedirs(build_dir) - # pylint: disable=protected-access helpers.clean_build_dir = patched_clean_build_dir cli.clean_build_dir = patched_clean_build_dir @@ -48,7 +47,7 @@ FILTER_PLATFORMIO_LINES = [ r"CONFIGURATION: https://docs.platformio.org/.*", r"DEBUG: Current.*", r"LDF Modes:.*", - r"LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*", + r"LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf.*", f"Looking for {IGNORE_LIB_WARNINGS} library in registry", f"Warning! Library `.*'{IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.", f"You can ignore this message, if `.*{IGNORE_LIB_WARNINGS}.*` is a built-in library.*", @@ -103,7 +102,10 @@ def run_platformio_cli_run(config, verbose, *args, **kwargs) -> Union[str, int]: def run_compile(config, verbose): - return run_platformio_cli_run(config, verbose) + args = [] + if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]: + args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"] + return run_platformio_cli_run(config, verbose, *args) def _run_idedata(config): @@ -310,7 +312,7 @@ class IDEData: return str(Path(self.firmware_elf_path).with_suffix(".bin")) @property - def extra_flash_images(self) -> List[FlashImage]: + def extra_flash_images(self) -> list[FlashImage]: return [ FlashImage(path=entry["path"], offset=entry["offset"]) for entry in self.raw["extra"]["flash_images"] diff --git a/esphome/storage_json.py b/esphome/storage_json.py index a941fca0af..bbdfbbc8a2 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -4,7 +4,7 @@ from datetime import datetime import json import logging import os -from typing import Any, Optional, List +from typing import Optional from esphome import const from esphome.core import CORE @@ -15,28 +15,28 @@ from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) -def storage_path(): # type: () -> str +def storage_path() -> str: return CORE.relative_internal_path(f"{CORE.config_filename}.json") -def ext_storage_path(base_path, config_filename): # type: (str, str) -> str +def ext_storage_path(base_path: str, config_filename: str) -> str: return os.path.join(base_path, ".esphome", f"{config_filename}.json") -def esphome_storage_path(base_path): # type: (str) -> str +def esphome_storage_path(base_path: str) -> str: return os.path.join(base_path, ".esphome", "esphome.json") -def trash_storage_path(base_path): # type: (str) -> str +def trash_storage_path(base_path: str) -> str: return os.path.join(base_path, ".esphome", "trash") -# pylint: disable=too-many-instance-attributes class StorageJSON: def __init__( self, storage_version, name, + friendly_name, comment, esphome_version, src_version, @@ -49,35 +49,38 @@ class StorageJSON: ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version = storage_version # type: int + self.storage_version: int = storage_version # The name of the node - self.name = name # type: str + self.name: str = name + # The friendly name of the node + self.friendly_name: str = friendly_name # The comment of the node - self.comment = comment # type: str + self.comment: str = comment # The esphome version this was compiled with - self.esphome_version = esphome_version # type: str + self.esphome_version: str = esphome_version # The version of the file in src/main.cpp - Used to migrate the file assert src_version is None or isinstance(src_version, int) - self.src_version = src_version # type: int + self.src_version: int = src_version # Address of the ESP, for example livingroom.local or a static IP - self.address = address # type: str + self.address: str = address # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) - self.web_port = web_port # type: int + self.web_port: int = web_port # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. - self.target_platform = target_platform # type: str + self.target_platform: str = target_platform # The absolute path to the platformio project - self.build_path = build_path # type: str + self.build_path: str = build_path # The absolute path to the firmware binary - self.firmware_bin_path = firmware_bin_path # type: str + self.firmware_bin_path: str = firmware_bin_path # A list of strings of names of loaded integrations - self.loaded_integrations = loaded_integrations # type: List[str] + self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations.sort() def as_dict(self): return { "storage_version": self.storage_version, "name": self.name, + "friendly_name": self.friendly_name, "comment": self.comment, "esphome_version": self.esphome_version, "src_version": self.src_version, @@ -97,8 +100,8 @@ class StorageJSON: @staticmethod def from_esphome_core( - esph, old - ): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON + esph: CoreType, old: Optional["StorageJSON"] + ) -> "StorageJSON": hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 @@ -107,6 +110,7 @@ class StorageJSON: return StorageJSON( storage_version=1, name=esph.name, + friendly_name=esph.friendly_name, comment=esph.comment, esphome_version=const.__version__, src_version=1, @@ -119,27 +123,31 @@ class StorageJSON: ) @staticmethod - def from_wizard(name: str, address: str, esp_platform: str) -> "StorageJSON": + def from_wizard( + name: str, friendly_name: str, address: str, platform: str + ) -> "StorageJSON": return StorageJSON( storage_version=1, name=name, + friendly_name=friendly_name, comment=None, - esphome_version=const.__version__, + esphome_version=None, src_version=1, address=address, web_port=None, - target_platform=esp_platform, + target_platform=platform, build_path=None, firmware_bin_path=None, loaded_integrations=[], ) @staticmethod - def _load_impl(path): # type: (str) -> Optional[StorageJSON] + def _load_impl(path: str) -> Optional["StorageJSON"]: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] name = storage.get("name") + friendly_name = storage.get("friendly_name") comment = storage.get("comment") esphome_version = storage.get( "esphome_version", storage.get("esphomeyaml_version") @@ -154,6 +162,7 @@ class StorageJSON: return StorageJSON( storage_version, name, + friendly_name, comment, esphome_version, src_version, @@ -166,13 +175,13 @@ class StorageJSON: ) @staticmethod - def load(path): # type: (str) -> Optional[StorageJSON] + def load(path: str) -> Optional["StorageJSON"]: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None - def __eq__(self, o): # type: (Any) -> bool + def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() @@ -182,15 +191,15 @@ class EsphomeStorageJSON: ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version = storage_version # type: int + self.storage_version: int = storage_version # The cookie secret for the dashboard - self.cookie_secret = cookie_secret # type: str + self.cookie_secret: str = cookie_secret # The last time ESPHome checked for an update as an isoformat encoded str - self.last_update_check_str = last_update_check # type: str + self.last_update_check_str: str = last_update_check # Cache of the version gotten in the last version check - self.remote_version = remote_version # type: Optional[str] + self.remote_version: Optional[str] = remote_version - def as_dict(self): # type: () -> dict + def as_dict(self) -> dict: return { "storage_version": self.storage_version, "cookie_secret": self.cookie_secret, @@ -199,24 +208,24 @@ class EsphomeStorageJSON: } @property - def last_update_check(self): # type: () -> Optional[datetime] + def last_update_check(self) -> Optional[datetime]: try: return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") except Exception: # pylint: disable=broad-except return None @last_update_check.setter - def last_update_check(self, new): # type: (datetime) -> None + def last_update_check(self, new: datetime) -> None: self.last_update_check_str = new.strftime("%Y-%m-%dT%H:%M:%S") - def to_json(self): # type: () -> dict + def to_json(self) -> dict: return f"{json.dumps(self.as_dict(), indent=2)}\n" - def save(self, path): # type: (str) -> None + def save(self, path: str) -> None: write_file_if_changed(path, self.to_json()) @staticmethod - def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] + def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -228,14 +237,14 @@ class EsphomeStorageJSON: ) @staticmethod - def load(path): # type: (str) -> Optional[EsphomeStorageJSON] + def load(path: str) -> Optional["EsphomeStorageJSON"]: try: return EsphomeStorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None @staticmethod - def get_default(): # type: () -> EsphomeStorageJSON + def get_default() -> "EsphomeStorageJSON": return EsphomeStorageJSON( storage_version=1, cookie_secret=binascii.hexlify(os.urandom(64)).decode(), @@ -243,5 +252,5 @@ class EsphomeStorageJSON: remote_version=None, ) - def __eq__(self, o): # type: (Any) -> bool + def __eq__(self, o) -> bool: return isinstance(o, EsphomeStorageJSON) and self.as_dict() == o.as_dict() diff --git a/esphome/types.py b/esphome/types.py index 6bbfb00ce6..adb16fa91b 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,5 +1,5 @@ """This helper module tracks commonly used types in the esphome python codebase.""" -from typing import Dict, Union, List +from typing import Union from esphome.core import ID, Lambda, EsphomeCore @@ -8,11 +8,11 @@ ConfigFragmentType = Union[ int, float, None, - Dict[Union[str, int], "ConfigFragmentType"], - List["ConfigFragmentType"], + dict[Union[str, int], "ConfigFragmentType"], + list["ConfigFragmentType"], ID, Lambda, ] -ConfigType = Dict[str, ConfigFragmentType] +ConfigType = dict[str, ConfigFragmentType] CoreType = EsphomeCore ConfigPathType = Union[str, int] diff --git a/esphome/util.py b/esphome/util.py index 927c50fe89..0d60212f50 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,4 @@ -import typing -from typing import Union, List +from typing import Union import collections import io @@ -35,7 +34,7 @@ class RegistryEntry: return Schema(self.raw_schema) -class Registry(dict): +class Registry(dict[str, RegistryEntry]): def __init__(self, base_schema=None, type_id_key=None): super().__init__() self.base_schema = base_schema or {} @@ -242,7 +241,7 @@ def is_dev_esphome_version(): return "dev" in const.__version__ -def parse_esphome_version() -> typing.Tuple[int, int, int]: +def parse_esphome_version() -> tuple[int, int, int]: match = re.match(r"^(\d+).(\d+).(\d+)(-dev\d*|b\d*)?$", const.__version__) if match is None: raise ValueError(f"Failed to parse ESPHome version '{const.__version__}'") @@ -282,7 +281,7 @@ class SerialPort: # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py -def get_serial_ports() -> List[SerialPort]: +def get_serial_ports() -> list[SerialPort]: from serial.tools.list_ports import comports result = [] diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index f4ed2fe03b..281ef10964 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -204,7 +204,6 @@ class _Schema(vol.Schema): return self @schema_extractor_extended - # pylint: disable=signature-differs def extend(self, *schemas, **kwargs): extra = kwargs.pop("extra", None) if kwargs: diff --git a/esphome/vscode.py b/esphome/vscode.py index 68d59abd02..cb2f51976f 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -1,22 +1,20 @@ import json import os -# pylint: disable=unused-import +from typing import Optional + from esphome.config import load_config, _format_vol_invalid, Config from esphome.core import CORE, DocumentRange import esphome.config_validation as cv -# pylint: disable=unused-import, wrong-import-order -from typing import Optional + +def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]: + return res.get_deepest_document_range_for_path( + invalid.path, invalid.error_message == "extra keys not allowed" + ) -def _get_invalid_range(res, invalid): - # type: (Config, cv.Invalid) -> Optional[DocumentRange] - return res.get_deepest_document_range_for_path(invalid.path) - - -def _dump_range(range): - # type: (Optional[DocumentRange]) -> Optional[dict] +def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]: if range is None: return None return { diff --git a/esphome/wizard.py b/esphome/wizard.py index 602f4ecf04..fd661af639 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -9,7 +9,6 @@ import esphome.config_validation as cv from esphome.helpers import get_bool_env, write_file from esphome.log import color, Fore -# pylint: disable=anomalous-backslash-in-string from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD @@ -47,6 +46,11 @@ BASE_CONFIG = """esphome: name: {name} """ +BASE_CONFIG_FRIENDLY = """esphome: + name: {name} + friendly_name: {friendly_name} +""" + LOGGER_API_CONFIG = """ # Enable logging logger: @@ -81,11 +85,20 @@ esp32: type: esp-idf """ +RP2040_CONFIG = """ +rp2040: + board: {board} + framework: + # Required until https://github.com/platformio/platform-raspberrypi/pull/36 is merged + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git +""" + HARDWARE_BASE_CONFIGS = { "ESP8266": ESP8266_CONFIG, "ESP32": ESP32_CONFIG, "ESP32S2": ESP32S2_CONFIG, "ESP32C3": ESP32C3_CONFIG, + "RP2040": RP2040_CONFIG, } @@ -102,7 +115,12 @@ def wizard_file(**kwargs): kwargs["fallback_name"] = ap_name kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) - config = BASE_CONFIG.format(**kwargs) + if kwargs.get("friendly_name"): + base = BASE_CONFIG_FRIENDLY + else: + base = BASE_CONFIG + + config = base.format(**kwargs) config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) @@ -164,6 +182,7 @@ captive_portal: def wizard_write(path, **kwargs): from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.rp2040 import boards as rp2040_boards name = kwargs["name"] board = kwargs["board"] @@ -173,13 +192,17 @@ def wizard_write(path, **kwargs): kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: - kwargs["platform"] = ( - "ESP8266" if board in esp8266_boards.ESP8266_BOARD_PINS else "ESP32" - ) + if board in esp8266_boards.ESP8266_BOARD_PINS: + platform = "ESP8266" + elif board in rp2040_boards.RP2040_BOARD_PINS: + platform = "RP2040" + else: + platform = "ESP32" + kwargs["platform"] = platform hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) - storage = StorageJSON.from_wizard(name, f"{name}.local", hardware) + storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) 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 31b47e243e..2bf665c2b2 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -2,7 +2,7 @@ import logging import os import re from pathlib import Path -from typing import Dict, List, Union +from typing import Union from esphome.config import iter_components from esphome.const import ( @@ -98,7 +98,7 @@ def replace_file_content(text, pattern, repl): return content_new, count -def storage_should_clean(old, new): # type: (StorageJSON, StorageJSON) -> bool +def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: if old is None: return True @@ -123,7 +123,7 @@ def update_storage_json(): new.save(path) -def format_ini(data: Dict[str, Union[str, List[str]]]) -> str: +def format_ini(data: dict[str, Union[str, list[str]]]) -> str: content = "" for key, value in sorted(data.items()): if isinstance(value, list): @@ -142,7 +142,10 @@ def get_ini_content(): # Sort to avoid changing build flags order CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) - content = f"[env:{CORE.name}]\n" + content = "[platformio]\n" + content += f"description = ESPHome {__version__}\n" + + content += f"[env:{CORE.name}]\n" content += format_ini(CORE.platformio_options) return content @@ -226,7 +229,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): - source_files: List[loader.FileResource] = [] + source_files: list[loader.FileResource] = [] for _, component, _ in iter_components(CORE.config): source_files += component.resources source_files_map = { diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 75aec0edc8..8a03c431a7 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -10,7 +10,7 @@ import yaml import yaml.constructor from esphome import core -from esphome.config_helpers import read_config_file +from esphome.config_helpers import read_config_file, Extend from esphome.core import ( EsphomeError, IPAddress, @@ -88,7 +88,7 @@ def _add_data_ref(fn): return wrapped -class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors +class ESPHomeLoader(yaml.SafeLoader): """Loader class that keeps track of line numbers.""" @_add_data_ref @@ -338,6 +338,10 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors obj = self.construct_scalar(node) return add_class_to_obj(obj, ESPForceValue) + @_add_data_ref + def construct_extend(self, node): + return Extend(str(node.value)) + ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor( @@ -369,6 +373,7 @@ ESPHomeLoader.add_constructor( ) ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) +ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) def load_yaml(fname, clear_secrets=True): @@ -390,8 +395,11 @@ def _load_yaml_internal(fname): loader.dispose() -def dump(dict_): +def dump(dict_, show_secrets=False): """Dump YAML to a string and remove null.""" + if show_secrets: + _SECRET_VALUES.clear() + _SECRET_CACHE.clear() return yaml.dump( dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper ) @@ -419,7 +427,7 @@ def is_secret(value): return None -class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors +class ESPHomeDumper(yaml.SafeDumper): def represent_mapping(self, tag, mapping, flow_style=None): value = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 1fbdf7e93f..b0dddfd152 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,7 +1,7 @@ import socket import threading import time -from typing import Dict, Optional +from typing import Optional import logging from dataclasses import dataclass @@ -71,12 +71,12 @@ class DashboardStatus(threading.Thread): threading.Thread.__init__(self) self.zc = zc self.query_hosts: set[str] = set() - self.key_to_host: Dict[str, str] = {} + self.key_to_host: dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update - def request_query(self, hosts: Dict[str, str]) -> None: + def request_query(self, hosts: dict[str, str]) -> None: self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() @@ -118,14 +118,18 @@ ESPHOME_SERVICE_TYPE = "_esphomelib._tcp.local." TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url" TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_VERSION = b"project_version" +TXT_RECORD_NETWORK = b"network" +TXT_RECORD_FRIENDLY_NAME = b"friendly_name" @dataclass class DiscoveredImport: + friendly_name: Optional[str] device_name: str package_import_url: str project_name: str project_version: str + network: str class DashboardImportDiscovery: @@ -134,7 +138,7 @@ class DashboardImportDiscovery: self.service_browser = ServiceBrowser( self.zc, ESPHOME_SERVICE_TYPE, [self._on_update] ) - self.import_state = {} + self.import_state: dict[str, DiscoveredImport] = {} def _on_update( self, @@ -153,6 +157,11 @@ class DashboardImportDiscovery: return if state_change == ServiceStateChange.Removed: self.import_state.pop(name, None) + return + + if state_change == ServiceStateChange.Updated and name not in self.import_state: + # Ignore updates for devices that are not in the import state + return info = zeroconf.get_service_info(service_type, name) _LOGGER.debug("-> resolved info: %s", info) @@ -171,12 +180,18 @@ class DashboardImportDiscovery: import_url = info.properties[TXT_RECORD_PACKAGE_IMPORT_URL].decode() project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() + network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode() + friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME) + if friendly_name is not None: + friendly_name = friendly_name.decode() self.import_state[name] = DiscoveredImport( + friendly_name=friendly_name, device_name=node_name, package_import_url=import_url, project_name=project_name, project_version=project_version, + network=network, ) def cancel(self) -> None: diff --git a/platformio.ini b/platformio.ini index 9b943242f7..da3bb9d29f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,8 +34,8 @@ build_flags = [common] lib_deps = esphome/noise-c@0.1.4 ; api - makuna/NeoPixelBus@2.6.9 ; neopixelbus - esphome/Improv@1.2.1 ; improv_serial / esp32_improv + makuna/NeoPixelBus@2.7.3 ; neopixelbus + esphome/Improv@1.2.3 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 @@ -79,19 +79,20 @@ build_flags = ; This are common settings for the ESP8266 using Arduino. [common:esp8266-arduino] extends = common:arduino -platform = platformio/espressif8266 @ 3.2.0 +platform = platformio/espressif8266@3.2.0 platform_packages = - platformio/framework-arduinoespressif8266 @ ~3.30002.0 + platformio/framework-arduinoespressif8266@~3.30002.0 framework = arduino lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) - ottowinter/ESPAsyncTCP-esphome@1.2.3 ; async_tcp + esphome/ESPAsyncTCP-esphome@1.2.3 ; async_tcp ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) + crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir build_flags = ${common:arduino.build_flags} -Wno-nonnull-compare @@ -102,25 +103,26 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = platformio/espressif32 @ 3.5.0 +platform = platformio/espressif32@5.3.0 platform_packages = - platformio/framework-arduinoespressif32 @ ~3.10006.0 + platformio/framework-arduinoespressif32@~3.20005.0 framework = arduino board = nodemcu-32s lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first - FS ; web_server_base (Arduino built-in) - WiFi ; wifi,web_server_base,ethernet (Arduino built-in) - Update ; ota,web_server_base (Arduino built-in) + FS ; web_server_base (Arduino built-in) + WiFi ; wifi,web_server_base,ethernet (Arduino built-in) + Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@1.2.2 ; async_tcp - WiFiClientSecure ; http_request,nextion (Arduino built-in) - HTTPClient ; http_request,nextion (Arduino built-in) - ESPmDNS ; mdns (Arduino built-in) - DNSServer ; captive_portal (Arduino built-in) - esphome/ESP32-audioI2S@2.1.0 ; i2s_audio + esphome/AsyncTCP-esphome@1.2.2 ; async_tcp + WiFiClientSecure ; http_request,nextion (Arduino built-in) + HTTPClient ; http_request,nextion (Arduino built-in) + ESPmDNS ; mdns (Arduino built-in) + DNSServer ; captive_portal (Arduino built-in) + esphome/ESP32-audioI2S@2.0.6 ; i2s_audio + crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir build_flags = ${common:arduino.build_flags} -DUSE_ESP32 @@ -131,9 +133,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = platformio/espressif32 @ 3.5.0 +platform = platformio/espressif32@5.3.0 platform_packages = - platformio/framework-espidf @ ~3.40302.0 + platformio/framework-espidf@~3.40404.0 framework = espidf lib_deps = @@ -146,6 +148,25 @@ build_flags = -DUSE_ESP32_FRAMEWORK_ESP_IDF extra_scripts = post:esphome/components/esp32/post_build.py.script +; These are common settings for the RP2040 using Arduino. +[common:rp2040-arduino] +extends = common:arduino +board_build.core = earlephilhower +board_build.filesystem_size = 0.5m + +platform = https://github.com/maxgerhardt/platform-raspberrypi.git +platform_packages = + ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip + +framework = arduino +lib_deps = + ${common:arduino.lib_deps} +build_flags = + ${common:arduino.build_flags} + -DUSE_RP2040 + -DUSE_RP2040_FRAMEWORK_ARDUINO + ; All the actual environments are defined below. [env:esp8266-arduino] extends = common:esp8266-arduino @@ -164,6 +185,7 @@ build_flags = [env:esp32-arduino] extends = common:esp32-arduino board = esp32dev +board_build.partitions = huge_app.csv build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} @@ -222,3 +244,10 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} + +[env:rp2040-pico-arduino] +extends = common:rp2040-arduino +board = rpipico +build_flags = + ${common:rp2040-arduino.build_flags} + ${flags:runtime.build_flags} diff --git a/pyproject.toml b/pyproject.toml index 7a75060c8e..a49abb7b3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] -target-version = ["py36", "py37", "py38"] +target-version = ["py39", "py310"] exclude = 'generated' diff --git a/requirements.txt b/requirements.txt index f4cb4285ab..be5133f5f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,21 @@ voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 -colorama==0.4.5 -tornado==6.1 +colorama==0.4.6 +tornado==6.3.1 tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.0.2 # When updating platformio, also update Dockerfile -esptool==3.3.1 +platformio==6.1.6 # When updating platformio, also update Dockerfile +esptool==4.5.1 click==8.1.3 -esphome-dashboard==20220508.0 -aioesphomeapi==10.13.0 -zeroconf==0.39.0 +esphome-dashboard==20230214.0 +aioesphomeapi==13.7.2 +zeroconf==0.56.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 kconfiglib==13.7.1 + +# esp-idf >= 5.0 requires this +pyparsing >= 3.0 diff --git a/requirements_test.txt b/requirements_test.txt index ed48818276..b18aabe7b4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.14.5 -flake8==5.0.4 -black==22.6.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==2.37.3 # also change in .pre-commit-config.yaml when updating +pylint==2.17.3 +flake8==6.0.0 # also change in .pre-commit-config.yaml when updating +black==23.3.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.1.1 -pytest-cov==3.0.0 -pytest-mock==3.8.2 -pytest-asyncio==0.19.0 +pytest==7.3.1 +pytest-cov==4.0.0 +pytest-mock==3.10.0 +pytest-asyncio==0.21.0 asyncmock==0.4.2 hypothesis==5.49.0 diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py deleted file mode 100644 index f5297c062c..0000000000 --- a/script/api_protobuf/api_options_pb2.py +++ /dev/null @@ -1,251 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: api_options.proto - -import sys - -_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1")) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name="api_options.proto", - package="", - syntax="proto2", - serialized_options=None, - serialized_pb=_b( - '\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse' - ), - dependencies=[ - google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR, - ], -) - -_APISOURCETYPE = _descriptor.EnumDescriptor( - name="APISourceType", - full_name="APISourceType", - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name="SOURCE_BOTH", index=0, number=0, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SOURCE_SERVER", index=1, number=1, serialized_options=None, type=None - ), - _descriptor.EnumValueDescriptor( - name="SOURCE_CLIENT", index=2, number=2, serialized_options=None, type=None - ), - ], - containing_type=None, - serialized_options=None, - serialized_start=63, - serialized_end=133, -) -_sym_db.RegisterEnumDescriptor(_APISOURCETYPE) - -APISourceType = enum_type_wrapper.EnumTypeWrapper(_APISOURCETYPE) -SOURCE_BOTH = 0 -SOURCE_SERVER = 1 -SOURCE_CLIENT = 2 - -NEEDS_SETUP_CONNECTION_FIELD_NUMBER = 1038 -needs_setup_connection = _descriptor.FieldDescriptor( - name="needs_setup_connection", - full_name="needs_setup_connection", - index=0, - number=1038, - type=8, - cpp_type=7, - label=1, - has_default_value=True, - default_value=True, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -NEEDS_AUTHENTICATION_FIELD_NUMBER = 1039 -needs_authentication = _descriptor.FieldDescriptor( - name="needs_authentication", - full_name="needs_authentication", - index=1, - number=1039, - type=8, - cpp_type=7, - label=1, - has_default_value=True, - default_value=True, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -ID_FIELD_NUMBER = 1036 -id = _descriptor.FieldDescriptor( - name="id", - full_name="id", - index=2, - number=1036, - type=13, - cpp_type=3, - label=1, - has_default_value=True, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -SOURCE_FIELD_NUMBER = 1037 -source = _descriptor.FieldDescriptor( - name="source", - full_name="source", - index=3, - number=1037, - type=14, - cpp_type=8, - label=1, - has_default_value=True, - default_value=0, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -IFDEF_FIELD_NUMBER = 1038 -ifdef = _descriptor.FieldDescriptor( - name="ifdef", - full_name="ifdef", - index=4, - number=1038, - type=9, - cpp_type=9, - label=1, - has_default_value=False, - default_value=_b("").decode("utf-8"), - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -LOG_FIELD_NUMBER = 1039 -log = _descriptor.FieldDescriptor( - name="log", - full_name="log", - index=5, - number=1039, - type=8, - cpp_type=7, - label=1, - has_default_value=True, - default_value=True, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) -NO_DELAY_FIELD_NUMBER = 1040 -no_delay = _descriptor.FieldDescriptor( - name="no_delay", - full_name="no_delay", - index=6, - number=1040, - type=8, - cpp_type=7, - label=1, - has_default_value=True, - default_value=False, - message_type=None, - enum_type=None, - containing_type=None, - is_extension=True, - extension_scope=None, - serialized_options=None, - file=DESCRIPTOR, -) - - -_VOID = _descriptor.Descriptor( - name="void", - full_name="void", - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[], - extensions=[], - nested_types=[], - enum_types=[], - serialized_options=None, - is_extendable=False, - syntax="proto2", - extension_ranges=[], - oneofs=[], - serialized_start=55, - serialized_end=61, -) - -DESCRIPTOR.message_types_by_name["void"] = _VOID -DESCRIPTOR.enum_types_by_name["APISourceType"] = _APISOURCETYPE -DESCRIPTOR.extensions_by_name["needs_setup_connection"] = needs_setup_connection -DESCRIPTOR.extensions_by_name["needs_authentication"] = needs_authentication -DESCRIPTOR.extensions_by_name["id"] = id -DESCRIPTOR.extensions_by_name["source"] = source -DESCRIPTOR.extensions_by_name["ifdef"] = ifdef -DESCRIPTOR.extensions_by_name["log"] = log -DESCRIPTOR.extensions_by_name["no_delay"] = no_delay -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -void = _reflection.GeneratedProtocolMessageType( - "void", - (_message.Message,), - dict( - DESCRIPTOR=_VOID, - __module__="api_options_pb2" - # @@protoc_insertion_point(class_scope:void) - ), -) -_sym_db.RegisterMessage(void) - -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( - needs_setup_connection -) -google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension( - needs_authentication -) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id) -source.enum_type = _APISOURCETYPE -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(ifdef) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(log) -google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(no_delay) - -# @@protoc_insertion_point(module_scope) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 26bf8647af..dba6f47d43 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -25,7 +25,7 @@ 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 aioesphomeapi.api_options_pb2 as pb import google.protobuf.descriptor_pb2 as descriptor file_header = "// This file was automatically generated with a tool.\n" @@ -546,7 +546,8 @@ def build_enum_type(desc): out += f" {v.name} = {v.number},\n" out += "};\n" - cpp = f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n" + cpp = f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" + cpp += f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n" cpp += f" switch (value) {{\n" for v in desc.value: cpp += f" case enums::{v.name}:\n" @@ -555,6 +556,7 @@ def build_enum_type(desc): cpp += f' return "UNKNOWN";\n' cpp += f" }}\n" cpp += f"}}\n" + cpp += f"#endif\n" return out, cpp diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 0b3cdf976d..4c8639a1b3 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -245,7 +245,7 @@ def do_esp32(): setEnum( output["esp32"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], - list(esp32_boards.BOARD_TO_VARIANT.keys()), + list(esp32_boards.BOARDS.keys()), ) @@ -283,6 +283,32 @@ def fix_script(): config_schema["is_list"] = True +def fix_menu(): + # # Menu has a recursive schema which is not kept properly + schemas = output["display_menu_base"][S_SCHEMAS] + # 1. Move items to a new schema + schemas["MENU_TYPES"] = { + S_TYPE: S_SCHEMA, + S_SCHEMA: { + S_CONFIG_VARS: { + "items": schemas["DISPLAY_MENU_BASE_SCHEMA"][S_SCHEMA][S_CONFIG_VARS][ + "items" + ] + } + }, + } + # 2. Remove items from the base schema + schemas["DISPLAY_MENU_BASE_SCHEMA"][S_SCHEMA][S_CONFIG_VARS].pop("items") + # 3. Add extends to this + schemas["DISPLAY_MENU_BASE_SCHEMA"][S_SCHEMA][S_EXTENDS].append( + "display_menu_base.MENU_TYPES" + ) + # 4. Configure menu items inside as recursive + menu = schemas["MENU_TYPES"][S_SCHEMA][S_CONFIG_VARS]["items"]["types"]["menu"] + menu[S_CONFIG_VARS].pop("items") + menu[S_EXTENDS] = ["display_menu_base.MENU_TYPES"] + + def get_logger_tags(): pattern = re.compile(r'^static const char \*const TAG = "(\w.*)";', re.MULTILINE) # tags not in components dir @@ -565,6 +591,7 @@ def build_schema(): fix_script() add_logger_tags() shrink() + fix_menu() # aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc. data = {} diff --git a/script/ci-custom.py b/script/ci-custom.py index 6f69b55d2c..f95039576b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -534,6 +534,7 @@ def lint_relative_py_import(fname): "esphome/components/socket/headers.h", "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", + "esphome/components/rp2040/core.cpp", ], ) def lint_namespace(fname, content): diff --git a/script/clang-format b/script/clang-format index ae807262f1..165fbd269f 100755 --- a/script/clang-format +++ b/script/clang-format @@ -17,7 +17,7 @@ def run_format(args, queue, lock, failed_files): """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() - invocation = ["clang-format-11"] + invocation = ["clang-format-13"] if args.inplace: invocation.append("-i") else: @@ -59,14 +59,14 @@ def main(): args = parser.parse_args() try: - get_output("clang-format-11", "-version") + get_output("clang-format-13", "-version") except: print( """ Oops. It looks like clang-format is not installed. - Please check you can run "clang-format-11 -version" in your terminal and install - clang-format (v11) if necessary. + Please check you can run "clang-format-13 -version" in your terminal and install + clang-format (v13) if necessary. Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-format. diff --git a/script/clang-tidy b/script/clang-tidy index 327b593008..5d2cba6eb5 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -82,6 +82,7 @@ def clang_options(idedata): "-mtext-section-literals", "-mfix-esp32-psram-cache-issue", "-mfix-esp32-psram-cache-strategy=memw", + "-fno-tree-switch-conversion", ) ) diff --git a/script/lint-python b/script/lint-python index 90b5dcd59f..7de1de80b0 100755 --- a/script/lint-python +++ b/script/lint-python @@ -109,7 +109,7 @@ def main(): print_error(file_, linno, msg) errors += 1 - PYUPGRADE_TARGET = "--py38-plus" + PYUPGRADE_TARGET = "--py39-plus" cmd = ["pyupgrade", PYUPGRADE_TARGET] + files print() print("Running pyupgrade...") diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py new file mode 100755 index 0000000000..2340410161 --- /dev/null +++ b/script/platformio_install_deps.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# This script is used to preinstall +# all platformio libraries in the global storage + +import configparser +import subprocess +import sys + +config = configparser.ConfigParser(inline_comment_prefixes=(";",)) +config.read(sys.argv[1]) + +libs = [] +tools = [] +platforms = [] +# Extract from every lib_deps key in all sections +for section in config.sections(): + conf = config[section] + if "lib_deps" in conf: + for lib_dep in conf["lib_deps"].splitlines(): + if not lib_dep: + # Empty line or comment + continue + if lib_dep.startswith("${"): + # Extending from another section + continue + if "@" not in lib_dep: + # No version pinned, this is an internal lib + continue + libs.append("-l") + libs.append(lib_dep) + if "platform" in conf: + platforms.append("-p") + platforms.append(conf["platform"]) + if "platform_packages" in conf: + for tool in conf["platform_packages"].splitlines(): + if not tool: + # Empty line or comment + continue + if tool.startswith("${"): + # Extending from another section + continue + if tool.find("https://github.com") != -1: + split = tool.find("@") + tool = tool[split + 1 :] + tools.append("-t") + tools.append(tool) + +subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools]) diff --git a/script/setup b/script/setup index 71828deeaa..5acd1a9f13 100755 --- a/script/setup +++ b/script/setup @@ -4,7 +4,15 @@ set -e cd "$(dirname "$0")/.." + +if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ]; then + python3 -m venv venv + source venv/bin/activate +fi + pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip3 install --no-use-pep517 -e . pre-commit install + +script/platformio_install_deps.py platformio.ini diff --git a/script/sync-device_class.py b/script/sync-device_class.py new file mode 100755 index 0000000000..ae6f4be0c8 --- /dev/null +++ b/script/sync-device_class.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import re + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass + +BLOCKLIST = ( + # requires special support on HA side + "enum", +) + +DOMAINS = { + "binary_sensor": BinarySensorDeviceClass, + "button": ButtonDeviceClass, + "cover": CoverDeviceClass, + "number": NumberDeviceClass, + "sensor": SensorDeviceClass, + "switch": SwitchDeviceClass, +} + + +def sub(path, pattern, repl): + with open(path) as handle: + content = handle.read() + content = re.sub(pattern, repl, content, flags=re.MULTILINE) + with open(path, "w") as handle: + handle.write(content) + + +def main(): + classes = {"EMPTY": ""} + allowed = {} + + for domain, enum in DOMAINS.items(): + available = { + cls.value.upper(): cls.value for cls in enum if cls.value not in BLOCKLIST + } + + classes.update(available) + allowed[domain] = list(available.keys()) + ["EMPTY"] + + # replace constant defines in const.py + out = "" + for cls in sorted(classes): + out += f'DEVICE_CLASS_{cls.upper()} = "{classes[cls]}"\n' + sub("esphome/const.py", '(DEVICE_CLASS_\\w+ = "\\w*"\r?\n)+', out) + + for domain in sorted(allowed): + # replace imports + out = "" + for item in sorted(allowed[domain]): + out += f" DEVICE_CLASS_{item.upper()},\n" + + sub( + f"esphome/components/{domain}/__init__.py", + "( DEVICE_CLASS_\\w+,\r?\n)+", + out, + ) + + +if __name__ == "__main__": + main() diff --git a/script/test b/script/test index 9f5dca65fa..36be9118ed 100755 --- a/script/test +++ b/script/test @@ -11,3 +11,4 @@ esphome compile tests/test2.yaml esphome compile tests/test3.yaml esphome compile tests/test4.yaml esphome compile tests/test5.yaml +esphome compile tests/test8.yaml diff --git a/setup.py b/setup.py index 941c8089ec..95453960ff 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( zip_safe=False, platforms="any", test_suite="tests", - python_requires=">=3.8,<4.0", + python_requires=">=3.9.0", install_requires=REQUIRES, keywords=["home", "automation"], entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, diff --git a/tests/README.md b/tests/README.md index 546025526f..6d83fc6886 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,3 +24,6 @@ Current test_.yaml file contents. | test3.yaml | ESP8266 | wifi | N/A | test4.yaml | ESP32 | ethernet | None | test5.yaml | ESP32 | wifi | ble_server +| test6.yaml | RP2040 | wifi | N/A +| test7.yaml | ESP32-C3 | wifi | N/A +| test8.yaml | ESP32-S3 | wifi | None diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.yaml b/tests/component_tests/binary_sensor/test_binary_sensor.yaml index 912ae115eb..f98ce693f7 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.yaml +++ b/tests/component_tests/binary_sensor/test_binary_sensor.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test platform: ESP8266 @@ -6,13 +7,13 @@ esphome: binary_sensor: - platform: gpio id: bs_1 - name: "test bs1" + name: test bs1 internal: true pin: number: D0 - platform: gpio id: bs_2 - name: "test bs2" + name: test bs2 internal: false pin: number: D1 diff --git a/tests/component_tests/button/test_button.yaml b/tests/component_tests/button/test_button.yaml index 32d2e8d93b..48e13f0353 100644 --- a/tests/component_tests/button/test_button.yaml +++ b/tests/component_tests/button/test_button.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test platform: ESP8266 diff --git a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml index 18a425df58..96514a677f 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml +++ b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test platform: ESP32 diff --git a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml index 49a7f510f2..0e8e598402 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml +++ b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test platform: ESP32 diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py new file mode 100644 index 0000000000..0e24d78f5c --- /dev/null +++ b/tests/component_tests/packages/test_packages.py @@ -0,0 +1,351 @@ +"""Tests for the packages component.""" + +import pytest + + +from esphome.const import ( + CONF_DOMAIN, + CONF_ESPHOME, + CONF_FILTERS, + CONF_ID, + CONF_MULTIPLY, + CONF_NAME, + CONF_OFFSET, + CONF_PACKAGES, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_SENSOR, + CONF_SSID, + CONF_UPDATE_INTERVAL, + CONF_WIFI, +) +from esphome.components.packages import do_packages_pass +from esphome.config_helpers import Extend +import esphome.config_validation as cv + +# Test strings +TEST_DEVICE_NAME = "test_device_name" +TEST_PLATFORM = "test_platform" +TEST_WIFI_SSID = "test_wifi_ssid" +TEST_PACKAGE_WIFI_SSID = "test_package_wifi_ssid" +TEST_PACKAGE_WIFI_PASSWORD = "test_package_wifi_password" +TEST_DOMAIN = "test_domain_name" +TEST_SENSOR_PLATFORM_1 = "test_sensor_platform_1" +TEST_SENSOR_PLATFORM_2 = "test_sensor_platform_2" +TEST_SENSOR_NAME_1 = "test_sensor_name_1" +TEST_SENSOR_NAME_2 = "test_sensor_name_2" +TEST_SENSOR_ID_1 = "test_sensor_id_1" +TEST_SENSOR_ID_2 = "test_sensor_id_2" +TEST_SENSOR_UPDATE_INTERVAL = "test_sensor_update_interval" + + +@pytest.fixture(name="basic_wifi") +def fixture_basic_wifi(): + return { + CONF_SSID: TEST_PACKAGE_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + } + + +@pytest.fixture(name="basic_esphome") +def fixture_basic_esphome(): + return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM} + + +def test_package_unused(basic_esphome, basic_wifi): + """ + Ensures do_package_pass does not change a config if packages aren't used. + """ + config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + actual = do_packages_pass(config) + assert actual == config + + +def test_package_invalid_dict(basic_esphome, basic_wifi): + """ + Ensures an error is raised if packages is not valid. + + """ + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi} + + with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +def test_package_include(basic_wifi, basic_esphome): + """ + Tests the simple case where an independent config present in a package is added to the top-level config as is. + + In this test, the CONF_WIFI config is expected to be simply added to the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + } + + expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_append(basic_wifi, basic_esphome): + """ + Tests the case where a key is present in both a package and top-level config. + + In this test, CONF_WIFI is defined in a package, and CONF_DOMAIN is added to it at the top level. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: {CONF_DOMAIN: TEST_DOMAIN}, + } + + expected = { + CONF_ESPHOME: basic_esphome, + CONF_WIFI: { + CONF_SSID: TEST_PACKAGE_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + CONF_DOMAIN: TEST_DOMAIN, + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_override(basic_wifi, basic_esphome): + """ + Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. + + In this test, CONF_SSID should be overwritten by that defined in the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: {CONF_SSID: TEST_WIFI_SSID}, + } + + expected = { + CONF_ESPHOME: basic_esphome, + CONF_WIFI: { + CONF_SSID: TEST_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_multiple_package_order(): + """ + Ensures that mutiple packages are merged in order. + """ + config = { + CONF_PACKAGES: { + "package1": { + "logger": { + "level": "DEBUG", + }, + }, + "package2": { + "logger": { + "level": "VERBOSE", + }, + }, + }, + } + + expected = { + "logger": { + "level": "VERBOSE", + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_list_merge(): + """ + Ensures lists defined in both a package and the top-level config are merged correctly + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + }, + CONF_SENSOR: [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ], + } + + expected = { + CONF_SENSOR: [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_2}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_list_merge_by_id(): + """ + Ensures that components with matching IDs are merged correctly. + + In this test, a sensor is defined in a package, and a CONF_UPDATE_INTERVAL is added at the top level, + and a sensor name is overridden in another sensor. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + "package2": { + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_DOMAIN: "2", + } + ], + }, + "package3": { + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_DOMAIN: "3", + } + ], + }, + }, + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL, + }, + {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL, + CONF_DOMAIN: "3", + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_merge_by_id_with_list(): + """ + Ensures that components with matching IDs are merged correctly when their configuration contains lists. + + For example, a sensor with filters defined in both a package and the top level config should be merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]} + ] + } + }, + CONF_SENSOR: [ + {CONF_ID: Extend(TEST_SENSOR_ID_1), CONF_FILTERS: [{CONF_OFFSET: 146.0}]} + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}, {CONF_OFFSET: 146.0}], + } + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_merge_by_missing_id(): + """ + Ensures that components with missing IDs are not merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}, + ] + } + }, + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]}, + {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], + }, + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], + }, + { + CONF_ID: Extend(TEST_SENSOR_ID_2), + CONF_FILTERS: [{CONF_OFFSET: 146.0}], + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected diff --git a/tests/component_tests/sensor/test_sensor.yaml b/tests/component_tests/sensor/test_sensor.yaml index a38dd14041..8c0fd85b17 100644 --- a/tests/component_tests/sensor/test_sensor.yaml +++ b/tests/component_tests/sensor/test_sensor.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test platform: ESP8266 @@ -7,6 +8,6 @@ sensor: - platform: adc pin: A0 id: s_1 - name: "test s1" + name: test s1 update_interval: 60s - device_class: "voltage" + device_class: voltage diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index d956387665..236b9f5fc2 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -3,18 +3,19 @@ // matter at all, as long as it compiles). // Not used during runtime nor for CI. -#include -#include -#include -#include #include +#include +#include +#include +#include using namespace esphome; void setup() { - App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); - auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0); // NOLINT + App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); + log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); auto *wifi = new wifi::WiFiComponent(); // NOLINT diff --git a/tests/test1.yaml b/tests/test1.yaml index 1a9d69eac8..a235ff1502 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1,3 +1,4 @@ +--- substitutions: devicename: test1 sensorname: my @@ -43,13 +44,13 @@ esphome: json: key: !lambda |- return id(${textname}_text).state; - greeting: "Hello World" + greeting: Hello World - http_request.send: method: PUT url: https://esphome.io headers: Content-Type: application/json - body: "Some data" + body: Some data verify_ssl: false on_response: then: @@ -95,8 +96,8 @@ mqtt: password: "debug" client_id: someclient use_abbreviations: false - discovery: True - discovery_retain: False + discovery: true + discovery_retain: false discovery_prefix: discovery discovery_unique_id_generator: legacy topic_prefix: helloworld @@ -109,7 +110,7 @@ mqtt: topic: topic/to/send/to payload: hi qos: 2 - retain: True + retain: true keepalive: 60s reboot_timeout: 60s on_message: @@ -154,18 +155,19 @@ mqtt: return effect; - light.control: id: ${roomname}_lights + # yamllint disable-line rule:line-length brightness: !lambda "return id(${roomname}_lights).current_values.get_brightness() + 0.5;" - light.dim_relative: id: ${roomname}_lights relative_brightness: 5% - uart.write: - id: uart0 + id: uart_0 data: Hello World - uart.write: - id: uart0 + id: uart_0 data: [0x00, 0x20, 0x30] - uart.write: - id: uart0 + id: uart_0 data: !lambda |- return {}; on_connect: @@ -179,7 +181,7 @@ mqtt: i2c: sda: 21 scl: 22 - scan: True + scan: true frequency: 100kHz setup_priority: -100 id: i2c_bus @@ -192,12 +194,12 @@ spi: uart: - tx_pin: number: GPIO22 - inverted: yes + inverted: true rx_pin: number: GPIO23 - inverted: yes + inverted: true baud_rate: 115200 - id: uart0 + id: uart_0 parity: NONE data_bits: 8 stop_bits: 1 @@ -220,9 +222,15 @@ uart: rx_pin: GPIO26 baud_rate: 115200 rx_buffer_size: 1024 + - id: ld2410_uart + tx_pin: 18 + rx_pin: 23 + baud_rate: 256000 + parity: NONE + stop_bits: 1 ota: - safe_mode: True + safe_mode: true password: "superlongpasswordthatnoonewillknow" port: 3286 reboot_timeout: 2min @@ -233,14 +241,14 @@ ota: ESP_LOGD("ota", "State %d", state); on_begin: then: - logger.log: "OTA begin" + logger.log: OTA begin on_progress: then: lambda: >- ESP_LOGD("ota", "Got progress %f", x); on_end: then: - logger.log: "OTA end" + logger.log: OTA end on_error: then: lambda: >- @@ -258,7 +266,7 @@ web_server: version: 2 power_supply: - id: "atx_power_supply" + id: atx_power_supply enable_time: 20ms keep_on_time: 10s pin: @@ -288,8 +296,6 @@ adalight: esp32_ble_tracker: -bluetooth_proxy: - ble_client: - mac_address: AA:BB:CC:DD:EE:FF id: ble_foo @@ -308,22 +314,25 @@ bedjet: id: my_bedjet_client time_id: sntp_time mcp23s08: - - id: "mcp23s08_hub" + - id: mcp23s08_hub cs_pin: GPIO12 deviceaddress: 0 mcp23s17: - - id: "mcp23s17_hub" + - id: mcp23s17_hub cs_pin: GPIO12 deviceaddress: 1 sensor: + - platform: internal_temperature + name: Internal Temperature - platform: ble_client + type: characteristic ble_client_id: ble_foo - name: "Green iTag btn" - service_uuid: "ffe0" - characteristic_uuid: "ffe1" - descriptor_uuid: "ffe2" + name: Green iTag btn + service_uuid: ffe0 + characteristic_uuid: ffe1 + descriptor_uuid: ffe2 notify: true update_interval: never lambda: |- @@ -333,9 +342,14 @@ sensor: then: - lambda: |- ESP_LOGD("green_btn", "Button was pressed, val%f", x); + - platform: ble_client + type: rssi + ble_client_id: ble_foo + name: Green iTag RSSI + update_interval: 15s - platform: adc pin: A0 - name: "Living Room Brightness" + name: Living Room Brightness update_interval: "1:01" attenuation: 2.5db unit_of_measurement: "°C" @@ -378,17 +392,20 @@ sensor: - heartbeat: 5s - debounce: 0.1s - delta: 5.0 + - delta: 1% - or: - throttle: 1s - delta: 5.0 - lambda: return x * (9.0/5.0) + 32.0; on_value: then: + # yamllint disable rule:line-length - lambda: |- ESP_LOGD("main", "Got value %f", x); id(${sensorname}_sensor).publish_state(42.0); ESP_LOGI("main", "Value of my sensor: %f", id(${sensorname}_sensor).state); ESP_LOGI("main", "Raw Value of my sensor: %f", id(${sensorname}_sensor).state); + # yamllint enable rule:line-length on_value_range: above: 5 below: 10 @@ -405,79 +422,105 @@ sensor: ESP_LOGD("main", "Got raw value %f", x); - logger.log: level: DEBUG - format: "Got raw value %f" + format: Got raw value %f args: ["x"] - - logger.log: "Got raw value NAN" + - logger.log: Got raw value NAN - mqtt.publish: topic: some/topic payload: Hello qos: 2 - retain: True + retain: true - platform: esp32_hall name: ESP32 Hall Sensor - platform: ads1115 - multiplexer: "A0_A1" + multiplexer: A0_A1 gain: 1.024 id: ${sensorname}_sensor filters: state_topic: hi/me retain: false availability: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR + i2c_id: i2c_bus - platform: atm90e32 cs_pin: 5 phase_a: voltage: - name: "EMON Line Voltage A" + name: EMON Line Voltage A current: - name: "EMON CT1 Current" + name: EMON CT1 Current power: - name: "EMON Active Power CT1" + name: EMON Active Power CT1 reactive_power: - name: "EMON Reactive Power CT1" + name: EMON Reactive Power CT1 power_factor: - name: "EMON Power Factor CT1" + name: EMON Power Factor CT1 gain_voltage: 7305 gain_ct: 27961 phase_b: current: - name: "EMON CT2 Current" + name: EMON CT2 Current power: - name: "EMON Active Power CT2" + name: EMON Active Power CT2 reactive_power: - name: "EMON Reactive Power CT2" + name: EMON Reactive Power CT2 power_factor: - name: "EMON Power Factor CT2" + name: EMON Power Factor CT2 gain_voltage: 7305 gain_ct: 27961 phase_c: current: - name: "EMON CT3 Current" + name: EMON CT3 Current power: - name: "EMON Active Power CT3" + name: EMON Active Power CT3 reactive_power: - name: "EMON Reactive Power CT3" + name: EMON Reactive Power CT3 power_factor: - name: "EMON Power Factor CT3" + name: EMON Power Factor CT3 gain_voltage: 7305 gain_ct: 27961 frequency: - name: "EMON Line Frequency" + name: EMON Line Frequency chip_temperature: - name: "EMON Chip Temp A" + name: EMON Chip Temp A line_frequency: 60Hz current_phases: 3 gain_pga: 2X - platform: bh1750 - name: "Living Room Brightness 3" + name: Living Room Brightness 3 internal: true address: 0x23 update_interval: 30s - retain: False + retain: false availability: state_topic: livingroom/custom_state_topic i2c_id: i2c_bus - platform: max44009 - name: "Outside Brightness 1" + name: Outside Brightness 1 internal: true address: 0x4A update_interval: 30s @@ -485,13 +528,13 @@ sensor: i2c_id: i2c_bus - platform: bme280 temperature: - name: "Outside Temperature" + name: Outside Temperature oversampling: 16x pressure: - name: "Outside Pressure" + name: Outside Pressure oversampling: none humidity: - name: "Outside Humidity" + name: Outside Humidity oversampling: 8x address: 0x77 iir_filter: 16x @@ -499,14 +542,14 @@ sensor: i2c_id: i2c_bus - platform: bme680 temperature: - name: "Outside Temperature" + name: Outside Temperature oversampling: 16x pressure: - name: "Outside Pressure" + name: Outside Pressure humidity: - name: "Outside Humidity" + name: Outside Humidity gas_resistance: - name: "Outside Gas Sensor" + name: Outside Gas Sensor address: 0x77 heater: temperature: 320 @@ -515,9 +558,9 @@ sensor: i2c_id: i2c_bus - platform: bmp085 temperature: - name: "Outside Temperature" + name: Outside Temperature pressure: - name: "Outside Pressure" + name: Outside Pressure filters: - lambda: >- return x / powf(1.0 - (x / 44330.0), 5.255); @@ -525,54 +568,65 @@ sensor: i2c_id: i2c_bus - platform: bmp280 temperature: - name: "Outside Temperature" + name: Outside Temperature oversampling: 16x pressure: - name: "Outside Pressure" + name: Outside Pressure address: 0x77 update_interval: 15s iir_filter: 16x i2c_id: i2c_bus - platform: dallas address: 0x1C0000031EDD2A28 - name: "Living Room Temperature" + name: Living Room Temperature resolution: 9 - platform: dallas index: 1 - name: "Living Room Temperature 2" + name: Living Room Temperature 2 - platform: dht pin: GPIO26 temperature: - name: "Living Room Temperature 3" + id: dht_temperature + name: Living Room Temperature 3 humidity: - name: "Living Room Humidity 3" + id: dht_humidity + name: Living Room Humidity 3 model: AM2302 update_interval: 15s - platform: dht12 temperature: - name: "Living Room Temperature 4" + name: Living Room Temperature 4 humidity: - name: "Living Room Humidity 4" + name: Living Room Humidity 4 update_interval: 15s i2c_id: i2c_bus - platform: duty_cycle pin: GPIO25 name: Duty Cycle Sensor + - platform: ee895 + co2: + name: Office CO2 1 + temperature: + name: Office Temperature 1 + pressure: + name: Office Pressure 1 + address: 0x5F + i2c_id: i2c_bus - platform: esp32_hall - name: "ESP32 Hall Sensor" + name: ESP32 Hall Sensor update_interval: 15s - platform: ens210 temperature: - name: "Living Room Temperature 5" + name: Living Room Temperature 5 humidity: - name: 'Living Room Humidity 5' + name: Living Room Humidity 5 update_interval: 15s i2c_id: i2c_bus - platform: hdc1080 temperature: - name: 'Living Room Temperature 6' + name: Living Room Temperature 6 humidity: - name: 'Living Room Humidity 5' + name: Living Room Humidity 5 update_interval: 15s i2c_id: i2c_bus - platform: hlw8012 @@ -580,14 +634,14 @@ sensor: cf_pin: 14 cf1_pin: 13 current: - name: "HLW8012 Current" + name: HLW8012 Current voltage: - name: "HLW8012 Voltage" + name: HLW8012 Voltage power: - name: "HLW8012 Power" + name: HLW8012 Power id: hlw8012_power energy: - name: "HLW8012 Energy" + name: HLW8012 Energy id: hlw8012_energy update_interval: 15s current_resistor: 0.001 ohm @@ -597,53 +651,60 @@ sensor: model: hlw8012 - platform: total_daily_energy power_id: hlw8012_power - name: "HLW8012 Total Daily Energy" + name: HLW8012 Total Daily Energy - platform: integration sensor: hlw8012_power - name: "Integration Sensor" + name: Integration Sensor time_unit: s - platform: integration sensor: hlw8012_power - name: "Integration Sensor lazy" + name: Integration Sensor lazy time_unit: s - platform: hmc5883l address: 0x68 field_strength_x: - name: "HMC5883L Field Strength X" + name: HMC5883L Field Strength X field_strength_y: - name: "HMC5883L Field Strength Y" + name: HMC5883L Field Strength Y field_strength_z: - name: "HMC5883L Field Strength Z" + name: HMC5883L Field Strength Z heading: - name: "HMC5883L Heading" + name: HMC5883L Heading range: 130uT oversampling: 8x update_interval: 15s i2c_id: i2c_bus - platform: honeywellabp pressure: - name: "Honeywell pressure" + name: Honeywell pressure min_pressure: 0 max_pressure: 15 temperature: - name: "Honeywell temperature" + name: Honeywell temperature cs_pin: GPIO5 + - platform: hte501 + temperature: + name: Office Temperature 2 + humidity: + name: Office Humidity 1 + address: 0x40 + i2c_id: i2c_bus - platform: qmc5883l address: 0x0D field_strength_x: - name: "QMC5883L Field Strength X" + name: QMC5883L Field Strength X field_strength_y: - name: "QMC5883L Field Strength Y" + name: QMC5883L Field Strength Y field_strength_z: - name: "QMC5883L Field Strength Z" + name: QMC5883L Field Strength Z heading: - name: "QMC5883L Heading" + name: QMC5883L Heading range: 800uT oversampling: 256x update_interval: 15s i2c_id: i2c_bus - platform: hx711 - name: "HX711 Value" + name: HX711 Value dout_pin: GPIO23 clk_pin: GPIO25 gain: 128 @@ -652,13 +713,13 @@ sensor: address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA219 Current" + name: INA219 Current power: - name: "INA219 Power" + name: INA219 Power bus_voltage: - name: "INA219 Bus Voltage" + name: INA219 Bus Voltage shunt_voltage: - name: "INA219 Shunt Voltage" + name: INA219 Shunt Voltage max_voltage: 32.0V max_current: 3.2A update_interval: 15s @@ -667,13 +728,13 @@ sensor: address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA226 Current" + name: INA226 Current power: - name: "INA226 Power" + name: INA226 Power bus_voltage: - name: "INA226 Bus Voltage" + name: INA226 Bus Voltage shunt_voltage: - name: "INA226 Shunt Voltage" + name: INA226 Shunt Voltage max_current: 3.2A update_interval: 15s i2c_id: i2c_bus @@ -682,17 +743,17 @@ sensor: channel_1: shunt_resistance: 0.1 ohm current: - name: "INA3221 Channel 1 Current" + name: INA3221 Channel 1 Current power: - name: "INA3221 Channel 1 Power" + name: INA3221 Channel 1 Power bus_voltage: - name: "INA3221 Channel 1 Bus Voltage" + name: INA3221 Channel 1 Bus Voltage shunt_voltage: - name: "INA3221 Channel 1 Shunt Voltage" + name: INA3221 Channel 1 Shunt Voltage update_interval: 15s i2c_id: i2c_bus - platform: kalman_combinator - name: "Kalman-filtered temperature" + name: Kalman-filtered temperature process_std_dev: 0.00139 sources: - source: scd30_temperature @@ -702,114 +763,123 @@ sensor: error: 1.5 - platform: htu21d temperature: - name: "Living Room Temperature 6" + name: Living Room Temperature 6 humidity: - name: "Living Room Humidity 6" + name: Living Room Humidity 6 update_interval: 15s i2c_id: i2c_bus - platform: max6675 - name: "Living Room Temperature" + name: Living Room Temperature cs_pin: GPIO23 update_interval: 15s - platform: max31855 - name: "Den Temperature" + name: Den Temperature cs_pin: GPIO23 update_interval: 15s reference_temperature: - name: "MAX31855 Internal Temperature" + name: MAX31855 Internal Temperature - platform: max31856 - name: "BBQ Temperature" + name: BBQ Temperature cs_pin: GPIO17 update_interval: 15s mains_filter: 50Hz - platform: max31865 - name: "Water Tank Temperature" + name: Water Tank Temperature cs_pin: GPIO23 update_interval: 15s - reference_resistance: "430 Ω" - rtd_nominal_resistance: "100 Ω" + reference_resistance: 430 Ω + rtd_nominal_resistance: 100 Ω - platform: mhz19 - uart_id: uart0 + uart_id: uart_0 co2: - name: "MH-Z19 CO2 Value" + name: MH-Z19 CO2 Value temperature: - name: "MH-Z19 Temperature" + name: MH-Z19 Temperature update_interval: 15s automatic_baseline_calibration: false - platform: mpu6050 address: 0x68 accel_x: - name: "MPU6050 Accel X" + name: MPU6050 Accel X accel_y: - name: "MPU6050 Accel Y" + name: MPU6050 Accel Y accel_z: - name: "MPU6050 Accel z" + name: MPU6050 Accel z gyro_x: - name: "MPU6050 Gyro X" + name: MPU6050 Gyro X gyro_y: - name: "MPU6050 Gyro Y" + name: MPU6050 Gyro Y gyro_z: - name: "MPU6050 Gyro z" + name: MPU6050 Gyro z temperature: - name: "MPU6050 Temperature" + name: MPU6050 Temperature i2c_id: i2c_bus - platform: mpu6886 address: 0x68 accel_x: - name: "MPU6886 Accel X" + name: MPU6886 Accel X accel_y: - name: "MPU6886 Accel Y" + name: MPU6886 Accel Y accel_z: - name: "MPU6886 Accel z" + name: MPU6886 Accel z gyro_x: - name: "MPU6886 Gyro X" + name: MPU6886 Gyro X gyro_y: - name: "MPU6886 Gyro Y" + name: MPU6886 Gyro Y gyro_z: - name: "MPU6886 Gyro z" + name: MPU6886 Gyro z temperature: - name: "MPU6886 Temperature" + name: MPU6886 Temperature + i2c_id: i2c_bus + - platform: mmc5603 + address: 0x30 + field_strength_x: + name: HMC5883L Field Strength X + field_strength_y: + name: HMC5883L Field Strength Y + field_strength_z: + name: HMC5883L Field Strength Z i2c_id: i2c_bus - platform: dps310 temperature: - name: "DPS310 Temperature" + name: DPS310 Temperature pressure: - name: "DPS310 Pressure" + name: DPS310 Pressure address: 0x77 update_interval: 15s i2c_id: i2c_bus - platform: ms5611 temperature: - name: "Outside Temperature" + name: Outside Temperature pressure: - name: "Outside Pressure" + name: Outside Pressure address: 0x77 update_interval: 15s i2c_id: i2c_bus - platform: pmsa003i pm_1_0: - name: "PMSA003i PM1.0" + name: PMSA003i PM1.0 pm_2_5: - name: "PMSA003i PM2.5" + name: PMSA003i PM2.5 pm_10_0: - name: "PMSA003i PM10.0" + name: PMSA003i PM10.0 pmc_0_3: - name: "PMSA003i PMC <0.3µm" + name: PMSA003i PMC <0.3µm pmc_0_5: - name: "PMSA003i PMC <0.5µm" + name: PMSA003i PMC <0.5µm pmc_1_0: - name: "PMSA003i PMC <1µm" + name: PMSA003i PMC <1µm pmc_2_5: - name: "PMSA003i PMC <2.5µm" + name: PMSA003i PMC <2.5µm pmc_5_0: - name: "PMSA003i PMC <5µm" + name: PMSA003i PMC <5µm pmc_10_0: - name: "PMSA003i PMC <10µm" + name: PMSA003i PMC <10µm address: 0x12 - standard_units: True + standard_units: true i2c_id: i2c_bus - platform: pulse_counter - name: "Pulse Counter" + name: Pulse Counter pin: GPIO12 count_mode: rising_edge: INCREMENT @@ -817,7 +887,7 @@ sensor: internal_filter: 13us update_interval: 15s - platform: pulse_meter - name: "Pulse Meter" + name: Pulse Meter id: pulse_meter_sensor pin: GPIO12 internal_filter: 100ms @@ -827,20 +897,20 @@ sensor: id: pulse_meter_sensor value: 12345 total: - name: "Pulse Meter Total" + name: Pulse Meter Total - platform: qmp6988 temperature: - name: "Living Temperature QMP" + name: Living Temperature QMP oversampling: 32x pressure: - name: "Living Pressure QMP" + name: Living Pressure QMP oversampling: 2x address: 0x70 update_interval: 30s iir_filter: 16x i2c_id: i2c_bus - platform: rotary_encoder - name: "Rotary Encoder" + name: Rotary Encoder id: rotary_encoder1 pin_a: GPIO23 pin_b: GPIO25 @@ -860,49 +930,51 @@ sensor: id: rotary_encoder1 value: !lambda "return -1;" on_clockwise: - - logger.log: "Clockwise" + - logger.log: Clockwise + - display_menu.down: on_anticlockwise: - - logger.log: "Anticlockwise" + - logger.log: Anticlockwise + - display_menu.up: - platform: pulse_width name: Pulse Width pin: GPIO12 - platform: sm300d2 - uart_id: uart0 + uart_id: uart_0 co2: - name: "SM300D2 CO2 Value" + name: SM300D2 CO2 Value formaldehyde: - name: "SM300D2 Formaldehyde Value" + name: SM300D2 Formaldehyde Value tvoc: - name: "SM300D2 TVOC Value" + name: SM300D2 TVOC Value pm_2_5: - name: "SM300D2 PM2.5 Value" + name: SM300D2 PM2.5 Value pm_10_0: - name: "SM300D2 PM10 Value" + name: SM300D2 PM10 Value temperature: - name: "SM300D2 Temperature Value" + name: SM300D2 Temperature Value humidity: - name: "SM300D2 Humidity Value" + name: SM300D2 Humidity Value update_interval: 60s - platform: sht3xd temperature: - name: "Living Room Temperature 8" + name: Living Room Temperature 8 humidity: - name: "Living Room Humidity 8" + name: Living Room Humidity 8 address: 0x44 i2c_id: i2c_bus update_interval: 15s - platform: sts3x - name: "Living Room Temperature 9" + name: Living Room Temperature 9 address: 0x4A i2c_id: i2c_bus - platform: scd30 co2: - name: "Living Room CO2 9" + name: Living Room CO2 9 temperature: id: scd30_temperature - name: "Living Room Temperature 9" + name: Living Room Temperature 9 humidity: - name: "Living Room Humidity 9" + name: Living Room Humidity 9 address: 0x61 update_interval: 15s automatic_self_calibration: true @@ -913,12 +985,12 @@ sensor: - platform: scd4x id: scd40 co2: - name: "SCD4X CO2" + name: SCD4X CO2 temperature: id: scd4x_temperature - name: "SCD4X Temperature" + name: SCD4X Temperature humidity: - name: "SCD4X Humidity" + name: SCD4X Humidity update_interval: 15s automatic_self_calibration: true altitude_compensation: 10m @@ -927,63 +999,63 @@ sensor: i2c_id: i2c_bus - platform: sgp30 eco2: - name: "Workshop eCO2" + name: Workshop eCO2 accuracy_decimals: 1 tvoc: - name: "Workshop TVOC" + name: Workshop TVOC accuracy_decimals: 1 address: 0x58 update_interval: 5s i2c_id: i2c_bus - platform: sps30 pm_1_0: - name: "Workshop PM <1µm Weight concentration" - id: "workshop_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" + 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" + 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" + 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" + 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" + 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" + 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" + 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" + name: Workshop PM <10µm Number concentration + id: workshop_PMC_10_0 address: 0x69 update_interval: 10s i2c_id: i2c_bus - platform: sht4x temperature: - name: "SHT4X Temperature" + name: SHT4X Temperature humidity: - name: "SHT4X Humidity" + name: SHT4X Humidity address: 0x44 update_interval: 15s i2c_id: i2c_bus - platform: shtcx temperature: - name: "Living Room Temperature 10" + name: Living Room Temperature 10 humidity: - name: "Living Room Humidity 10" + name: Living Room Humidity 10 address: 0x70 update_interval: 15s i2c_id: i2c_bus - platform: template - name: "Template Sensor" + name: Template Sensor state_class: measurement id: template_sensor lambda: |- @@ -1001,7 +1073,7 @@ sensor: id: template_sensor state: !lambda "return NAN;" - platform: tsl2561 - name: "TSL2561 Ambient Light" + name: TSL2561 Ambient Light address: 0x39 update_interval: 15s is_cs_package: true @@ -1015,35 +1087,41 @@ sensor: integration_time: 600ms gain: high visible: - name: "tsl2591 visible" + name: tsl2591 visible id: tsl2591_vis - unit_of_measurement: "pH" + unit_of_measurement: pH infrared: - name: "tsl2591 infrared" + name: tsl2591 infrared id: tsl2591_ir full_spectrum: - name: "tsl2591 full_spectrum" + name: tsl2591 full_spectrum id: tsl2591_fs calculated_lux: - name: "tsl2591 calculated_lux" + name: tsl2591 calculated_lux id: tsl2591_cl i2c_id: i2c_bus + - platform: tee501 + name: Office Temperature 3 + address: 0x48 + i2c_id: i2c_bus - platform: ultrasonic trigger_pin: GPIO25 echo_pin: number: GPIO23 inverted: true - name: "Ultrasonic Sensor" + name: Ultrasonic Sensor timeout: 5.5m id: ultrasonic_sensor1 - platform: uptime name: Uptime Sensor + - id: !extend ${devicename}_uptime_pcg + unit_of_measurement: s - platform: wifi_signal - name: "WiFi Signal Sensor" + name: WiFi Signal Sensor update_interval: 15s - platform: mqtt_subscribe - name: "MQTT Subscribe Sensor 1" - topic: "mqtt/topic" + name: MQTT Subscribe Sensor 1 + topic: mqtt/topic id: the_sensor qos: 2 on_value: @@ -1053,11 +1131,11 @@ sensor: root["key"] = id(the_sensor).state; root["greeting"] = "Hello World"; - platform: sds011 - uart_id: uart0 + uart_id: uart_0 pm_2_5: - name: "SDS011 PM2.5" + name: SDS011 PM2.5 pm_10_0: - name: "SDS011 PM10.0" + name: SDS011 PM10.0 update_interval: 5min rx_only: false - platform: ccs811 @@ -1070,9 +1148,9 @@ sensor: i2c_id: i2c_bus - platform: tx20 wind_speed: - name: "Windspeed" + name: Windspeed wind_direction_degrees: - name: "Winddirection Degrees" + name: Winddirection Degrees pin: number: GPIO04 mode: INPUT @@ -1080,48 +1158,48 @@ sensor: clock_pin: GPIO5 data_pin: GPIO4 co2: - name: "ZyAura CO2" + name: ZyAura CO2 temperature: - name: "ZyAura Temperature" + name: ZyAura Temperature humidity: - name: "ZyAura Humidity" + name: ZyAura Humidity - platform: as3935 lightning_energy: - name: "Lightning Energy" + name: Lightning Energy distance: - name: "Distance Storm" + name: Distance Storm - platform: tmp117 - name: "TMP117 Temperature" + name: TMP117 Temperature update_interval: 5s i2c_id: i2c_bus - platform: hm3301 pm_1_0: - name: "PM1.0" + name: PM1.0 pm_2_5: - name: "PM2.5" + name: PM2.5 pm_10_0: - name: "PM10.0" + name: PM10.0 aqi: - name: "AQI" - calculation_type: "CAQI" + name: AQI + calculation_type: CAQI i2c_id: i2c_bus - platform: teleinfo - tag_name: "HCHC" - name: "hchc" - unit_of_measurement: "Wh" + tag_name: HCHC + name: hchc + unit_of_measurement: Wh icon: mdi:flash teleinfo_id: myteleinfo - platform: mcp9808 - name: "MCP9808 Temperature" + name: MCP9808 Temperature update_interval: 15s i2c_id: i2c_bus - platform: ezo id: ph_ezo address: 99 - unit_of_measurement: "pH" + unit_of_measurement: pH i2c_id: i2c_bus - platform: sdp3x - name: "HVAC Filter Pressure drop" + name: HVAC Filter Pressure drop id: filter_pressure update_interval: 5s accuracy_decimals: 3 @@ -1129,11 +1207,11 @@ sensor: - platform: cs5460a id: cs5460a1 current: - name: "Socket current" + name: Socket current voltage: - name: "Mains voltage" + name: Mains voltage power: - name: "Socket power" + name: Socket power on_value: then: cs5460a.restart: cs5460a1 @@ -1141,8 +1219,8 @@ sensor: pga_gain: 10X current_gain: 0.01 voltage_gain: 0.000573 - current_hpf: on - voltage_hpf: on + current_hpf: true + voltage_hpf: true phase_offset: 20 pulse_energy: 0.01 kWh cs_pin: @@ -1151,7 +1229,7 @@ sensor: - platform: max9611 i2c_id: i2c_bus shunt_resistance: 0.2 ohm - gain: "1X" + gain: 1X voltage: name: Max9611 Voltage current: @@ -1161,9 +1239,46 @@ sensor: temperature: name: Max9611 Temp update_interval: 1s + - platform: mlx90614 + i2c_id: i2c_bus + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 + - platform: mpl3115a2 + i2c_id: i2c_bus + temperature: + name: "MPL3115A2 Temperature" + pressure: + name: "MPL3115A2 Pressure" + update_interval: 10s + - platform: ld2410 + moving_distance: + name: "Moving distance (cm)" + still_distance: + name: "Still Distance (cm)" + moving_energy: + name: "Move Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Distance Detection" + - platform: sen21231 + name: "Person Sensor" + i2c_id: i2c_bus + - platform: fs3000 + name: "Air Velocity" + model: 1005 + update_interval: 60s + i2c_id: i2c_bus + - platform: absolute_humidity + name: DHT Absolute Humidity + temperature: dht_temperature + humidity: dht_humidity esp32_touch: - setup_mode: False + setup_mode: false iir_filter: 10ms sleep_duration: 27ms measurement_duration: 8ms @@ -1180,7 +1295,7 @@ binary_sensor: number: 1 # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP - inverted: False + inverted: false - platform: gpio name: "MCP23S17 Pin #1" pin: @@ -1189,7 +1304,7 @@ binary_sensor: number: 1 # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP - inverted: False + inverted: false - platform: gpio name: "MCP23S17 Pin #1 with interrupt" pin: @@ -1198,11 +1313,11 @@ binary_sensor: number: 1 # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP - inverted: False + inverted: false interrupt: FALLING - platform: gpio pin: GPIO9 - name: "Living Room Window" + name: Living Room Window device_class: window filters: - invert: @@ -1242,7 +1357,7 @@ binary_sensor: - OFF for at least 0.2s then: - logger.log: - format: "Multi Clicked TWO" + format: Multi Clicked TWO level: warn - timing: - OFF for 1s to 2s @@ -1250,30 +1365,45 @@ binary_sensor: - OFF for at least 0.5s then: - logger.log: - format: "Multi Clicked LONG SINGLE" + format: Multi Clicked LONG SINGLE level: warn - timing: - ON for at most 1s - OFF for at least 0.5s then: - logger.log: - format: "Multi Clicked SINGLE" + format: Multi Clicked SINGLE level: warn id: binary_sensor1 - platform: gpio pin: number: GPIO9 mode: INPUT_PULLUP - name: "Living Room Window 2" + name: Living Room Window 2 + - platform: gpio + pin: + number: GPIO9 + mode: INPUT_OUTPUT_OPEN_DRAIN + name: Living Room Button - platform: status - name: "Living Room Status" + name: Living Room Status - platform: esp32_touch - name: "ESP32 Touch Pad GPIO27" + name: ESP32 Touch Pad GPIO27 pin: GPIO27 threshold: 1000 id: btn_left + on_press: + - if: + condition: + display_menu.is_active: + then: + - display_menu.enter: + else: + - display_menu.left: + - display_menu.right: + - display_menu.show: - platform: template - name: "Garage Door Open" + name: Garage Door Open id: garage_door lambda: |- if (isnan(id(${sensorname}_sensor).state)) { @@ -1291,7 +1421,7 @@ binary_sensor: on_press: - binary_sensor.template.publish: id: garage_door - state: OFF + state: false - output.ledc.set_frequency: id: gpio_19 frequency: 500.0Hz @@ -1301,41 +1431,48 @@ binary_sensor: - platform: pn532 pn532_id: pn532_bs uid: 74-10-37-94 - name: "PN532 NFC Tag" + name: PN532 NFC Tag - platform: rdm6300 uid: 7616525 - name: "RDM6300 NFC Tag" + name: RDM6300 NFC Tag - platform: gpio - name: "PCF binary sensor" + name: PCF binary sensor pin: pcf8574: pcf8574_hub number: 1 mode: INPUT - inverted: True + inverted: true - platform: gpio - name: "MCP21 binary sensor" + name: PCA9554 binary sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + name: MCP21 binary sensor pin: mcp23xxx: mcp23017_hub number: 1 mode: INPUT - inverted: True + inverted: true - platform: gpio - name: "MCP22 binary sensor" + name: MCP22 binary sensor pin: mcp23xxx: mcp23008_hub number: 7 mode: INPUT_PULLUP - inverted: False + inverted: false - platform: gpio - name: "MCP23 binary sensor" + name: MCP23 binary sensor pin: mcp23016: mcp23016_hub number: 7 mode: INPUT - inverted: False + inverted: false - platform: remote_receiver - name: "Raw Remote Receiver Test" + name: Raw Remote Receiver Test raw: code: [ @@ -1376,7 +1513,7 @@ binary_sensor: 1709, ] - platform: as3935 - name: "Storm Alert" + name: Storm Alert - platform: analog_threshold name: Analog Trheshold 1 sensor_id: template_sensor @@ -1405,6 +1542,13 @@ binary_sensor: id: close_sensor - platform: template id: close_obstacle_sensor + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still pca9685: frequency: 500 @@ -1429,12 +1573,34 @@ my9231: num_chips: 2 bit_depth: 16 +sm2235: + data_pin: GPIO4 + clock_pin: GPIO5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +sm2335: + data_pin: GPIO4 + clock_pin: GPIO5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +bp1658cj: + data_pin: GPIO3 + clock_pin: GPIO5 + max_power_color_channels: 4 + max_power_white_channels: 6 + +bp5758d: + data_pin: GPIO3 + clock_pin: GPIO5 + output: - platform: gpio pin: GPIO26 id: gpio_26 power_supply: atx_power_supply - inverted: False + inverted: false - platform: ledc pin: 19 id: gpio_19 @@ -1468,67 +1634,74 @@ output: - platform: tlc59208f id: tlc_0 channel: 0 - tlc59208f_id: "tlc59208f_1" + tlc59208f_id: tlc59208f_1 - platform: tlc59208f id: tlc_1 channel: 1 - tlc59208f_id: "tlc59208f_1" + tlc59208f_id: tlc59208f_1 - platform: tlc59208f id: tlc_2 channel: 2 - tlc59208f_id: "tlc59208f_1" + tlc59208f_id: tlc59208f_1 - platform: tlc59208f id: tlc_3 channel: 0 - tlc59208f_id: "tlc59208f_2" + tlc59208f_id: tlc59208f_2 - platform: tlc59208f id: tlc_4 channel: 1 - tlc59208f_id: "tlc59208f_2" + tlc59208f_id: tlc59208f_2 - platform: tlc59208f id: tlc_5 channel: 2 - tlc59208f_id: "tlc59208f_2" + tlc59208f_id: tlc59208f_2 - platform: tlc59208f id: tlc_6 channel: 0 - tlc59208f_id: "tlc59208f_3" + tlc59208f_id: tlc59208f_3 - platform: tlc59208f id: tlc_7 channel: 1 - tlc59208f_id: "tlc59208f_3" + tlc59208f_id: tlc59208f_3 - platform: tlc59208f id: tlc_8 channel: 2 - tlc59208f_id: "tlc59208f_3" + tlc59208f_id: tlc59208f_3 - platform: gpio id: id2 pin: pcf8574: pcf8574_hub number: 0 mode: OUTPUT - inverted: False + inverted: false + - platform: gpio + id: id26 + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false - platform: gpio id: id22 pin: mcp23xxx: mcp23017_hub number: 0 mode: OUTPUT - inverted: False + inverted: false - platform: gpio id: id23 pin: mcp23xxx: mcp23008_hub number: 0 mode: OUTPUT - inverted: False + inverted: false - platform: gpio id: id25 pin: mcp23016: mcp23016_hub number: 0 mode: OUTPUT - inverted: False + inverted: false - platform: my9231 id: my_0 channel: 0 @@ -1547,6 +1720,36 @@ output: - platform: my9231 id: my_5 channel: 5 + - platform: sm2235 + id: sm2235_red + channel: 1 + - platform: sm2235 + id: sm2235_green + channel: 0 + - platform: sm2235 + id: sm2235_blue + channel: 2 + - platform: sm2235 + id: sm2235_coldwhite + channel: 4 + - platform: sm2235 + id: sm2235_warmwhite + channel: 3 + - platform: sm2335 + id: sm2335_red + channel: 1 + - platform: sm2335 + id: sm2335_green + channel: 0 + - platform: sm2335 + id: sm2335_blue + channel: 2 + - platform: sm2335 + id: sm2335_coldwhite + channel: 4 + - platform: sm2335 + id: sm2335_warmwhite + channel: 3 - platform: slow_pwm id: id24 pin: GPIO26 @@ -1583,32 +1786,73 @@ output: vref: internal gain: X2 power_down: gnd_500k + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 + - platform: x9c + id: test_x9c + cs_pin: GPIO25 + inc_pin: GPIO26 + ud_pin: GPIO27 + initial_value: 0.5 e131: light: - platform: binary - name: "Desk Lamp" + name: Desk Lamp output: gpio_26 effects: - strobe: - strobe: - name: "My Strobe" + name: My Strobe colors: - - state: True + - state: true duration: 250ms - - state: False + - state: false duration: 250ms on_turn_on: - switch.template.publish: id: livingroom_lights - state: yes + state: true on_turn_off: - switch.template.publish: id: livingroom_lights - state: yes + state: true - platform: monochromatic - name: "Kitchen Lights" + name: Kitchen Lights id: kitchen output: gpio_19 gamma_correct: 2.8 @@ -1617,7 +1861,7 @@ light: - strobe: - flicker: - flicker: - name: "My Flicker" + name: My Flicker alpha: 98% intensity: 1.5% - lambda: @@ -1629,20 +1873,20 @@ light: if (state == 4) state = 0; - platform: rgb - name: "Living Room Lights" + name: Living Room Lights id: ${roomname}_lights red: pca_0 green: pca_1 blue: pca_2 - platform: rgbw - name: "Living Room Lights 2" + name: Living Room Lights 2 red: pca_3 green: pca_4 blue: pca_5 white: pca_6 color_interlock: true - platform: rgbww - name: "Living Room Lights 2" + name: Living Room Lights 2 red: pca_3 green: pca_4 blue: pca_5 @@ -1652,7 +1896,7 @@ light: warm_white_color_temperature: 500 mireds color_interlock: true - platform: rgbct - name: "Living Room Lights 2" + name: Living Room Lights 2 red: pca_3 green: pca_4 blue: pca_5 @@ -1662,14 +1906,14 @@ light: warm_white_color_temperature: 500 mireds color_interlock: true - platform: cwww - name: "Living Room Lights 2" + name: Living Room Lights 2 cold_white: pca_6 warm_white: pca_6 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds constant_brightness: true - platform: color_temperature - name: "Living Room Lights 2" + name: Living Room Lights 2 color_temperature: pca_6 brightness: pca_6 cold_white_color_temperature: 153 mireds @@ -1683,7 +1927,7 @@ light: max_refresh_rate: 20ms power_supply: atx_power_supply color_correct: [75%, 100%, 50%] - name: "FastLED WS2811 Light" + name: FastLED WS2811 Light effects: - addressable_color_wipe: - addressable_color_wipe: @@ -1698,7 +1942,7 @@ light: blue: 0% num_leds: 1 add_led_interval: 100ms - reverse: False + reverse: false - addressable_scan: - addressable_scan: name: Scan Effect With Custom Values @@ -1726,7 +1970,7 @@ light: update_interval: 16ms intensity: 5% - addressable_lambda: - name: "Test For Custom Lambda Effect" + name: Test For Custom Lambda Effect lambda: |- if (initial_run) { it[0] = current_color; @@ -1762,10 +2006,10 @@ light: data_rate: 2MHz num_leds: 60 rgb_order: BRG - name: "FastLED SPI Light" + name: FastLED SPI Light - platform: neopixelbus id: addr3 - name: "Neopixelbus Light" + name: Neopixelbus Light gamma_correct: 2.8 color_correct: [0.0, 0.0, 0.0, 0.0] default_transition_length: 10s @@ -1781,7 +2025,7 @@ light: num_leds: 60 pin: GPIO23 - platform: partition - name: "Partition Light" + name: Partition Light segments: - id: addr1 from: 0 @@ -1794,18 +2038,6 @@ light: to: 25 - single_light_id: ${roomname}_lights - - platform: shelly_dimmer - name: "Shelly Dimmer Light" - power: - name: "Shelly Dimmer Power" - voltage: - name: "Shelly Dimmer Voltage" - current: - name: "Shelly Dimmer Current" - max_brightness: 500 - firmware: "51.6" - uart_id: uart0 - remote_transmitter: - pin: 32 carrier_duty_percent: 100% @@ -1813,8 +2045,8 @@ remote_transmitter: climate: - platform: tcl112 name: TCL112 Climate With Sensor - supports_heat: True - supports_cool: True + supports_heat: true + supports_cool: true sensor: ${sensorname}_sensor - platform: tcl112 name: TCL112 Climate @@ -1836,8 +2068,8 @@ climate: target_temperature_state_topic: target/temperature/state/topic - platform: coolix name: Coolix Climate With Sensor - supports_heat: True - supports_cool: True + supports_heat: true + supports_cool: true sensor: ${sensorname}_sensor - platform: coolix name: Coolix Climate @@ -1845,6 +2077,9 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: daikin_brc + name: Daikin BRC Climate + use_fahrenheit: true - platform: delonghi name: Delonghi Climate - platform: yashima @@ -1870,10 +2105,12 @@ climate: name: Midea IR use_fahrenheit: true - platform: midea + on_control: + logger.log: Control message received! on_state: - logger.log: "State changed!" + logger.log: State changed! id: midea_unit - uart_id: uart0 + uart_id: uart_0 name: Midea Climate transmitter_id: period: 1s @@ -1905,11 +2142,11 @@ climate: - HORIZONTAL - BOTH outdoor_temperature: - name: "Temp" + name: Temp power_usage: - name: "Power" + name: Power humidity_setpoint: - name: "Humidity" + name: Humidity - platform: anova name: Anova cooker ble_client_id: ble_blah @@ -1955,7 +2192,7 @@ switch: # Use pin number 0 number: 0 mode: OUTPUT - inverted: False + inverted: false - platform: gpio name: "MCP23S17 Pin #0" pin: @@ -1963,12 +2200,12 @@ switch: # Use pin number 0 number: 1 mode: OUTPUT - inverted: False + inverted: false - platform: gpio pin: GPIO25 - name: "Living Room Dehumidifier" + name: Living Room Dehumidifier icon: "mdi:restart" - inverted: True + inverted: true command_topic: custom_command_topic command_retain: true restore_mode: ALWAYS_OFF @@ -2045,20 +2282,20 @@ switch: remote_transmitter.transmit_rc_switch_type_a: group: "11001" device: "01000" - state: True + state: true protocol: pulse_length: 175 sync: [1, 31] zero: [1, 3] one: [3, 1] - inverted: False + inverted: false - platform: template name: RC Switch Type B turn_on_action: remote_transmitter.transmit_rc_switch_type_b: address: 4 channel: 2 - state: True + state: true - platform: template name: RC Switch Type C turn_on_action: @@ -2066,14 +2303,14 @@ switch: family: "a" group: 1 device: 2 - state: True + state: true - platform: template name: RC Switch Type D turn_on_action: remote_transmitter.transmit_rc_switch_type_d: group: "a" device: 2 - state: True + state: true - platform: template name: RC5 turn_on_action: @@ -2091,12 +2328,49 @@ switch: turn_on_action: remote_transmitter.transmit_aeha: address: 0x8008 - data: [0x00, 0x02, 0xFD, 0xFF, 0x00, 0x33, 0xCC, 0x49, 0xB6, 0xC8, 0x37, 0x16, 0xE9, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xCA, 0x35, 0x8F, 0x70, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF] + data: + [ + 0x00, + 0x02, + 0xFD, + 0xFF, + 0x00, + 0x33, + 0xCC, + 0x49, + 0xB6, + 0xC8, + 0x37, + 0x16, + 0xE9, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0xCA, + 0x35, + 0x8F, + 0x70, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + 0x00, + 0xFF, + ] - platform: template name: Living Room Lights id: livingroom_lights - optimistic: True - assumed_state: yes + optimistic: true + assumed_state: true turn_on_action: - switch.turn_on: living_room_lights_on - output.set_level: @@ -2119,22 +2393,24 @@ switch: level: !lambda "return 0.5;" turn_off_action: - switch.turn_on: living_room_lights_off - restore_state: False + restore_state: false on_turn_on: - switch.template.publish: id: livingroom_lights - state: yes + state: true - platform: restart - name: "Living Room Restart" + name: Living Room Restart - platform: safe_mode - name: "Living Room Restart (Safe Mode)" + name: Living Room Restart (Safe Mode) + - platform: factory_reset + name: Living Room Restart (Factory Default Settings) - platform: shutdown - name: "Living Room Shutdown" + name: Living Room Shutdown - platform: output - name: "Generic Output" + name: Generic Output output: pca_6 - platform: template - name: "Template Switch" + name: Template Switch id: my_switch lambda: |- if (id(binary_sensor1).state) { @@ -2152,27 +2428,27 @@ switch: // Switch is OFF, do something else here } optimistic: true - assumed_state: no - restore_state: True + assumed_state: false + restore_state: true on_turn_off: - switch.template.publish: id: my_switch state: !lambda "return false;" - platform: uart - uart_id: uart0 - name: "UART String Output" - data: "DataToSend" + uart_id: uart_0 + name: UART String Output + data: DataToSend - platform: uart - uart_id: uart0 - name: "UART Bytes Output" + uart_id: uart_0 + name: UART Bytes Output data: [0xDE, 0xAD, 0xBE, 0xEF] - platform: uart - uart_id: uart0 - name: "UART Recurring Output" + uart_id: uart_0 + name: UART Recurring Output data: [0xDE, 0xAD, 0xBE, 0xEF] send_every: 1s - platform: template - assumed_state: yes + assumed_state: true name: Stepper Switch turn_on_action: - stepper.set_target: @@ -2194,7 +2470,7 @@ switch: sn74hc595: sn74hc595_hub # Use pin number 0 number: 0 - inverted: False + inverted: false - platform: template id: ble1_status optimistic: true @@ -2206,7 +2482,7 @@ switch: fan: - platform: binary output: gpio_26 - name: "Living Room Fan 1" + name: Living Room Fan 1 oscillation_output: gpio_19 direction_output: gpio_26 - platform: speed @@ -2214,7 +2490,7 @@ fan: icon: mdi:weather-windy output: pca_6 speed_count: 10 - name: "Living Room Fan 2" + name: Living Room Fan 2 oscillation_output: gpio_19 direction_output: gpio_26 oscillation_state_topic: oscillation/state/topic @@ -2225,13 +2501,13 @@ fan: speed_command_topic: speed/command/topic on_speed_set: then: - - logger.log: "Fan speed was changed!" + - logger.log: Fan speed was changed! - platform: bedjet name: My Bedjet fan bedjet_id: my_bedjet_client - platform: copy source_id: fan_speed - name: "Fan Speed Copy" + name: Fan Speed Copy interval: - interval: 10s @@ -2242,6 +2518,7 @@ interval: - display.page.show_previous: display1 - interval: 2s then: + # yamllint disable rule:line-length - lambda: |- static uint16_t btn_left_state = id(btn_left)->get_value(); @@ -2250,13 +2527,14 @@ interval: 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); + # yamllint enable rule:line-length - if: condition: display.is_displaying_page: id: display1 page_id: page1 then: - - logger.log: "Seeing page 1" + - logger.log: Seeing page 1 color: - id: kbx_red @@ -2267,9 +2545,12 @@ color: red: 0% green: 1% blue: 100% + - id: kbx_green + hex: "3DEC55" display: - platform: lcd_gpio + id: my_lcd_gpio dimensions: 18x4 data_pins: - GPIO19 @@ -2328,7 +2609,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1306_i2c - model: "SSD1306_128X64" + model: SSD1306_128X64 reset_pin: GPIO23 address: 0x3C id: display1 @@ -2349,28 +2630,28 @@ display: ESP_LOGD("display", "1 -> 2"); i2c_id: i2c_bus - platform: ssd1306_spi - model: "SSD1306 128x64" + model: SSD1306 128x64 cs_pin: GPIO23 dc_pin: GPIO23 reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1322_spi - model: "SSD1322 256x64" + model: SSD1322 256x64 cs_pin: GPIO23 dc_pin: GPIO23 reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1325_spi - model: "SSD1325 128x64" + model: SSD1325 128x64 cs_pin: GPIO23 dc_pin: GPIO23 reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1327_i2c - model: "SSD1327 128X128" + model: SSD1327 128X128 reset_pin: GPIO23 address: 0x3D id: display1327 @@ -2384,7 +2665,7 @@ display: // Nothing i2c_id: i2c_bus - platform: ssd1327_spi - model: "SSD1327 128x128" + model: SSD1327 128x128 cs_pin: GPIO23 dc_pin: GPIO23 reset_pin: GPIO23 @@ -2397,7 +2678,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1351_spi - model: "SSD1351 128x128" + model: SSD1351 128x128 cs_pin: GPIO23 dc_pin: GPIO23 reset_pin: GPIO23 @@ -2420,7 +2701,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 - model: "INITR_BLACKTAB" + model: INITR_BLACKTAB cs_pin: GPIO5 dc_pin: GPIO16 reset_pin: GPIO23 @@ -2431,24 +2712,18 @@ display: row_start: 0 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9341 - model: "TFT 2.4" + - platform: ili9xxx + model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 reset_pin: GPIO22 - led_pin: - number: GPIO15 - inverted: true lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9341 - model: "TFT 2.4" + - platform: ili9xxx + model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 reset_pin: GPIO22 - led_pin: - number: GPIO15 - inverted: true auto_clear_enabled: false rotation: 90 lambda: |- @@ -2471,6 +2746,17 @@ display: it.print_sad(true); it.print_bracket(true); it.print_battery(true); + - platform: tm1621 + id: tm1621_display + cs_pin: GPIO17 + data_pin: GPIO5 + read_pin: GPIO23 + write_pin: GPIO18 + lambda: |- + it.printf(0, "%.1f", id(dht_temperature).state); + it.display_celsius(true); + it.printf(1, "%.1f", id(dht_humidity).state); + it.display_humidity(true); tm1651: id: tm1651_battery @@ -2505,7 +2791,7 @@ pn532_i2c: i2c_id: i2c_bus rdm6300: - uart_id: uart0 + uart_id: uart_0 rc522_spi: cs_pin: GPIO23 @@ -2529,12 +2815,12 @@ rc522_i2c: mcp4728: - id: mcp4728_dac - store_in_eeprom: False + store_in_eeprom: false address: 0x60 i2c_id: i2c_bus gps: - uart_id: uart0 + uart_id: uart_0 time: - platform: sntp @@ -2562,7 +2848,7 @@ time: cover: - platform: template - name: "Template Cover" + name: Template Cover id: template_cover lambda: |- if (id(binary_sensor1).state) { @@ -2575,8 +2861,8 @@ cover: - cover.template.publish: id: template_cover state: CLOSED - assumed_state: no - has_position: yes + assumed_state: false + has_position: true position_state_topic: position/state/topic position_command_topic: position/command/topic tilt_lambda: !lambda "return 0.5;" @@ -2589,12 +2875,12 @@ cover: then: - lambda: 'ESP_LOGD("cover", "closed");' - platform: am43 - name: "Test AM43" + name: Test AM43 id: am43_test ble_client_id: ble_foo icon: mdi:blinds - platform: feedback - name: "Feedback Cover" + name: Feedback Cover id: gate device_class: gate @@ -2638,24 +2924,29 @@ tca9548a: i2c_id: multiplex0_chan0 pcf8574: - - id: "pcf8574_hub" + - id: pcf8574_hub address: 0x21 - pcf8575: False + pcf8575: false + i2c_id: i2c_bus + +pca9554: + - id: pca9554_hub + address: 0x3F i2c_id: i2c_bus mcp23017: - - id: "mcp23017_hub" - open_drain_interrupt: "true" + - id: mcp23017_hub + open_drain_interrupt: true i2c_id: i2c_bus mcp23008: - - id: "mcp23008_hub" + - id: mcp23008_hub address: 0x22 - open_drain_interrupt: "true" + open_drain_interrupt: true i2c_id: i2c_bus mcp23016: - - id: "mcp23016_hub" + - id: mcp23016_hub address: 0x23 i2c_id: i2c_bus @@ -2672,29 +2963,29 @@ stepper: globals: - id: glob_int type: int - restore_value: yes + restore_value: true initial_value: "0" - id: glob_float type: float - restore_value: yes + restore_value: true initial_value: "0.0f" - id: glob_bool type: bool - restore_value: no + restore_value: false initial_value: "true" - id: glob_string type: std::string - restore_value: no + restore_value: false # initial_value: "" - id: glob_bool_processed type: bool - restore_value: no + restore_value: false initial_value: "false" text_sensor: - platform: ble_client ble_client_id: ble_foo - name: "Sensor Location" + name: Sensor Location service_uuid: "180d" characteristic_uuid: "2a38" descriptor_uuid: "2902" @@ -2705,7 +2996,7 @@ text_sensor: - lambda: |- ESP_LOGD("green_btn", "Location changed: %s", x.c_str()); - platform: mqtt_subscribe - name: "MQTT Subscribe Text" + name: MQTT Subscribe Text topic: "the/topic" qos: 2 on_value: @@ -2742,25 +3033,25 @@ text_sensor: id: ${textname}_text - platform: wifi_info scan_results: - name: "Scan Results" + name: Scan Results ip_address: - name: "IP Address" + name: IP Address ssid: - name: "SSID" + name: SSID bssid: - name: "BSSID" + name: BSSID mac_address: - name: "Mac Address" + name: Mac Address - platform: version - name: "ESPHome Version No Timestamp" - hide_timestamp: True + name: ESPHome Version No Timestamp + hide_timestamp: true - platform: teleinfo - tag_name: "OPTARIF" - name: "optarif" + tag_name: OPTARIF + name: optarif teleinfo_id: myteleinfo sn74hc595: - - id: "sn74hc595_hub" + - id: sn74hc595_hub data_pin: GPIO21 clock_pin: GPIO23 latch_pin: GPIO22 @@ -2849,7 +3140,7 @@ canbus: teleinfo: id: myteleinfo - uart_id: uart0 + uart_id: uart_0 update_interval: 60s historical_mode: true @@ -2883,7 +3174,7 @@ qr_code: lock: - platform: template id: test_lock1 - name: "Template Switch" + name: Template Switch lambda: |- if (id(binary_sensor1).state) { return LOCK_STATE_LOCKED; @@ -2891,7 +3182,7 @@ lock: return LOCK_STATE_UNLOCKED; } optimistic: true - assumed_state: no + assumed_state: false on_unlock: - lock.template.publish: id: test_lock1 @@ -2901,7 +3192,7 @@ lock: id: test_lock1 state: !lambda "return LOCK_STATE_LOCKED;" - platform: output - name: "Generic Output Lock" + name: Generic Output Lock id: test_lock2 output: pca_6 - platform: copy @@ -2910,7 +3201,7 @@ lock: button: - platform: template - name: "Start calibration" + name: Start calibration on_press: - scd4x.perform_forced_calibration: value: 419 @@ -2937,3 +3228,98 @@ button: name: Midea Power Inverse on_press: midea_ac.power_toggle: + +ld2410: + id: my_ld2410 + uart_id: ld2410_uart + timeout: 150s + max_move_distance: 6m + max_still_distance: 0.75m + g0_move_threshold: 10 + g0_still_threshold: 20 + g2_move_threshold: 20 + g2_still_threshold: 21 + g8_move_threshold: 80 + g8_still_threshold: 81 + +lcd_menu: + display_id: my_lcd_gpio + mark_back: 0x5e + mark_selected: 0x3e + mark_editing: 0x2a + mark_submenu: 0x7e + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "root leave");' + items: + - type: back + text: Back + - type: label + - type: menu + text: Submenu 1 + items: + - type: back + text: Back + - type: menu + text: Submenu 21 + items: + - type: back + text: Back + - type: command + text: Show Main + on_value: + then: + - display_menu.show_main: + - type: select + text: Enum Item + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: Number + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: Hide + on_value: + then: + - display_menu.hide: + - type: switch + text: Switch + switch: my_switch + on_text: Bright + off_text: Dark + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' diff --git a/tests/test2.yaml b/tests/test2.yaml index 3dd8f824dd..51ec69ef34 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -1,3 +1,4 @@ +--- esphome: name: $devicename platform: ESP32 @@ -28,7 +29,7 @@ api: i2c: sda: 21 scl: 22 - scan: False + scan: false spi: clk_pin: GPIO21 @@ -47,7 +48,7 @@ uart: - lambda: UARTDebug::log_hex(direction, bytes, ':'); ota: - safe_mode: True + safe_mode: true port: 3286 num_attempts: 15 @@ -67,7 +68,7 @@ as3935_i2c: irq_pin: GPIO12 mcp3008: - - id: 'mcp3008_hub' + - id: mcp3008_hub cs_pin: GPIO12 output: @@ -86,35 +87,35 @@ sensor: id: ha_hello_world_temperature - platform: ble_rssi mac_address: AC:37:43:77:5F:4C - name: 'BLE Google Home Mini RSSI value' + name: BLE Google Home Mini RSSI value - platform: ble_rssi - service_uuid: '11aa' - name: 'BLE Test Service 16' + service_uuid: 11aa + name: BLE Test Service 16 - platform: ble_rssi - service_uuid: '11223344' - name: 'BLE Test Service 32' + service_uuid: "11223344" + name: BLE Test Service 32 - platform: ble_rssi - service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: 'BLE Test Service 128' + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 - platform: ble_rssi - service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: 'BLE Test iBeacon UUID' + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test iBeacon UUID - platform: b_parasite mac_address: F0:CA:F0:CA:01:01 humidity: - name: 'b-parasite Air Humidity' + name: b-parasite Air Humidity temperature: - name: 'b-parasite Air Temperature' + name: b-parasite Air Temperature moisture: - name: 'b-parasite Soil Moisture' + name: b-parasite Soil Moisture battery_voltage: - name: 'b-parasite Battery Voltage' + name: b-parasite Battery Voltage illuminance: - name: 'b-parasite Illuminance' + name: b-parasite Illuminance - platform: senseair id: senseair0 co2: - name: 'SenseAir CO2 Value' + name: SenseAir CO2 Value on_value: then: - senseair.background_calibration: senseair0 @@ -126,167 +127,167 @@ sensor: - platform: ruuvitag mac_address: FF:56:D3:2F:7D:E8 humidity: - name: 'RuuviTag Humidity' + name: RuuviTag Humidity temperature: - name: 'RuuviTag Temperature' + name: RuuviTag Temperature pressure: - name: 'RuuviTag Pressure' + name: RuuviTag Pressure acceleration_x: - name: 'RuuviTag Acceleration X' + name: RuuviTag Acceleration X acceleration_y: - name: 'RuuviTag Acceleration Y' + name: RuuviTag Acceleration Y acceleration_z: - name: 'RuuviTag Acceleration Z' + name: RuuviTag Acceleration Z battery_voltage: - name: 'RuuviTag Battery Voltage' + name: RuuviTag Battery Voltage tx_power: - name: 'RuuviTag TX Power' + name: RuuviTag TX Power movement_counter: - name: 'RuuviTag Movement Counter' + name: RuuviTag Movement Counter measurement_sequence_number: - name: 'RuuviTag Measurement Sequence Number' + name: RuuviTag Measurement Sequence Number - platform: as3935 lightning_energy: - name: 'Lightning Energy' + name: Lightning Energy distance: - name: 'Distance Storm' + name: Distance Storm - platform: xiaomi_hhccjcy01 mac_address: 94:2B:FF:5C:91:61 temperature: - name: 'Xiaomi HHCCJCY01 Temperature' + name: Xiaomi HHCCJCY01 Temperature moisture: - name: 'Xiaomi HHCCJCY01 Moisture' + name: Xiaomi HHCCJCY01 Moisture illuminance: - name: 'Xiaomi HHCCJCY01 Illuminance' + name: Xiaomi HHCCJCY01 Illuminance conductivity: - name: 'Xiaomi HHCCJCY01 Soil Conductivity' + name: Xiaomi HHCCJCY01 Soil Conductivity battery_level: - name: 'Xiaomi HHCCJCY01 Battery Level' + name: Xiaomi HHCCJCY01 Battery Level - platform: xiaomi_lywsdcgq mac_address: 7A:80:8E:19:36:BA temperature: - name: 'Xiaomi LYWSDCGQ Temperature' + name: Xiaomi LYWSDCGQ Temperature humidity: - name: 'Xiaomi LYWSDCGQ Humidity' + name: Xiaomi LYWSDCGQ Humidity battery_level: - name: 'Xiaomi LYWSDCGQ Battery Level' + name: Xiaomi LYWSDCGQ Battery Level - platform: xiaomi_lywsd02 mac_address: 3F:5B:7D:82:58:4E temperature: - name: 'Xiaomi LYWSD02 Temperature' + name: Xiaomi LYWSD02 Temperature humidity: - name: 'Xiaomi LYWSD02 Humidity' + name: Xiaomi LYWSD02 Humidity battery_level: - name: 'Xiaomi LYWSD02 Battery Level' + name: Xiaomi LYWSD02 Battery Level - platform: xiaomi_cgg1 mac_address: 7A:80:8E:19:36:BA temperature: - name: 'Xiaomi CGG1 Temperature' + name: Xiaomi CGG1 Temperature humidity: - name: 'Xiaomi CGG1 Humidity' + name: Xiaomi CGG1 Humidity battery_level: - name: 'Xiaomi CGG1 Battery Level' + name: Xiaomi CGG1 Battery Level - platform: xiaomi_gcls002 - mac_address: '94:2B:FF:5C:91:61' + mac_address: 94:2B:FF:5C:91:61 temperature: - name: 'GCLS02 Temperature' + name: GCLS02 Temperature moisture: - name: 'GCLS02 Moisture' + name: GCLS02 Moisture conductivity: - name: 'GCLS02 Soil Conductivity' + name: GCLS02 Soil Conductivity illuminance: - name: 'GCLS02 Illuminance' + name: GCLS02 Illuminance - platform: xiaomi_hhccpot002 - mac_address: '94:2B:FF:5C:91:61' + mac_address: 94:2B:FF:5C:91:61 moisture: - name: 'HHCCPOT002 Moisture' + name: HHCCPOT002 Moisture conductivity: - name: 'HHCCPOT002 Soil Conductivity' + name: HHCCPOT002 Soil Conductivity - platform: xiaomi_lywsd03mmc - mac_address: 'A4:C1:38:4E:16:78' - bindkey: 'e9efaa6873f9f9c87a5e75a5f814801c' + mac_address: A4:C1:38:4E:16:78 + bindkey: e9efaa6873f9f9c87a5e75a5f814801c temperature: - name: 'Xiaomi LYWSD03MMC Temperature' + name: Xiaomi LYWSD03MMC Temperature humidity: - name: 'Xiaomi LYWSD03MMC Humidity' + name: Xiaomi LYWSD03MMC Humidity battery_level: - name: 'Xiaomi LYWSD03MMC Battery Level' + name: Xiaomi LYWSD03MMC Battery Level - platform: xiaomi_cgd1 - mac_address: 'A4:C1:38:D1:61:7D' - bindkey: 'c99d2313182473b38001086febf781bd' + mac_address: A4:C1:38:D1:61:7D + bindkey: c99d2313182473b38001086febf781bd temperature: - name: 'Xiaomi CGD1 Temperature' + name: Xiaomi CGD1 Temperature humidity: - name: 'Xiaomi CGD1 Humidity' + name: Xiaomi CGD1 Humidity battery_level: - name: 'Xiaomi CGD1 Battery Level' + name: Xiaomi CGD1 Battery Level - platform: xiaomi_jqjcy01ym - mac_address: '7A:80:8E:19:36:BA' + mac_address: 7A:80:8E:19:36:BA temperature: - name: 'JQJCY01YM Temperature' + name: JQJCY01YM Temperature humidity: - name: 'JQJCY01YM Humidity' + name: JQJCY01YM Humidity formaldehyde: - name: 'JQJCY01YM Formaldehyde' + name: JQJCY01YM Formaldehyde battery_level: - name: 'JQJCY01YM Battery Level' + name: JQJCY01YM Battery Level - platform: xiaomi_mhoc303 - mac_address: 'E7:50:59:32:A0:1C' + mac_address: E7:50:59:32:A0:1C temperature: - name: 'MHO-C303 Temperature' + name: MHO-C303 Temperature humidity: - name: 'MHO-C303 Humidity' + name: MHO-C303 Humidity battery_level: - name: 'MHO-C303 Battery Level' + name: MHO-C303 Battery Level - platform: atc_mithermometer - mac_address: 'A4:C1:38:4E:16:78' + mac_address: A4:C1:38:4E:16:78 temperature: - name: 'ATC Temperature' + name: ATC Temperature humidity: - name: 'ATC Humidity' + name: ATC Humidity battery_level: - name: 'ATC Battery-Level' + name: ATC Battery-Level battery_voltage: - name: 'ATC Battery-Voltage' + name: ATC Battery-Voltage - platform: pvvx_mithermometer - mac_address: 'A4:C1:38:4E:16:78' + mac_address: A4:C1:38:4E:16:78 temperature: - name: 'PVVX Temperature' + name: PVVX Temperature humidity: - name: 'PVVX Humidity' + name: PVVX Humidity battery_level: - name: 'PVVX Battery-Level' + name: PVVX Battery-Level battery_voltage: - name: 'PVVX Battery-Voltage' + name: PVVX Battery-Voltage - platform: inkbird_ibsth1_mini mac_address: 38:81:D7:0A:9C:11 temperature: - name: 'Inkbird IBS-TH1 Temperature' + name: Inkbird IBS-TH1 Temperature humidity: - name: 'Inkbird IBS-TH1 Humidity' + name: Inkbird IBS-TH1 Humidity battery_level: - name: 'Inkbird IBS-TH1 Battery Level' + name: Inkbird IBS-TH1 Battery Level - platform: xiaomi_rtcgq02lm id: motion_rtcgq02lm battery_level: - name: 'Mi Motion Sensor 2 Battery level' + name: Mi Motion Sensor 2 Battery level - platform: ltr390 uv: - name: "LTR390 UV" + name: LTR390 UV uv_index: - name: "LTR390 UVI" + name: LTR390 UVI light: - name: "LTR390 Light" + name: LTR390 Light ambient_light: - name: "LTR390 ALS" - gain: "X3" + name: LTR390 ALS + gain: X3 resolution: 18 window_correction_factor: 1.0 address: 0x53 update_interval: 60s - platform: sgp4x voc: - name: "VOC Index" + name: VOC Index id: sgp40_voc_index algorithm_tuning: index_offset: 100 @@ -296,7 +297,7 @@ sensor: std_initial: 50 gain_factor: 230 nox: - name: "NOx" + name: NOx algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 @@ -307,7 +308,7 @@ sensor: update_interval: 5s - platform: mcp3008 update_interval: 5s - mcp3008_id: 'mcp3008_hub' + mcp3008_id: mcp3008_hub id: freezer_temp_source reference_voltage: 3.19 number: 0 @@ -315,69 +316,95 @@ sensor: ble_client_id: airthings01 update_interval: 5min temperature: - name: "Wave Plus Temperature" + name: Wave Plus Temperature radon: - name: "Wave Plus Radon" + name: Wave Plus Radon radon_long_term: - name: "Wave Plus Radon Long Term" + name: Wave Plus Radon Long Term pressure: - name: "Wave Plus Pressure" + name: Wave Plus Pressure humidity: - name: "Wave Plus Humidity" + name: Wave Plus Humidity co2: - name: "Wave Plus CO2" + name: Wave Plus CO2 tvoc: - name: "Wave Plus VOC" + name: Wave Plus VOC - platform: airthings_wave_mini ble_client_id: airthingsmini01 update_interval: 5min temperature: - name: "Wave Mini Temperature" + name: Wave Mini Temperature humidity: - name: "Wave Mini Humidity" + name: Wave Mini Humidity pressure: - name: "Wave Mini Pressure" + name: Wave Mini Pressure tvoc: - name: "Wave Mini VOC" + name: Wave Mini VOC - platform: ina260 address: 0x40 current: - name: "INA260 Current" + name: INA260 Current power: - name: "INA260 Power" + name: INA260 Power bus_voltage: - name: "INA260 Voltage" + name: INA260 Voltage update_interval: 60s - platform: radon_eye_rd200 ble_client_id: radon_eye_ble_id update_interval: 10min radon: - name: "RD200 Radon" + name: RD200 Radon radon_long_term: - name: "RD200 Radon Long Term" + name: RD200 Radon Long Term - platform: mopeka_pro_check mac_address: D3:75:F2:DC:16:91 tank_type: CUSTOM custom_distance_full: 40cm custom_distance_empty: 10mm temperature: - name: "Propane test temp" + name: Propane test temp level: - name: "Propane test level" + name: Propane test level distance: - name: "Propane test distance" + name: Propane test distance battery_level: - name: "Propane test battery level" + name: Propane test battery level + - platform: ufire_ec + id: ufire_ec_board + ec: + name: Ufire EC + temperature_sensor: ha_hello_world_temperature + temperature_compensation: 20.0 + temperature_coefficient: 0.019 + - platform: ufire_ise + id: ufire_ise_board + temperature_sensor: ha_hello_world_temperature + ph: + name: Ufire pH + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 time: - platform: homeassistant on_time: - - at: '16:00:00' + - at: "16:00:00" then: - logger.log: It's 16:00 esp32_touch: - setup_mode: True + setup_mode: true binary_sensor: - platform: homeassistant @@ -389,76 +416,89 @@ binary_sensor: id: ha_hello_world_binary_attribute - platform: ble_presence mac_address: AC:37:43:77:5F:4C - name: 'ESP32 BLE Tracker Google Home Mini' + name: ESP32 BLE Tracker Google Home Mini - platform: ble_presence - service_uuid: '11aa' - name: 'BLE Test Service 16 Presence' + service_uuid: 11aa + name: BLE Test Service 16 Presence - platform: ble_presence - service_uuid: '11223344' - name: 'BLE Test Service 32 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' + service_uuid: 11223344-5566-7788-99aa-bbccddeeff00 + name: BLE Test Service 128 Presence - platform: ble_presence - ibeacon_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00 ibeacon_major: 100 ibeacon_minor: 1 - name: 'BLE Test iBeacon Presence' + name: BLE Test iBeacon Presence - platform: esp32_touch - name: 'ESP32 Touch Pad GPIO27' + name: ESP32 Touch Pad GPIO27 pin: GPIO27 threshold: 1000 - platform: as3935 - name: 'Storm Alert' + name: Storm Alert - platform: xiaomi_mue4094rt - name: 'MUE4094RT Motion' - mac_address: '7A:80:8E:19:36:BA' - timeout: '5s' + name: MUE4094RT Motion + mac_address: 7A:80:8E:19:36:BA + timeout: 5s - platform: xiaomi_mjyd02yla - name: 'MJYD02YL-A Motion' - mac_address: '50:EC:50:CD:32:02' - bindkey: '48403ebe2d385db8d0c187f81e62cb64' + name: MJYD02YL-A Motion + mac_address: 50:EC:50:CD:32:02 + bindkey: 48403ebe2d385db8d0c187f81e62cb64 idle_time: - name: 'MJYD02YL-A Idle Time' + name: MJYD02YL-A Idle Time light: - name: 'MJYD02YL-A Light Status' + name: MJYD02YL-A Light Status battery_level: - name: 'MJYD02YL-A Battery Level' + name: MJYD02YL-A Battery Level - platform: xiaomi_wx08zm - name: 'WX08ZM Activation State' - mac_address: '74:a3:4a:b5:07:34' + name: WX08ZM Activation State + mac_address: 74:a3:4a:b5:07:34 tablet: - name: 'WX08ZM Tablet Resource' + name: WX08ZM Tablet Resource battery_level: - name: 'WX08ZM Battery Level' + name: WX08ZM Battery Level - platform: xiaomi_cgpr1 - name: 'CGPR1 Motion' - mac_address: '12:34:56:12:34:56' - bindkey: '48403ebe2d385db8d0c187f81e62cb64' + name: CGPR1 Motion + mac_address: "12:34:56:12:34:56" + bindkey: 48403ebe2d385db8d0c187f81e62cb64 battery_level: - name: 'CGPR1 battery Level' + name: CGPR1 battery Level idle_time: - name: 'CGPR1 Idle Time' + name: CGPR1 Idle Time illuminance: - name: 'CGPR1 Illuminance' + name: CGPR1 Illuminance - platform: xiaomi_rtcgq02lm id: motion_rtcgq02lm motion: - name: 'Mi Motion Sensor 2' + name: Mi Motion Sensor 2 light: - name: 'Mi Motion Sensor 2 Light' + name: Mi Motion Sensor 2 Light button: - name: 'Mi Motion Sensor 2 Button' + name: Mi Motion Sensor 2 Button + - platform: gpio + id: gpio_set_retry_test + pin: GPIO9 + on_press: + then: + - lambda: |- + App.scheduler.set_retry(id(gpio_set_retry_test), "set_retry_test", 100, 3, [](const uint8_t remaining) { + return remaining ? RetryResult::RETRY : RetryResult::DONE; // just to reference both symbols + }, 5.0f); esp32_ble_tracker: on_ble_advertise: - mac_address: AC:37:43:77:5F:4C then: + # yamllint disable rule:line-length - lambda: !lambda |- ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + # yamllint enable rule:line-length - then: + # yamllint disable rule:line-length - lambda: !lambda |- ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + # yamllint enable rule:line-length on_ble_service_data_advertise: - service_uuid: ABCD then: @@ -478,7 +518,6 @@ ble_client: - mac_address: 01:02:03:04:05:06 id: radon_eye_ble_id - airthings_ble: radon_eye_ble: @@ -489,21 +528,20 @@ xiaomi_ble: mopeka_ble: +bluetooth_proxy: + active: true + xiaomi_rtcgq02lm: - id: motion_rtcgq02lm mac_address: 01:02:03:04:05:06 - bindkey: '48403ebe2d385db8d0c187f81e62cb64' - -#esp32_ble_beacon: -# type: iBeacon -# uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' + bindkey: "48403ebe2d385db8d0c187f81e62cb64" status_led: pin: GPIO2 text_sensor: - platform: version - name: 'ESPHome Version' + name: ESPHome Version icon: mdi:icon id: version_sensor on_value: @@ -511,9 +549,21 @@ text_sensor: condition: - api.connected: then: + # yamllint disable rule:line-length - lambda: !lambda |- ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); + # yamllint enable rule:line-length - script.execute: my_script + - script.execute: + id: my_script_with_params + prefix: Running my_script_with_params + param2: 100 + param3: true + - script.execute: + id: my_script_with_params + prefix: Running my_script_with_params using lambda parameters + param2: !lambda return 200; + param3: !lambda return true; - homeassistant.service: service: notify.html5 data: @@ -537,19 +587,19 @@ text_sensor: - deep_sleep.enter: sleep_duration: !lambda "return 30 * 60 * 1000;" - platform: template - name: 'Template Text Sensor' + name: Template Text Sensor lambda: |- return {"Hello World"}; filters: - to_upper: - to_lower: - - append: "xyz" - - prepend: "abcd" + - append: xyz + - prepend: abcd - substitute: - Hello -> Goodbye - map: - red -> green - - lambda: return {"1234"}; + - lambda: 'return {"1234"};' - platform: homeassistant entity_id: sensor.hello_world2 id: ha_hello_world2 @@ -579,6 +629,13 @@ script: mode: restart then: - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_with_params + parameters: + prefix: string + param2: int + param3: bool + then: + - lambda: 'ESP_LOGD("main", (prefix + " Hello World!" + to_string(param2) + " " + to_string(param3)).c_str());' stepper: - platform: uln2003 @@ -587,7 +644,7 @@ stepper: pin_b: GPIO27 pin_c: GPIO25 pin_d: GPIO26 - sleep_when_done: no + sleep_when_done: false step_mode: HALF_STEP max_speed: 250 steps/s @@ -598,7 +655,7 @@ stepper: interval: interval: 5s then: - - logger.log: 'Interval Run' + - logger.log: Interval Run display: @@ -611,7 +668,7 @@ cap1188: switch: - platform: template - name: "Test BLE Write Action" + name: Test BLE Write Action turn_on_action: - ble_client.ble_write: id: airthings01 diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml new file mode 100644 index 0000000000..19e4341ee6 --- /dev/null +++ b/tests/test3.1.yaml @@ -0,0 +1,600 @@ +--- +esphome: + name: $device_name + comment: $device_comment + build_path: build/test3.1 + includes: + - custom.h + +esp8266: + board: d1_mini + +substitutions: + device_name: test3-1 + device_comment: test3-1 device + min_sub: "0.03" + max_sub: "12.0%" + +api: + +wifi: + ssid: "MySSID" + password: "password1" + +i2c: + sda: 4 + scl: 5 + scan: false + +spi: + clk_pin: GPIO12 + mosi_pin: GPIO13 + miso_pin: GPIO14 + +ota: + +logger: + +sensor: + - platform: apds9960 + type: proximity + name: APDS9960 Proximity + - platform: vl53l0x + name: VL53L0x Distance + address: 0x29 + update_interval: 60s + enable_pin: GPIO13 + timeout: 200us + - platform: apds9960 + type: clear + name: APDS9960 Clear + - platform: apds9960 + type: red + name: APDS9960 Red + - platform: apds9960 + type: green + name: APDS9960 Green + - platform: apds9960 + type: blue + name: APDS9960 Blue + + - platform: aht10 + temperature: + name: Temperature + humidity: + name: Humidity + - platform: am2320 + temperature: + name: Temperature + humidity: + name: Humidity + - platform: adc + pin: VCC + id: my_sensor + filters: + - offset: 5.0 + - multiply: 2.0 + - filter_out: NAN + - sliding_window_moving_average: + - exponential_moving_average: + - quantile: + window_size: 5 + send_every: 5 + send_first_at: 3 + quantile: .8 + - lambda: "return 0;" + - delta: 100 + - throttle: 100ms + - debounce: 500s + - calibrate_linear: + - 0 -> 0 + - 100 -> 100 + - calibrate_polynomial: + degree: 3 + datapoints: + - 0 -> 0 + - 100 -> 200 + - 400 -> 500 + - -50 -> -1000 + - -100 -> -10000 + - platform: cd74hc4067 + id: cd74hc4067_0 + number: 0 + sensor: my_sensor + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C + - platform: ntc + sensor: resist + name: NTC Sensor2 + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + - platform: ct_clamp + sensor: my_sensor + name: CT Clamp + sample_duration: 500ms + update_interval: 5s + + - platform: tcs34725 + red_channel: + name: Red Channel + green_channel: + name: Green Channel + blue_channel: + name: Blue Channel + clear_channel: + name: Clear Channel + illuminance: + name: Illuminance + color_temperature: + name: Color Temperature + integration_time: 614ms + gain: 60x + - platform: custom + lambda: |- + auto s = new CustomSensor(); + App.register_component(s); + return {s}; + sensors: + - id: custom_sensor + name: Custom Sensor + + - platform: ade7953 + irq_pin: GPIO16 + voltage: + name: ADE7953 Voltage + id: ade7953_voltage + current_a: + name: ADE7953 Current A + id: ade7953_current_a + current_b: + name: ADE7953 Current B + id: ade7953_current_b + active_power_a: + name: ADE7953 Active Power A + id: ade7953_active_power_a + active_power_b: + name: ADE7953 Active Power B + id: ade7953_active_power_b + + - platform: tmp102 + name: TMP102 Temperature + - platform: hm3301 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 + aqi: + name: AQI + calculation_type: AQI + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: pH + - platform: tof10120 + name: Distance sensor + update_interval: 5s + + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: 3X + x_axis: + name: mlxxaxis + y_axis: + name: mlxyaxis + z_axis: + name: mlxzaxis + resolution: 17BIT + temperature: + name: mlxtemp + oversampling: 2 + + - platform: adc128s102 + id: adc128s102_channel_0 + channel: 0 + +apds9960: + address: 0x20 + update_interval: 60s + +mpr121: + id: mpr121_first + address: 0x5A + +binary_sensor: + - platform: apds9960 + direction: up + name: APDS9960 Up + device_class: motion + filters: + - invert + - delayed_on: 20ms + - delayed_off: 20ms + - lambda: "return false;" + on_state: + - logger.log: New state + id: my_binary_sensor + - platform: apds9960 + direction: down + name: APDS9960 Down + - platform: apds9960 + direction: left + name: APDS9960 Left + - platform: apds9960 + direction: right + name: APDS9960 Right + + - platform: mpr121 + id: touchkey0 + channel: 0 + name: touchkey0 + - platform: mpr121 + channel: 1 + name: touchkey1 + id: bin1 + - platform: mpr121 + channel: 2 + name: touchkey2 + id: bin2 + - platform: mpr121 + channel: 3 + name: touchkey3 + id: bin3 + on_press: + then: + - switch.toggle: mpr121_toggle + - platform: ttp229_lsf + channel: 1 + name: TTP229 LSF Test + - platform: ttp229_bsf + channel: 1 + name: TTP229 BSF Test + - platform: custom + lambda: |- + auto s = new CustomBinarySensor(); + App.register_component(s); + return {s}; + binary_sensors: + - id: custom_binary_sensor + name: Custom Binary Sensor + + - platform: template + id: cover_toggle + on_press: + then: + - cover.toggle: time_based_cover + - cover.toggle: endstop_cover + - cover.toggle: current_based_cover + +globals: + - id: my_global_string + type: std::string + initial_value: '""' + +text_sensor: + - platform: custom + lambda: |- + auto s = new CustomTextSensor(); + App.register_component(s); + return {s}; + text_sensors: + - id: custom_text_sensor + name: Custom Text Sensor + +sm2135: + data_pin: GPIO12 + clock_pin: GPIO14 + +switch: + - platform: template + name: mpr121_toggle + id: mpr121_toggle + optimistic: true + - platform: gpio + id: gpio_switch1 + pin: + mcp23xxx: mcp23017_hub + number: 0 + mode: OUTPUT + interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] + - platform: gpio + id: gpio_switch2 + pin: + mcp23xxx: mcp23008_hub + number: 0 + mode: OUTPUT + interlock: *interlock + - platform: gpio + id: gpio_switch3 + pin: GPIO1 + interlock: *interlock + - platform: custom + lambda: |- + auto s = new CustomSwitch(); + return {s}; + switches: + - id: custom_switch + name: Custom Switch + on_turn_on: + - http_request.get: + url: https://esphome.io + headers: + Content-Type: application/json + verify_ssl: false + - http_request.post: + url: https://esphome.io + verify_ssl: false + json: + key: !lambda |- + return id(custom_text_sensor).state; + greeting: Hello World + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: Some data + verify_ssl: false + + +custom_component: + lambda: |- + auto s = new CustomComponent(); + s->set_update_interval(15000); + return {s}; + +stepper: + - platform: uln2003 + id: my_stepper + pin_a: GPIO12 + pin_b: GPIO13 + pin_c: GPIO14 + pin_d: GPIO15 + sleep_when_done: false + step_mode: HALF_STEP + max_speed: 250 steps/s + acceleration: inf + deceleration: inf + - platform: a4988 + id: my_stepper2 + step_pin: GPIO1 + dir_pin: GPIO2 + max_speed: 0.1 steps/s + acceleration: 10 steps/s^2 + deceleration: 10 steps/s^2 + +interval: + interval: 5s + then: + - logger.log: Interval Run + - stepper.set_target: + id: my_stepper2 + target: 500 + - stepper.set_target: + id: my_stepper + target: !lambda "return 0;" + - stepper.report_position: + id: my_stepper2 + position: 0 + - stepper.report_position: + id: my_stepper + position: !lambda "return 50/100.0;" + +cover: + - platform: endstop + name: Endstop Cover + id: endstop_cover + stop_action: + - switch.turn_on: gpio_switch1 + open_endstop: my_binary_sensor + open_action: + - switch.turn_on: gpio_switch1 + open_duration: 5min + close_endstop: my_binary_sensor + close_action: + - switch.turn_on: gpio_switch2 + - output.set_level: + id: out + level: 50% + - output.esp8266_pwm.set_frequency: + id: out + frequency: 500.0Hz + - output.esp8266_pwm.set_frequency: + id: out + frequency: !lambda "return 500.0;" + - servo.write: + id: my_servo + level: -100% + - servo.write: + id: my_servo + level: !lambda "return -1.0;" + - delay: 2s + - servo.detach: my_servo + close_duration: 4.5min + max_duration: 10min + - platform: time_based + name: Time Based Cover + id: time_based_cover + stop_action: + - switch.turn_on: gpio_switch1 + open_action: + - switch.turn_on: gpio_switch1 + open_duration: 5min + close_action: + - switch.turn_on: gpio_switch2 + close_duration: 4.5min + - platform: current_based + name: Current Based Cover + id: current_based_cover + open_sensor: ade7953_current_a + open_moving_current_threshold: 0.5 + open_obstacle_current_threshold: 0.8 + open_duration: 12s + open_action: + - switch.turn_on: gpio_switch1 + close_sensor: ade7953_current_b + close_moving_current_threshold: 0.5 + close_obstacle_current_threshold: 0.8 + close_duration: 10s + close_action: + - switch.turn_on: gpio_switch2 + stop_action: + - switch.turn_off: gpio_switch1 + - switch.turn_off: gpio_switch2 + obstacle_rollback: 30% + start_sensing_delay: 0.8s + malfunction_detection: true + malfunction_action: + then: + - logger.log: Malfunction Detected + - platform: template + name: Template Cover with Tilt + tilt_lambda: "return 0.5;" + tilt_action: + - output.set_level: + id: out + level: !lambda "return tilt;" + position_action: + - output.set_level: + id: out + level: !lambda "return pos;" + +output: + - platform: esp8266_pwm + id: out + pin: D3 + frequency: 50Hz + - platform: esp8266_pwm + id: out2 + pin: D4 + - platform: custom + type: binary + lambda: |- + auto s = new CustomBinaryOutput(); + App.register_component(s); + return {s}; + outputs: + - id: custom_binary + - platform: sigma_delta_output + id: sddac + update_interval: 60s + pin: D4 + turn_on_action: + then: + - logger.log: "Turned on" + turn_off_action: + then: + - logger.log: "Turned off" + state_change_action: + then: + - logger.log: + format: "Changed state: %d" + args: ["state"] + - platform: custom + type: float + lambda: |- + auto s = new CustomFloatOutput(); + App.register_component(s); + return {s}; + outputs: + - id: custom_float + - platform: slow_pwm + pin: GPIO5 + id: my_slow_pwm + period: 15s + restart_cycle_on_state_change: false + - platform: sm2135 + id: sm2135_0 + channel: 0 + - platform: sm2135 + id: sm2135_1 + channel: 1 + - platform: sm2135 + id: sm2135_2 + channel: 2 + - platform: sm2135 + id: sm2135_3 + channel: 3 + - platform: sm2135 + id: sm2135_4 + channel: 4 + +mcp23017: + id: mcp23017_hub + +mcp23008: + id: mcp23008_hub + + +light: + - platform: hbridge + name: Icicle Lights + pin_a: out + pin_b: out2 + +servo: + id: my_servo + output: out + restore: true + min_level: $min_sub + max_level: $max_sub + +ttp229_lsf: + +ttp229_bsf: + sdo_pin: D2 + scl_pin: D1 + + +display: + - platform: max7219digit + cs_pin: GPIO15 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: STOP + id: my_matrix + lambda: |- + it.printdigit("hello"); + + +http_request: + useragent: esphome/device + timeout: 10s + +button: + - platform: output + id: output_button + output: out + duration: 100ms + - platform: wake_on_lan + target_mac_address: 12:34:56:78:90:ab + name: wol_test_1 + id: wol_1 + - platform: factory_reset + name: Restart Button (Factory Default Settings) + +cd74hc4067: + pin_s0: GPIO12 + pin_s1: GPIO13 + pin_s2: GPIO14 + pin_s3: GPIO15 + +adc128s102: + cs_pin: GPIO12 diff --git a/tests/test3.yaml b/tests/test3.yaml index 4b2224dc3c..c4847725e8 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,3 +1,4 @@ +--- esphome: name: $device_name comment: $device_comment @@ -9,38 +10,34 @@ esphome: - wifi.connected - time.has_time then: - - logger.log: "Have time" - includes: - - custom.h + - logger.log: Have time esp8266: board: d1_mini - early_pin_init: True + early_pin_init: true substitutions: device_name: test3 device_comment: test3 device - min_sub: '0.03' - max_sub: '12.0%' api: port: 8000 - password: 'pwd' + password: pwd reboot_timeout: 0min encryption: - key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=' + key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= services: - service: hello_world variables: name: string then: - logger.log: - format: 'Hello World %s!' + format: Hello World %s! args: - name.c_str() - service: empty_service then: - - logger.log: 'Service Called' + - logger.log: Service Called - service: all_types variables: bool_: bool @@ -48,10 +45,7 @@ api: float_: float string_: string then: - - logger.log: 'Something happened' - - stepper.set_target: - id: my_stepper2 - target: !lambda 'return int_;' + - logger.log: Something happened - service: array_types variables: bool_arr: bool[] @@ -60,7 +54,9 @@ api: string_arr: string[] then: - logger.log: - format: 'Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)' + # yamllint disable rule:line-length + format: "Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)" + # yamllint enable rule:line-length args: - YESNO(bool_arr[0]) - bool_arr.size() @@ -80,31 +76,31 @@ api: variables: file: int then: - - dfplayer.play: !lambda 'return file;' + - 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_;' + 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;' + 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 + folder: !lambda "return folder;" + loop: true - service: dfplayer_set_device variables: @@ -117,12 +113,14 @@ api: variables: volume: int then: - - dfplayer.set_volume: !lambda 'return volume;' + - dfplayer.set_volume: !lambda "return volume;" - service: dfplayer_set_eq variables: preset: int then: - - dfplayer.set_eq: !lambda 'return static_cast(preset);' + # yamllint disable rule:line-length + - dfplayer.set_eq: !lambda "return static_cast(preset);" + # yamllint enable rule:line-length - service: dfplayer_sleep then: @@ -162,21 +160,21 @@ api: then: - tm1651.set_level_percent: id: tm1651_battery - level_percent: !lambda 'return level_percent;' + level_percent: !lambda "return level_percent;" - service: battery_level variables: level: int then: - tm1651.set_level: id: tm1651_battery - level: !lambda 'return level;' + level: !lambda "return level;" - service: battery_brightness variables: brightness: int then: - tm1651.set_brightness: id: tm1651_battery - brightness: !lambda 'return brightness;' + brightness: !lambda "return brightness;" - service: battery_turn_on then: - tm1651.turn_on: @@ -198,8 +196,8 @@ api: num_scans: int then: - fingerprint_grow.enroll: - finger_id: !lambda 'return finger_id;' - num_scans: !lambda 'return num_scans;' + finger_id: !lambda "return finger_id;" + num_scans: !lambda "return num_scans;" - service: fingerprint_grow_cancel_enroll then: - fingerprint_grow.cancel_enroll: @@ -208,57 +206,47 @@ api: finger_id: int then: - fingerprint_grow.delete: - finger_id: !lambda 'return finger_id;' + finger_id: !lambda "return finger_id;" - service: fingerprint_grow_delete_all then: - fingerprint_grow.delete_all: wifi: - ssid: 'MySSID' - password: 'password1' - -i2c: - sda: 4 - scl: 5 - scan: False - -spi: - clk_pin: GPIO12 - mosi_pin: GPIO13 - miso_pin: GPIO14 + ssid: "MySSID" + password: "password1" uart: - - id: uart1 + - id: uart_1 tx_pin: number: GPIO1 - inverted: yes + inverted: true rx_pin: GPIO3 baud_rate: 115200 - - id: uart2 + - id: uart_2 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 - - id: uart3 + - id: uart_3 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 4800 - - id: uart4 + - id: uart_4 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 - - id: uart5 + - id: uart_5 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 - - id: uart6 + - id: uart_6 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 - - id: uart7 + - id: uart_7 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 38400 - - id: uart8 + - id: uart_8 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 4800 @@ -266,20 +254,31 @@ uart: stop_bits: 2 # Specifically added for testing debug with no options at all. debug: - - id: uart9 + - id: uart_9 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 - - id: uart10 + - id: uart_10 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart_11 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 + - id: uart_12 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 modbus: - uart_id: uart1 + uart_id: uart_1 + +vbus: + uart_id: uart_4 ota: - safe_mode: True + safe_mode: true port: 3286 reboot_timeout: 15min @@ -289,6 +288,7 @@ logger: esp8266_store_log_strings_in_flash: true improv_serial: + next_url: https://esphome.io/?name={{device_name}}&version={{esphome_version}}&ip={{ip_address}} deep_sleep: run_duration: 20s @@ -298,174 +298,65 @@ wled: adalight: - sensor: - platform: daly_bms voltage: - name: "Battery Voltage" + name: Battery Voltage current: - name: "Battery Current" + name: Battery Current battery_level: - name: "Battery Level" + name: Battery Level max_cell_voltage: - name: "Max Cell Voltage" + name: Max Cell Voltage max_cell_voltage_number: - name: "Max Cell Voltage Number" + name: Max Cell Voltage Number min_cell_voltage: - name: "Min Cell Voltage" + name: Min Cell Voltage min_cell_voltage_number: - name: "Min Cell Voltage Number" + name: Min Cell Voltage Number max_temperature: - name: "Max Temperature" + name: Max Temperature max_temperature_probe_number: - name: "Max Temperature Probe Number" + name: Max Temperature Probe Number min_temperature: - name: "Min Temperature" + name: Min Temperature min_temperature_probe_number: - name: "Min Temperature Probe Number" + name: Min Temperature Probe Number remaining_capacity: - name: "Remaining Capacity" + name: Remaining Capacity cells_number: - name: "Cells Number" + name: Cells Number temperature_1: - name: "Temperature 1" + name: Temperature 1 temperature_2: - name: "Temperature 2" - - platform: apds9960 - type: proximity - name: APDS9960 Proximity - - platform: vl53l0x - name: 'VL53L0x Distance' - address: 0x29 - update_interval: 60s - enable_pin: GPIO13 - timeout: 200us - - platform: apds9960 - type: clear - name: APDS9960 Clear - - platform: apds9960 - type: red - name: APDS9960 Red - - platform: apds9960 - type: green - name: APDS9960 Green - - platform: apds9960 - type: blue - name: APDS9960 Blue + name: Temperature 2 + - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world - - platform: aht10 - temperature: - name: 'Temperature' - humidity: - name: 'Humidity' - - platform: am2320 - temperature: - name: 'Temperature' - humidity: - name: 'Humidity' + - platform: hydreon_rgxx - model: "RG 9" - uart_id: uart6 - id: "hydreon_rg9" + model: RG 9 + uart_id: uart_6 + id: hydreon_rg9 moisture: - name: "hydreon_rain" + name: hydreon_rain id: hydreon_rain - platform: hydreon_rgxx - model: "RG_15" - uart_id: uart6 + model: RG_15 + uart_id: uart_6 acc: - name: "hydreon_acc" + name: hydreon_acc event_acc: - name: "hydreon_event_acc" + name: hydreon_event_acc total_acc: - name: "hydreon_total_acc" + name: hydreon_total_acc r_int: - name: "hydreon_r_int" + name: hydreon_r_int - platform: adc pin: VCC id: my_sensor - filters: - - offset: 5.0 - - multiply: 2.0 - - filter_out: NAN - - sliding_window_moving_average: - - exponential_moving_average: - - quantile: - window_size: 5 - send_every: 5 - send_first_at: 3 - quantile: .8 - - lambda: 'return 0;' - - delta: 100 - - throttle: 100ms - - debounce: 500s - - calibrate_linear: - - 0 -> 0 - - 100 -> 100 - - calibrate_polynomial: - degree: 3 - datapoints: - - 0 -> 0 - - 100 -> 200 - - 400 -> 500 - - -50 -> -1000 - - -100 -> -10000 - - platform: cd74hc4067 - id: cd74hc4067_0 - number: 0 - sensor: my_sensor - - platform: resistance - sensor: my_sensor - configuration: DOWNSTREAM - resistor: 10kΩ - reference_voltage: 3.3V - name: Resistance - id: resist - - platform: ntc - sensor: resist - name: NTC Sensor - calibration: - b_constant: 3950 - reference_resistance: 10k - reference_temperature: 25°C - - platform: ntc - sensor: resist - name: NTC Sensor2 - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - - platform: ct_clamp - sensor: my_sensor - name: CT Clamp - sample_duration: 500ms - update_interval: 5s - - platform: tcs34725 - red_channel: - name: Red Channel - green_channel: - name: Green Channel - blue_channel: - name: Blue Channel - clear_channel: - name: Clear Channel - illuminance: - name: Illuminance - color_temperature: - name: Color Temperature - integration_time: 614ms - gain: 60x - - platform: custom - lambda: |- - auto s = new CustomSensor(); - App.register_component(s); - return {s}; - sensors: - - id: custom_sensor - name: Custom Sensor - platform: binary_sensor_map name: Binary Sensor Map type: group @@ -476,405 +367,384 @@ sensor: value: 15.0 - binary_sensor: bin3 value: 100.0 - - platform: ade7953 - irq_pin: GPIO16 - voltage: - name: ADE7953 Voltage - id: ade7953_voltage - current_a: - name: ADE7953 Current A - id: ade7953_current_a - current_b: - name: ADE7953 Current B - id: ade7953_current_b - active_power_a: - name: ADE7953 Active Power A - id: ade7953_active_power_a - active_power_b: - name: ADE7953 Active Power B - id: ade7953_active_power_b + + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 + - platform: bl0939 - uart_id: uart8 + uart_id: uart_8 voltage: - name: 'BL0939 Voltage' + name: BL0939 Voltage current_1: - name: 'BL0939 Current 1' + name: BL0939 Current 1 current_2: - name: 'BL0939 Current 2' + name: BL0939 Current 2 active_power_1: - name: 'BL0939 Active Power 1' + name: BL0939 Active Power 1 active_power_2: - name: 'BL0939 Active Power 2' + name: BL0939 Active Power 2 energy_1: - name: 'BL0939 Energy 1' + name: BL0939 Energy 1 energy_2: - name: 'BL0939 Energy 2' + name: BL0939 Energy 2 energy_total: - name: 'BL0939 Total energy' + name: BL0939 Total energy - platform: bl0940 - uart_id: uart3 + uart_id: uart_3 voltage: - name: 'BL0940 Voltage' + name: BL0940 Voltage current: - name: 'BL0940 Current' + name: BL0940 Current power: - name: 'BL0940 Power' + name: BL0940 Power energy: - name: 'BL0940 Energy' + name: BL0940 Energy internal_temperature: - name: 'BL0940 Internal temperature' + name: BL0940 Internal temperature external_temperature: - name: 'BL0940 External temperature' - - platform: pzem004t - uart_id: uart3 + name: BL0940 External temperature + - platform: bl0942 + uart_id: uart_3 voltage: - name: 'PZEM004T Voltage' + name: BL0942 Voltage current: - name: 'PZEM004T Current' + name: BL0942 Current power: - name: 'PZEM004T Power' + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency + - platform: pzem004t + uart_id: uart_3 + voltage: + name: PZEM004T Voltage + current: + name: PZEM004T Current + power: + name: PZEM004T Power - platform: pzemac id: pzemac1 voltage: - name: 'PZEMAC Voltage' + name: PZEMAC Voltage current: - name: 'PZEMAC Current' + name: PZEMAC Current power: - name: 'PZEMAC Power' + name: PZEMAC Power energy: - name: 'PZEMAC Energy' + name: PZEMAC Energy frequency: - name: 'PZEMAC Frequency' + name: PZEMAC Frequency power_factor: - name: 'PZEMAC Power Factor' + name: PZEMAC Power Factor - platform: pzemdc + id: pzemdc1 voltage: - name: 'PZEMDC Voltage' + name: PZEMDC Voltage current: - name: 'PZEMDC Current' + name: PZEMDC Current power: - name: 'PZEMDC Power' - - platform: tmp102 - name: 'TMP102 Temperature' - - platform: hm3301 - pm_1_0: - name: 'PM1.0' - pm_2_5: - name: 'PM2.5' - pm_10_0: - name: 'PM10.0' - aqi: - name: 'AQI' - calculation_type: 'AQI' + name: PZEMDC Power + energy: + name: PZEMDC Energy + - platform: pmsx003 - uart_id: uart9 + uart_id: uart_9 type: PMSX003 pm_1_0: - name: 'PM 1.0 Concentration' + name: PM 1.0 Concentration pm_2_5: - name: 'PM 2.5 Concentration' + name: PM 2.5 Concentration pm_10_0: - name: 'PM 10.0 Concentration' + name: PM 10.0 Concentration pm_1_0_std: - name: 'PM 1.0 Standard Atmospher Concentration' + name: PM 1.0 Standard Atmospher Concentration pm_2_5_std: - name: 'PM 2.5 Standard Atmospher Concentration' + name: PM 2.5 Standard Atmospher Concentration pm_10_0_std: - name: 'PM 10.0 Standard Atmospher Concentration' + name: PM 10.0 Standard Atmospher Concentration pm_0_3um: - name: 'Particulate Count >0.3um' + name: Particulate Count >0.3um pm_0_5um: - name: 'Particulate Count >0.5um' + name: Particulate Count >0.5um pm_1_0um: - name: 'Particulate Count >1.0um' + name: Particulate Count >1.0um pm_2_5um: - name: 'Particulate Count >2.5um' + name: Particulate Count >2.5um pm_5_0um: - name: 'Particulate Count >5.0um' + name: Particulate Count >5.0um pm_10_0um: - name: 'Particulate Count >10.0um' + name: Particulate Count >10.0um update_interval: 30s - platform: pmsx003 - uart_id: uart5 + uart_id: uart_5 type: PMS5003T + pm_1_0: + name: PM 1.0 Concentration pm_2_5: - name: 'PM 2.5 Concentration' + name: PM 2.5 Concentration + pm_10_0: + name: PM 10.0 Concentration + pm_1_0_std: + name: PM 1.0 Standard Atmospher Concentration + pm_2_5_std: + name: PM 2.5 Standard Atmospher Concentration + pm_10_0_std: + name: PM 10.0 Standard Atmospher Concentration + pm_0_3um: + name: Particulate Count >0.3um + pm_0_5um: + name: Particulate Count >0.5um + pm_1_0um: + name: Particulate Count >1.0um + pm_2_5um: + name: Particulate Count >2.5um temperature: - name: 'PMS Temperature' + name: PMS Temperature humidity: - name: 'PMS Humidity' + name: PMS Humidity - platform: pmsx003 - uart_id: uart6 + uart_id: uart_6 type: PMS5003ST pm_1_0: - name: 'PM 1.0 Concentration' + name: PM 1.0 Concentration pm_2_5: - name: 'PM 2.5 Concentration' + name: PM 2.5 Concentration pm_10_0: - name: 'PM 10.0 Concentration' + name: PM 10.0 Concentration pm_1_0_std: - name: 'PM 1.0 Standard Atmospher Concentration' + name: PM 1.0 Standard Atmospher Concentration pm_2_5_std: - name: 'PM 2.5 Standard Atmospher Concentration' + name: PM 2.5 Standard Atmospher Concentration pm_10_0_std: - name: 'PM 10.0 Standard Atmospher Concentration' + name: PM 10.0 Standard Atmospher Concentration pm_0_3um: - name: 'Particulate Count >0.3um' + name: Particulate Count >0.3um pm_0_5um: - name: 'Particulate Count >0.5um' + name: Particulate Count >0.5um pm_1_0um: - name: 'Particulate Count >1.0um' + name: Particulate Count >1.0um pm_2_5um: - name: 'Particulate Count >2.5um' + name: Particulate Count >2.5um pm_5_0um: - name: 'Particulate Count >5.0um' + name: Particulate Count >5.0um pm_10_0um: - name: 'Particulate Count >10.0um' + name: Particulate Count >10.0um temperature: - name: 'PMS Temperature' + name: PMS Temperature humidity: - name: 'PMS Humidity' + name: PMS Humidity formaldehyde: - name: 'PMS Formaldehyde Concentration' + name: PMS Formaldehyde Concentration - platform: cse7761 - uart_id: uart7 + uart_id: uart_7 voltage: - name: 'CSE7761 Voltage' + name: CSE7761 Voltage current_1: - name: 'CSE7761 Current 1' + name: CSE7761 Current 1 current_2: - name: 'CSE7761 Current 2' + name: CSE7761 Current 2 active_power_1: - name: 'CSE7761 Active Power 1' + name: CSE7761 Active Power 1 active_power_2: - name: 'CSE7761 Active Power 2' + name: CSE7761 Active Power 2 - platform: cse7766 - uart_id: uart3 + uart_id: uart_3 voltage: - name: 'CSE7766 Voltage' + name: CSE7766 Voltage current: - name: 'CSE7766 Current' + name: CSE7766 Current power: - name: 'CSE776 Power' - - platform: ezo - id: ph_ezo - address: 99 - unit_of_measurement: 'pH' - - platform: tof10120 - name: "Distance sensor" - update_interval: 5s + name: CSE776 Power + - platform: fingerprint_grow fingerprint_count: - name: "Fingerprint Count" + name: Fingerprint Count status: - name: "Fingerprint Status" + name: Fingerprint Status capacity: - name: "Fingerprint Capacity" + name: Fingerprint Capacity security_level: - name: "Fingerprint Security Level" + name: Fingerprint Security Level last_finger_id: - name: "Fingerprint Last Finger ID" + name: Fingerprint Last Finger ID last_confidence: - name: "Fingerprint Last Confidence" + name: Fingerprint Last Confidence - platform: sdm_meter phase_a: current: - name: 'Phase A Current' + name: Phase A Current voltage: - name: 'Phase A Voltage' + name: Phase A Voltage active_power: - name: 'Phase A Power' + name: Phase A Power power_factor: - name: 'Phase A Power Factor' + name: Phase A Power Factor apparent_power: - name: 'Phase A Apparent Power' + name: Phase A Apparent Power reactive_power: - name: 'Phase A Reactive Power' + name: Phase A Reactive Power phase_angle: - name: 'Phase A Phase Angle' + name: Phase A Phase Angle phase_b: current: - name: 'Phase B Current' + name: Phase B Current voltage: - name: 'Phase B Voltage' + name: Phase B Voltage active_power: - name: 'Phase B Power' + name: Phase B Power power_factor: - name: 'Phase B Power Factor' + name: Phase B Power Factor apparent_power: - name: 'Phase B Apparent Power' + name: Phase B Apparent Power reactive_power: - name: 'Phase B Reactive Power' + name: Phase B Reactive Power phase_angle: - name: 'Phase B Phase Angle' + name: Phase B Phase Angle phase_c: current: - name: 'Phase C Current' + name: Phase C Current voltage: - name: 'Phase C Voltage' + name: Phase C Voltage active_power: - name: 'Phase C Power' + name: Phase C Power power_factor: - name: 'Phase C Power Factor' + name: Phase C Power Factor apparent_power: - name: 'Phase C Apparent Power' + name: Phase C Apparent Power reactive_power: - name: 'Phase C Reactive Power' + name: Phase C Reactive Power phase_angle: - name: 'Phase C Phase Angle' + name: Phase C Phase Angle frequency: - name: 'Frequency' + name: Frequency import_active_energy: - name: 'Import Active Energy' + name: Import Active Energy export_active_energy: - name: 'Export Active Energy' + name: Export Active Energy import_reactive_energy: - name: 'Import Reactive Energy' + name: Import Reactive Energy export_reactive_energy: - name: 'Export Reactive Energy' + name: Export Reactive Energy - platform: dsmr energy_delivered_tariff1: name: dsmr_energy_delivered_tariff1 - platform: nextion id: testnumber - name: 'testnumber' + name: testnumber variable_name: testnumber - platform: nextion id: testwave - name: 'testwave' + name: testwave component_id: 2 wave_channel_id: 1 - - platform: mlx90393 - oversampling: 1 - filter: 0 - gain: "3X" - x_axis: - name: "mlxxaxis" - y_axis: - name: "mlxyaxis" - z_axis: - name: "mlxzaxis" - resolution: 17BIT - temperature: - name: "mlxtemp" - oversampling: 2 - platform: smt100 - uart_id: uart10 + uart_id: uart_10 counts: - name: "Counts" + name: Counts dielectric_constant: - name: "Dielectric Constant" + name: Dielectric Constant temperature: - name: "Temperature" + name: Temperature moisture: - name: "Moisture" + name: Moisture voltage: - name: "Voltage" + name: Voltage update_interval: 60s + + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature + time: - platform: homeassistant -apds9960: - address: 0x20 - update_interval: 60s - -mpr121: - id: mpr121_first - address: 0x5A - binary_sensor: - platform: daly_bms charging_mos_enabled: - name: "Charging MOS" + name: Charging MOS discharging_mos_enabled: - name: "Discharging MOS" - - platform: apds9960 - direction: up - name: APDS9960 Up - device_class: motion - filters: - - invert - - delayed_on: 20ms - - delayed_off: 20ms - - lambda: 'return false;' - on_state: - - logger.log: New state - id: my_binary_sensor - - platform: apds9960 - direction: down - name: APDS9960 Down - - platform: apds9960 - direction: left - name: APDS9960 Left - - platform: apds9960 - direction: right - name: APDS9960 Right + name: Discharging MOS + - platform: homeassistant entity_id: binary_sensor.hello_world id: ha_hello_world_binary - - platform: mpr121 - id: touchkey0 - channel: 0 - name: 'touchkey0' - - platform: mpr121 - channel: 1 - name: 'touchkey1' - id: bin1 - - platform: mpr121 - channel: 2 - name: 'touchkey2' - id: bin2 - - platform: mpr121 - channel: 3 - name: 'touchkey3' - id: bin3 - on_press: - then: - - switch.toggle: mpr121_toggle - - platform: ttp229_lsf - channel: 1 - name: TTP229 LSF Test - - platform: ttp229_bsf - channel: 1 - name: TTP229 BSF Test + - platform: fingerprint_grow - name: "Fingerprint Enrolling" - - platform: custom - lambda: |- - auto s = new CustomBinarySensor(); - App.register_component(s); - return {s}; - binary_sensors: - - id: custom_binary_sensor - name: Custom Binary Sensor + name: Fingerprint Enrolling - platform: nextion page_id: 0 component_id: 2 - name: 'Nextion Component 2 Touch' + name: Nextion Component 2 Touch - platform: nextion id: r0_sensor - name: 'R0 Sensor' + name: R0 Sensor component_name: page0.r0 - - platform: template - id: 'cover_toggle' - on_press: - then: - - cover.toggle: time_based_cover - - cover.toggle: endstop_cover + - platform: hydreon_rgxx - hydreon_rgxx_id: "hydreon_rg9" + hydreon_rgxx_id: hydreon_rg9 too_cold: - name: "rg9_toocold" + name: rg9_toocold em_sat: - name: "rg9_emsat" + name: rg9_emsat lens_bad: - name: "rg9_lens_bad" + name: rg9_lens_bad - platform: template - id: 'pzemac_reset_energy' + id: pzemac_reset_energy on_press: then: - pzemac.reset_energy: pzemac1 + - platform: template + id: pzemdc_reset_energy + on_press: + then: + - pzemdc.reset_energy: pzemdc1 + + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + + - platform: gpio + id: bin1 + pin: 1 + - platform: gpio + id: bin2 + pin: 2 + - platform: gpio + id: bin3 + pin: 3 globals: - id: my_global_string @@ -891,14 +761,16 @@ status_led: text_sensor: - platform: daly_bms status: - name: "BMS Status" + name: BMS Status - platform: version - name: 'ESPHome Version' + name: ESPHome Version icon: mdi:icon id: version_sensor on_value: + # yamllint disable rule:line-length - lambda: !lambda |- ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); + # yamllint enable rule:line-length - script.execute: my_script - script.wait: my_script - script.stop: my_script @@ -912,20 +784,12 @@ text_sensor: my_variable: |- return id(version_sensor).state; - platform: template - name: 'Template Text Sensor' + name: Template Text Sensor lambda: |- return {"Hello World"}; - platform: homeassistant entity_id: sensor.hello_world2 id: ha_hello_world2 - - platform: custom - lambda: |- - auto s = new CustomTextSensor(); - App.register_component(s); - return {s}; - text_sensors: - - id: custom_text_sensor - name: Custom Text Sensor - platform: nextion name: text0 id: text0 @@ -933,96 +797,31 @@ text_sensor: component_name: text0 - platform: dsmr identification: - name: "dsmr_identification" + name: dsmr_identification p1_version: - name: "dsmr_p1_version" + name: dsmr_p1_version script: - id: my_script then: - lambda: 'ESP_LOGD("main", "Hello World!");' -sm2135: - data_pin: GPIO12 - clock_pin: GPIO14 - switch: - - platform: template - name: 'mpr121_toggle' - id: mpr121_toggle - optimistic: True - platform: gpio id: gpio_switch1 - pin: - mcp23xxx: mcp23017_hub - number: 0 - mode: OUTPUT - interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] + pin: 1 - platform: gpio id: gpio_switch2 - pin: - mcp23xxx: mcp23008_hub - number: 0 - mode: OUTPUT - interlock: *interlock + pin: 2 - platform: gpio id: gpio_switch3 - pin: GPIO1 - interlock: *interlock - - platform: custom - lambda: |- - auto s = new CustomSwitch(); - return {s}; - switches: - - id: custom_switch - name: Custom Switch + pin: 3 + - platform: nextion id: r0 - name: 'R0 Switch' + name: R0 Switch component_name: page0.r0 -custom_component: - lambda: |- - auto s = new CustomComponent(); - s->set_update_interval(15000); - return {s}; - -stepper: - - platform: uln2003 - id: my_stepper - pin_a: GPIO12 - pin_b: GPIO13 - pin_c: GPIO14 - pin_d: GPIO15 - sleep_when_done: no - step_mode: HALF_STEP - max_speed: 250 steps/s - acceleration: inf - deceleration: inf - - platform: a4988 - id: my_stepper2 - step_pin: GPIO1 - dir_pin: GPIO2 - max_speed: 0.1 steps/s - acceleration: 10 steps/s^2 - deceleration: 10 steps/s^2 - -interval: - interval: 5s - then: - - logger.log: 'Interval Run' - - stepper.set_target: - id: my_stepper2 - target: 500 - - stepper.set_target: - id: my_stepper - target: !lambda 'return 0;' - - stepper.report_position: - id: my_stepper2 - position: 0 - - stepper.report_position: - id: my_stepper - position: !lambda 'return 50/100.0;' climate: - platform: bang_bang @@ -1042,8 +841,13 @@ climate: - platform: thermostat name: Thermostat Climate sensor: ha_hello_world - default_target_temperature_low: 18°C - default_target_temperature_high: 24°C + preset: + - name: Default Preset + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C idle_action: - switch.turn_on: gpio_switch1 cool_action: @@ -1088,14 +892,16 @@ climate: - switch.turn_on: gpio_switch1 fan_mode_diffuse_action: - switch.turn_on: gpio_switch2 + fan_mode_quiet_action: + - switch.turn_on: gpio_switch1 swing_off_action: - - switch.turn_on: gpio_switch1 + - switch.turn_on: gpio_switch2 swing_horizontal_action: - - switch.turn_on: gpio_switch2 - swing_vertical_action: - switch.turn_on: gpio_switch1 - swing_both_action: + swing_vertical_action: - switch.turn_on: gpio_switch2 + swing_both_action: + - switch.turn_on: gpio_switch1 startup_delay: true supplemental_cooling_delta: 2.0 cool_deadband: 0.5 @@ -1118,12 +924,9 @@ climate: fan_only_cooling: true fan_with_cooling: true fan_with_heating: true - away_config: - default_target_temperature_low: 16°C - default_target_temperature_high: 20°C - platform: pid id: pid_climate - name: 'PID Climate Controller' + name: PID Climate Controller sensor: ha_hello_world default_target_temperature: 21°C heat_output: my_slow_pwm @@ -1131,128 +934,67 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 + max_integral: 0.0 + output_averaging_samples: 1 + derivative_averaging_samples: 1 + deadband_parameters: + threshold_high: 0.4 + threshold_low: -2.0 + kp_multiplier: 0.0 + ki_multiplier: 0.0 + kd_multiplier: 0.0 + deadband_output_averaging_samples: 1 + - platform: haier + name: Haier AC + supported_swing_modes: + - vertical + - horizontal + - both + update_interval: 10s + uart_id: uart_12 sprinkler: - id: yard_sprinkler_ctrlr - main_switch: "Yard Sprinklers" - auto_advance_switch: "Yard Sprinklers Auto Advance" - reverse_switch: "Yard Sprinklers Reverse" + main_switch: Yard Sprinklers + auto_advance_switch: Yard Sprinklers Auto Advance + reverse_switch: Yard Sprinklers Reverse pump_start_pump_delay: 2s pump_stop_valve_delay: 4s pump_switch_off_during_valve_open_delay: true valve_open_delay: 5s valves: - - valve_switch: "Yard Valve 0" - enable_switch: "Enable Yard Valve 0" + - valve_switch: Yard Valve 0 + enable_switch: Enable Yard Valve 0 pump_switch_id: gpio_switch1 run_duration: 10s valve_switch_id: gpio_switch2 - - valve_switch: "Yard Valve 1" - enable_switch: "Enable Yard Valve 1" + - valve_switch: Yard Valve 1 + enable_switch: Enable Yard Valve 1 pump_switch_id: gpio_switch1 run_duration: 10s valve_switch_id: gpio_switch2 - - valve_switch: "Yard Valve 2" - enable_switch: "Enable Yard Valve 2" + - valve_switch: Yard Valve 2 + enable_switch: Enable Yard Valve 2 pump_switch_id: gpio_switch1 run_duration: 10s valve_switch_id: gpio_switch2 - id: garden_sprinkler_ctrlr - main_switch: "Garden Sprinklers" - auto_advance_switch: "Garden Sprinklers Auto Advance" - reverse_switch: "Garden Sprinklers Reverse" + main_switch: Garden Sprinklers + auto_advance_switch: Garden Sprinklers Auto Advance + reverse_switch: Garden Sprinklers Reverse valve_overlap: 5s valves: - - valve_switch: "Garden Valve 0" - enable_switch: "Enable Garden Valve 0" + - valve_switch: Garden Valve 0 + enable_switch: Enable Garden Valve 0 pump_switch_id: gpio_switch1 run_duration: 10s valve_switch_id: gpio_switch2 - - valve_switch: "Garden Valve 1" - enable_switch: "Enable Garden Valve 1" + - valve_switch: Garden Valve 1 + enable_switch: Enable Garden Valve 1 pump_switch_id: gpio_switch1 run_duration: 10s valve_switch_id: gpio_switch2 - -cover: - - platform: endstop - name: Endstop Cover - id: endstop_cover - stop_action: - - switch.turn_on: gpio_switch1 - open_endstop: my_binary_sensor - open_action: - - switch.turn_on: gpio_switch1 - open_duration: 5min - close_endstop: my_binary_sensor - close_action: - - switch.turn_on: gpio_switch2 - - output.set_level: - id: out - level: 50% - - output.esp8266_pwm.set_frequency: - id: out - frequency: 500.0Hz - - output.esp8266_pwm.set_frequency: - id: out - frequency: !lambda 'return 500.0;' - - servo.write: - id: my_servo - level: -100% - - servo.write: - id: my_servo - level: !lambda 'return -1.0;' - - delay: 2s - - servo.detach: my_servo - close_duration: 4.5min - max_duration: 10min - - platform: time_based - name: Time Based Cover - id: time_based_cover - stop_action: - - switch.turn_on: gpio_switch1 - open_action: - - switch.turn_on: gpio_switch1 - open_duration: 5min - close_action: - - switch.turn_on: gpio_switch2 - close_duration: 4.5min - - platform: current_based - name: "Current Based Cover" - open_sensor: ade7953_current_a - open_moving_current_threshold: 0.5 - open_obstacle_current_threshold: 0.8 - open_duration: 12s - open_action: - - switch.turn_on: gpio_switch1 - close_sensor: ade7953_current_b - close_moving_current_threshold: 0.5 - close_obstacle_current_threshold: 0.8 - close_duration: 10s - close_action: - - switch.turn_on: gpio_switch2 - stop_action: - - switch.turn_off: gpio_switch1 - - switch.turn_off: gpio_switch2 - obstacle_rollback: 30% - start_sensing_delay: 0.8s - malfunction_detection: true - malfunction_action: - then: - - logger.log: "Malfunction Detected" - - platform: template - name: Template Cover with Tilt - tilt_lambda: 'return 0.5;' - tilt_action: - - output.set_level: - id: out - level: !lambda 'return tilt;' - position_action: - - output.set_level: - id: out - level: !lambda 'return pos;' - output: - platform: esp8266_pwm id: out @@ -1261,48 +1003,11 @@ output: - platform: esp8266_pwm id: out2 pin: D4 - - platform: custom - type: binary - lambda: |- - auto s = new CustomBinaryOutput(); - App.register_component(s); - return {s}; - outputs: - - id: custom_binary - - platform: custom - type: float - lambda: |- - auto s = new CustomFloatOutput(); - App.register_component(s); - return {s}; - outputs: - - id: custom_float - platform: slow_pwm pin: GPIO5 id: my_slow_pwm period: 15s restart_cycle_on_state_change: false - - platform: sm2135 - id: sm2135_0 - channel: 0 - - platform: sm2135 - id: sm2135_1 - channel: 1 - - platform: sm2135 - id: sm2135_2 - channel: 2 - - platform: sm2135 - id: sm2135_3 - channel: 3 - - platform: sm2135 - id: sm2135_4 - channel: 4 - -mcp23017: - id: mcp23017_hub - -mcp23008: - id: mcp23008_hub e131: @@ -1317,7 +1022,7 @@ light: effects: - wled: - adalight: - uart_id: uart3 + uart_id: uart_3 - e131: universe: 1 - platform: hbridge @@ -1325,54 +1030,52 @@ light: pin_a: out pin_b: out2 - platform: sonoff_d1 - uart_id: uart2 - use_rm433_remote: False + uart_id: uart_2 + use_rm433_remote: false name: Sonoff D1 Dimmer id: d1_light restore_mode: RESTORE_DEFAULT_OFF - -servo: - id: my_servo - output: out - restore: true - min_level: $min_sub - max_level: $max_sub - -ttp229_lsf: - -ttp229_bsf: - sdo_pin: D2 - scl_pin: D1 + - platform: shelly_dimmer + name: "Shelly Dimmer Light" + power: + name: "Shelly Dimmer Power" + voltage: + name: "Shelly Dimmer Voltage" + current: + name: "Shelly Dimmer Current" + max_brightness: 500 + firmware: "51.6" + uart_id: uart_11 sim800l: - uart_id: uart4 + uart_id: uart_4 on_sms_received: - lambda: |- std::string str; str = sender; str = message; - sim800l.send_sms: - message: 'hello you' - recipient: '+1234' + message: hello you + recipient: "+1234" - sim800l.dial: - recipient: '+1234' + recipient: "+1234" dfplayer: - uart_id: uart5 + uart_id: uart_5 on_finished_playback: then: if: condition: not: dfplayer.is_playing then: - logger.log: 'Playback finished event' + logger.log: Playback finished event tm1651: id: tm1651_battery clk_pin: D6 dio_pin: D5 rf_bridge: - uart_id: uart5 + uart_id: uart_5 on_code_received: - lambda: |- uint32_t test; @@ -1394,47 +1097,20 @@ rf_bridge: test = data.length; test = data.protocol; test_code = data.code; - - rf_bridge.start_advanced_sniffing - - rf_bridge.stop_advanced_sniffing + - rf_bridge.start_advanced_sniffing: + - rf_bridge.stop_advanced_sniffing: - rf_bridge.send_advanced_code: length: 0x04 protocol: 0x01 - code: 'ABC123' + code: "ABC123" - rf_bridge.send_raw: - raw: 'AAA5070008001000ABC12355' - - http_request.get: - url: https://esphome.io - headers: - Content-Type: application/json - verify_ssl: false - - http_request.post: - url: https://esphome.io - verify_ssl: false - json: - key: !lambda |- - return id(version_sensor).state; - greeting: 'Hello World' - - http_request.send: - method: PUT - url: https://esphome.io - headers: - Content-Type: application/json - body: 'Some data' - verify_ssl: false + raw: "AAA5070008001000ABC12355" + display: - - platform: max7219digit - cs_pin: GPIO15 - num_chips: 4 - rotate_chip: 0 - intensity: 10 - scroll_mode: 'STOP' - id: my_matrix - lambda: |- - it.printdigit("hello"); - platform: nextion - uart_id: uart1 - tft_url: 'http://esphome.io/default35.tft' + uart_id: uart_1 + tft_url: http://esphome.io/default35.tft update_interval: 5s on_sleep: then: @@ -1449,11 +1125,6 @@ display: then: lambda: 'ESP_LOGD("display","Display shows new page %u", x);' - -http_request: - useragent: esphome/device - timeout: 10s - fingerprint_grow: sensing_pin: 4 password: 0x12FE37DC @@ -1462,8 +1133,8 @@ fingerprint_grow: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_finger_scan_matched data: - finger_id: !lambda 'return finger_id;' - confidence: !lambda 'return confidence;' + finger_id: !lambda "return finger_id;" + confidence: !lambda "return confidence;" on_finger_scan_unmatched: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched @@ -1471,23 +1142,23 @@ fingerprint_grow: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_enrollment_scan data: - finger_id: !lambda 'return finger_id;' - scan_num: !lambda 'return scan_num;' + finger_id: !lambda "return finger_id;" + scan_num: !lambda "return scan_num;" on_enrollment_done: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_node_enrollment_done data: - finger_id: !lambda 'return finger_id;' + finger_id: !lambda "return finger_id;" on_enrollment_failed: - homeassistant.event: event: esphome.${device_name}_fingerprint_grow_enrollment_failed data: - finger_id: !lambda 'return finger_id;' - uart_id: uart6 + finger_id: !lambda "return finger_id;" + uart_id: uart_6 dsmr: decryption_key: 00112233445566778899aabbccddeeff - uart_id: uart6 + uart_id: uart_6 max_telegram_length: 1000 request_pin: D5 request_interval: 20s @@ -1495,24 +1166,8 @@ dsmr: daly_bms: update_interval: 20s - uart_id: uart1 + uart_id: uart_1 qr_code: - id: homepage_qr value: https://esphome.io/index.html - -button: - - platform: output - id: output_button - output: out - duration: 100ms - - platform: wake_on_lan - target_mac_address: 12:34:56:78:90:ab - name: wol_test_1 - id: wol_1 - -cd74hc4067: - pin_s0: GPIO12 - pin_s1: GPIO13 - pin_s2: GPIO14 - pin_s3: GPIO15 diff --git a/tests/test4.yaml b/tests/test4.yaml index c3079037c1..7b8f139a43 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -1,3 +1,4 @@ +--- esphome: name: $devicename platform: ESP32 @@ -25,7 +26,7 @@ api: i2c: sda: 21 scl: 22 - scan: False + scan: false spi: clk_pin: GPIO21 @@ -38,7 +39,7 @@ uart: baud_rate: 115200 ota: - safe_mode: True + safe_mode: true port: 3286 logger: @@ -71,7 +72,7 @@ select: 2: Both pipsolar: - id: inverter0 + id: inverter0 sx1509: - id: sx1509_hub @@ -81,9 +82,9 @@ mcp3204: cs_pin: GPIO23 dac7678: - address: 0x4A - id: dac7678_hub1 - internal_reference: true + address: 0x4A + id: dac7678_hub1 + internal_reference: true sensor: - platform: homeassistant @@ -226,22 +227,36 @@ sensor: pv_charging_power: id: inverter0_pv_charging_power name: inverter0_pv_charging_power - - platform: "hrxl_maxsonar_wr" - name: "Rainwater Tank Level" + - platform: hrxl_maxsonar_wr + name: Rainwater Tank Level filters: - sliding_window_moving_average: window_size: 12 send_every: 12 - or: - - throttle: "20min" - - delta: 0.02 + - throttle: 20min + - delta: 0.02 - platform: mcp3204 - name: "MCP3204 Pin 1" + name: MCP3204 Pin 1 number: 1 id: mcp_sensor - platform: copy source_id: mcp_sensor - name: "MCP binary sensor copy" + name: MCP binary sensor copy + - platform: ufire_ec + id: ufire_ec_board + temperature: + name: Ufire Temperature + ec: + name: Ufire EC + temperature_compensation: 20.0 + temperature_coefficient: 0.019 + - platform: ufire_ise + id: ufire_ise_board + temperature: + name: Ufire Temperature + ph: + name: Ufire pH # # platform sensor.apds9960 requires component apds9960 @@ -321,27 +336,28 @@ binary_sensor: name: inverter0_backlight_on - platform: template id: ar1 - lambda: 'return {};' + lambda: "return {};" filters: - autorepeat: - - delay: 2s - time_off: 100ms - time_on: 900ms - - delay: 4s - time_off: 100ms - time_on: 400ms + - delay: 2s + time_off: 100ms + time_on: 900ms + - delay: 4s + time_off: 100ms + time_on: 400ms on_state: then: - lambda: 'ESP_LOGI("ar1:", "%d", x);' - - platform: xpt2046 - xpt2046_id: xpt_touchscreen + - platform: touchscreen + touchscreen_id: xpt_touchscreen id: touch_key0 x_min: 80 x_max: 160 y_min: 106 y_max: 212 - on_state: - - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));' + on_press: + - logger.log: Touched + - platform: gpio name: GPIO SX1509 test pin: @@ -356,8 +372,7 @@ binary_sensor: y_min: 0 y_max: 100 on_press: - - logger.log: "Touched" - + - logger.log: Touched climate: - platform: tuya @@ -366,6 +381,7 @@ climate: target_temperature_datapoint: 3 current_temperature_multiplier: 0.5 target_temperature_multiplier: 0.5 + reports_fahrenheit: true switch: - platform: tuya @@ -392,7 +408,7 @@ switch: light: - platform: fastled_clockless id: led_matrix_32x8 - name: "led_matrix_32x8" + name: led_matrix_32x8 chipset: WS2812B pin: GPIO15 num_leds: 256 @@ -417,7 +433,7 @@ cover: position_datapoint: 2 - platform: copy source_id: tuya_cover - name: "Tuya Cover copy" + name: Tuya Cover copy display: - platform: addressable_light @@ -516,7 +532,10 @@ text_sensor: name: inverter0_last_qflag - platform: copy source_id: inverter0_device_mode - name: "Inverter Text Sensor Copy" + name: Inverter Text Sensor Copy + - platform: ethernet_info + ip_address: + name: IP Address output: - platform: pipsolar @@ -524,37 +543,37 @@ output: battery_recharge_voltage: id: inverter0_battery_recharge_voltage_out - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 0 - id: 'dac7678_1_ch0' + id: dac7678_1_ch0 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 1 - id: 'dac7678_1_ch1' + id: dac7678_1_ch1 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 2 - id: 'dac7678_1_ch2' + id: dac7678_1_ch2 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 3 - id: 'dac7678_1_ch3' + id: dac7678_1_ch3 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 4 - id: 'dac7678_1_ch4' + id: dac7678_1_ch4 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 5 - id: 'dac7678_1_ch5' + id: dac7678_1_ch5 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 6 - id: 'dac7678_1_ch6' + id: dac7678_1_ch6 - platform: dac7678 - dac7678_id: 'dac7678_hub1' + dac7678_id: dac7678_hub1 channel: 7 - id: 'dac7678_1_ch7' + id: dac7678_1_ch7 esp32_camera: name: ESP-32 Camera data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] @@ -581,34 +600,9 @@ esp32_camera_web_server: external_components: - source: github://esphome/esphome@dev refresh: 1d - components: ["bh1750"] + components: [bh1750] - source: ../esphome/components - components: ["sntp"] -xpt2046: - id: xpt_touchscreen - cs_pin: 17 - irq_pin: 16 - update_interval: 50ms - report_interval: 1s - threshold: 400 - dimension_x: 240 - dimension_y: 320 - calibration_x_min: 3860 - calibration_x_max: 280 - calibration_y_min: 340 - calibration_y_max: 3860 - swap_x_y: False - on_state: - - lambda: |- - ESP_LOGI("main", "args x=%d, y=%d, touched=%s", x, y, (touched ? "touch" : "release")); - ESP_LOGI("main", "member x=%d, y=%d, touched=%d, x_raw=%d, y_raw=%d, z_raw=%d", - id(xpt_touchscreen).x, - id(xpt_touchscreen).y, - (int) id(xpt_touchscreen).touched, - id(xpt_touchscreen).x_raw, - id(xpt_touchscreen).y_raw, - id(xpt_touchscreen).z_raw - ); + components: [sntp] button: - platform: restart @@ -622,7 +616,6 @@ button: source_id: shutdown_btn name: Shutdown Button Copy - touchscreen: - platform: ektf2232 interrupt_pin: GPIO36 @@ -631,7 +624,25 @@ touchscreen: on_touch: - logger.log: format: Touch at (%d, %d) - args: ["touch.x", "touch.y"] + args: [touch.x, touch.y] + + - platform: xpt2046 + id: xpt_touchscreen + cs_pin: 17 + interrupt_pin: 16 + display: inkplate_display + update_interval: 50ms + report_interval: 1s + threshold: 400 + calibration_x_min: 3860 + calibration_x_max: 280 + calibration_y_min: 340 + calibration_y_max: 3860 + swap_x_y: false + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] - platform: lilygo_t5_47 id: lilygo_touchscreen @@ -640,15 +651,17 @@ touchscreen: on_touch: - logger.log: format: Touch at (%d, %d) - args: ["touch.x", "touch.y"] + args: [touch.x, touch.y] + +i2s_audio: + i2s_lrclk_pin: GPIO26 + i2s_bclk_pin: GPIO27 media_player: - platform: i2s_audio - name: ${friendly_name} + name: None dac_type: external - i2s_lrclk_pin: GPIO26 i2s_dout_pin: GPIO25 - i2s_bclk_pin: GPIO27 mute_pin: GPIO14 on_state: - media_player.play: @@ -673,4 +686,33 @@ prometheus: relabel: ha_hello_world: id: hellow_world - name: "Hello World" + name: Hello World + +microphone: + - platform: i2s_audio + id: mic_id + i2s_din_pin: GPIO23 + + +voice_assistant: + microphone: mic_id + on_start: + - logger.log: "Voice assistant started" + on_stt_end: + - logger.log: + format: "Voice assistant STT ended with result %s" + args: [x.c_str()] + on_tts_start: + - logger.log: + format: "Voice assistant TTS started with text %s" + args: [x.c_str()] + on_tts_end: + - logger.log: + format: "Voice assistant TTS ended with url %s" + args: [x.c_str()] + on_end: + - logger.log: "Voice assistant ended" + on_error: + - logger.log: + format: "Voice assistant error - code %s, message: %s" + args: [code.c_str(), message.c_str()] diff --git a/tests/test5.yaml b/tests/test5.yaml index c193009ead..0d044ac241 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -1,3 +1,4 @@ +--- esphome: name: test5 build_path: build/test5 @@ -28,11 +29,11 @@ ota: logger: uart: - - id: uart1 + - id: uart_1 tx_pin: 1 rx_pin: 3 baud_rate: 9600 - - id: uart2 + - id: uart_2 tx_pin: 17 rx_pin: 16 baud_rate: 19200 @@ -41,7 +42,7 @@ i2c: frequency: 100khz modbus: - uart_id: uart1 + uart_id: uart_1 flow_control_pin: 5 id: mod_bus1 @@ -60,8 +61,13 @@ mqtt: topic: testing/sensor/testing_sensor/state qos: 0 then: + # yamllint disable rule:line-length - lambda: |- - ESP_LOGD("Mqtt Test","testing/sensor/testing_sensor/state=[%s]",x.c_str()); + ESP_LOGD("Mqtt Test", "testing/sensor/testing_sensor/state=[%s]", x.c_str()); + # yamllint enable rule:line-length + +vbus: + - uart_id: uart_2 binary_sensor: - platform: gpio @@ -74,8 +80,128 @@ binary_sensor: id: modbus_binsensortest register_type: read address: 0x3200 - bitmask: 0x80 #(bit 8) - lambda: !lambda "{ return x ;}" + bitmask: 0x80 # (bit 8) + lambda: "return x;" + + - platform: tm1638 + id: Button0 + key: 0 + filters: + - delayed_on: 10ms + on_press: + then: + - switch.turn_on: Led0 + on_release: + then: + - switch.turn_off: Led0 + + - platform: tm1638 + id: Button1 + key: 1 + on_press: + then: + - switch.turn_on: Led1 + on_release: + then: + - switch.turn_off: Led1 + + - platform: tm1638 + id: Button2 + key: 2 + on_press: + then: + - switch.turn_on: Led2 + on_release: + then: + - switch.turn_off: Led2 + + - platform: tm1638 + id: Button3 + key: 3 + on_press: + then: + - switch.turn_on: Led3 + on_release: + then: + - switch.turn_off: Led3 + + - platform: tm1638 + id: Button4 + key: 4 + on_press: + then: + - output.turn_on: Led4 + on_release: + then: + - output.turn_off: Led4 + + - platform: tm1638 + id: Button5 + key: 5 + on_press: + then: + - output.turn_on: Led5 + on_release: + then: + - output.turn_off: Led5 + + - platform: tm1638 + id: Button6 + key: 6 + on_press: + then: + - output.turn_on: Led6 + on_release: + then: + - output.turn_off: Led6 + + - platform: tm1638 + id: Button7 + key: 7 + on_press: + then: + - output.turn_on: Led7 + on_release: + then: + - output.turn_off: Led7 + + - platform: gpio + id: sn74hc165_pin_0 + pin: + sn74hc165: sn74hc165_hub + number: 0 + + - platform: ezo_pmp + pump_state: + name: "Pump State" + is_paused: + name: "Is Paused" + + - platform: matrix_keypad + keypad_id: keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + + - platform: vbus + model: deltasol_bs_plus + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; tlc5947: data_pin: GPIO12 @@ -103,19 +229,39 @@ output: address: 0x9001 value_type: U_WORD + - platform: tm1638 + id: Led4 + led: 4 + + - platform: tm1638 + id: Led5 + led: 5 + + - platform: tm1638 + id: Led6 + led: 6 + + - platform: tm1638 + id: Led7 + led: 7 + demo: esp32_ble: esp32_ble_server: - manufacturer: "ESPHome" - model: "Test5" + manufacturer: ESPHome + model: Test5 esp32_improv: authorizer: io0_button authorized_duration: 1min status_indicator: built_in_led +ezo_pmp: + id: hcl_pump + update_interval: 1s + number: - platform: template name: My template number @@ -126,14 +272,15 @@ number: step: 5 unit_of_measurement: "%" mode: slider + device_class: humidity on_value: - logger.log: - format: "Number changed to %f" - args: ["x"] + format: Number changed to %f + args: [x] set_action: - logger.log: - format: "Template Number set to %f" - args: ["x"] + format: Template Number set to %f + args: [x] - number.set: id: template_number_id value: 50 @@ -163,10 +310,10 @@ number: - id: modbus_numbertest platform: modbus_controller modbus_controller_id: modbus_controller_test - name: "ModbusNumber" + name: ModbusNumber address: 0x9002 value_type: U_WORD - lambda: "return x * 1.0; " + lambda: "return x * 1.0;" write_lambda: |- return x * 1.0 ; multiply: 1.0 @@ -180,11 +327,11 @@ select: restore_value: true on_value: - logger.log: - format: "Select changed to %s (index %d)" + format: Select changed to %s (index %d)" args: ["x.c_str()", "i"] set_action: - logger.log: - format: "Template Select set to %s" + format: Template Select set to %s args: ["x.c_str()"] - select.set: id: template_select_id @@ -216,7 +363,7 @@ select: - three - platform: modbus_controller - name: "Modbus Select Register 1000" + name: Modbus Select Register 1000 address: 1000 value_type: U_WORD optionsmap: @@ -226,43 +373,45 @@ select: "Three": 3 sensor: + - platform: internal_temperature + name: Internal Temperature - platform: selec_meter total_active_energy: - name: "SelecEM2M Total Active Energy" + name: SelecEM2M Total Active Energy import_active_energy: - name: "SelecEM2M Import Active Energy" + name: SelecEM2M Import Active Energy export_active_energy: - name: "SelecEM2M Export Active Energy" + name: SelecEM2M Export Active Energy total_reactive_energy: - name: "SelecEM2M Total Reactive Energy" + name: SelecEM2M Total Reactive Energy import_reactive_energy: - name: "SelecEM2M Import Reactive Energy" + name: SelecEM2M Import Reactive Energy export_reactive_energy: - name: "SelecEM2M Export Reactive Energy" + name: SelecEM2M Export Reactive Energy apparent_energy: - name: "SelecEM2M Apparent Energy" + name: SelecEM2M Apparent Energy active_power: - name: "SelecEM2M Active Power" + name: SelecEM2M Active Power reactive_power: - name: "SelecEM2M Reactive Power" + name: SelecEM2M Reactive Power apparent_power: - name: "SelecEM2M Apparent Power" + name: SelecEM2M Apparent Power voltage: - name: "SelecEM2M Voltage" + name: SelecEM2M Voltage current: - name: "SelecEM2M Current" + name: SelecEM2M Current power_factor: - name: "SelecEM2M Power Factor" + name: SelecEM2M Power Factor frequency: - name: "SelecEM2M Frequency" + name: SelecEM2M Frequency maximum_demand_active_power: - name: "SelecEM2M Maximum Demand Active Power" + name: SelecEM2M Maximum Demand Active Power disabled_by_default: true maximum_demand_reactive_power: - name: "SelecEM2M Maximum Demand Reactive Power" + name: SelecEM2M Maximum Demand Reactive Power disabled_by_default: true maximum_demand_apparent_power: - name: "SelecEM2M Maximum Demand Apparent Power" + name: SelecEM2M Maximum Demand Apparent Power disabled_by_default: true - id: modbus_sensortest @@ -273,47 +422,47 @@ sensor: value_type: U_WORD - platform: t6615 - uart_id: uart2 + uart_id: uart_2 co2: name: CO2 Sensor - platform: bmp3xx temperature: - name: "BMP Temperature" + name: BMP Temperature oversampling: 16x pressure: - name: "BMP Pressure" + name: BMP Pressure address: 0x77 iir_filter: 2X - platform: sen5x id: sen54 temperature: - name: "Temperature" + name: Temperature accuracy_decimals: 1 humidity: - name: "Humidity" + name: Humidity accuracy_decimals: 0 pm_1_0: - name: " PM <1µm Weight concentration" + name: PM <1µm Weight concentration id: pm_1_0 accuracy_decimals: 1 pm_2_5: - name: " PM <2.5µm Weight concentration" + name: PM <2.5µm Weight concentration id: pm_2_5 accuracy_decimals: 1 pm_4_0: - name: " PM <4µm Weight concentration" + name: PM <4µm Weight concentration id: pm_4_0 accuracy_decimals: 1 pm_10_0: - name: " PM <10µm Weight concentration" + name: PM <10µm Weight concentration id: pm_10_0 accuracy_decimals: 1 nox: - name: "NOx" + name: NOx voc: - name: "VOC" + name: VOC algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 @@ -328,13 +477,53 @@ sensor: acceleration_mode: low store_baseline: true address: 0x69 - - platform: mcp9600 thermocouple_type: K hot_junction: - name: "Thermocouple Temperature" + name: Thermocouple Temperature cold_junction: - name: "Ambient Temperature" + name: Ambient Temperature + + - platform: ezo_pmp + current_volume_dosed: + name: Current Volume Dosed + total_volume_dosed: + name: Total Volume Dosed + absolute_total_volume_dosed: + name: Absolute Total Volume Dosed + pump_voltage: + name: Pump Voltage + last_volume_requested: + name: Last Volume Requested + max_flow_rate: + name: Max Flow Rate + + - platform: vbus + model: deltasol c + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time + + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + sensors: + - id: vcustom + name: VBus Custom Sensor + lambda: return x[0] / 10.0; + + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature script: - id: automation_test @@ -342,7 +531,7 @@ script: - repeat: count: 5 then: - - logger.log: "looping!" + - logger.log: looping! switch: - platform: modbus_controller @@ -351,3 +540,92 @@ switch: register_type: coil address: 2 bitmask: 1 + + - platform: tm1638 + id: Led0 + led: 0 + name: TM1638Led0 + + - platform: tm1638 + id: Led1 + led: 1 + name: TM1638Led1 + + - platform: tm1638 + id: Led2 + led: 2 + name: TM1638Led2 + + - platform: tm1638 + id: Led3 + led: 3 + name: TM1638Led3 + +display: + - platform: tm1638 + id: primarydisplay + stb_pin: 5 #TM1638 STB + clk_pin: 18 #TM1638 CLK + dio_pin: 23 #TM1638 DIO + update_interval: 5s + intensity: 5 + lambda: |- + it.print("81818181"); + +time: + - platform: pcf85063 + +text_sensor: + - platform: ezo_pmp + dosing_mode: + name: Dosing Mode + calibration_status: + name: Calibration Status + on_value: + - ezo_pmp.dose_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.dose_volume_over_time: + id: hcl_pump + volume: 10 + duration: 2 + - ezo_pmp.dose_with_constant_flow_rate: + id: hcl_pump + volume_per_minute: 10 + duration: 2 + - ezo_pmp.set_calibration_volume: + id: hcl_pump + volume: 10 + - ezo_pmp.find: hcl_pump + - ezo_pmp.dose_continuously: hcl_pump + - ezo_pmp.clear_total_volume_dosed: hcl_pump + - ezo_pmp.clear_calibration: hcl_pump + - ezo_pmp.pause_dosing: hcl_pump + - ezo_pmp.stop_dosing: hcl_pump + - ezo_pmp.arbitrary_command: + id: hcl_pump + command: D,? + +sn74hc165: + id: sn74hc165_hub + data_pin: GPIO12 + clock_pin: GPIO14 + load_pin: GPIO27 + clock_inhibit_pin: GPIO26 + sr_count: 4 + +matrix_keypad: + id: keypad + rows: + - pin: 21 + - pin: 19 + columns: + - pin: 17 + - pin: 16 + keys: "1234" + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 diff --git a/tests/test6.yaml b/tests/test6.yaml new file mode 100644 index 0000000000..7d4bd7bb19 --- /dev/null +++ b/tests/test6.yaml @@ -0,0 +1,42 @@ +--- +esphome: + name: test6 + project: + name: esphome.test6_project + version: "1.0.0" + +rp2040: + board: rpipicow + framework: + # Waiting for https://github.com/platformio/platform-raspberrypi/pull/36 + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git + +wifi: + networks: + - ssid: "MySSID" + password: "password1" + +api: + +ota: + +logger: + +binary_sensor: + - platform: gpio + pin: GPIO5 + id: pin_5_button + +output: + - platform: gpio + pin: GPIO4 + id: pin_4 + +switch: + - platform: output + output: pin_4 + id: pin_4_switch + +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/test7.yaml b/tests/test7.yaml new file mode 100644 index 0000000000..10e1b035ab --- /dev/null +++ b/tests/test7.yaml @@ -0,0 +1,33 @@ +# Tests for ESP32-C3 boards which use toolchain-riscv32-esp +--- +wifi: + ssid: 'ssid' + +esp32: + board: lolin_c3_mini + framework: + type: arduino + +esphome: + name: 'on-response-test' + on_boot: + then: + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: Some data + verify_ssl: false + on_response: + then: + - logger.log: + format: "Response status: %d" + args: + - status_code + +logger: + +http_request: + useragent: esphome/tagreader + timeout: 10s diff --git a/tests/test8.yaml b/tests/test8.yaml new file mode 100644 index 0000000000..1d3c8a31f4 --- /dev/null +++ b/tests/test8.yaml @@ -0,0 +1,27 @@ +# Tests for ESP32-S3 boards +--- +wifi: + ssid: "ssid" + +esp32: + board: esp32-c3-devkitm-1 + variant: ESP32S3 + framework: + type: arduino + +esphome: + name: esp32-s3-test + +logger: + +light: + - platform: neopixelbus + type: GRB + variant: WS2812 + pin: 33 + num_leds: 1 + id: neopixel + method: esp32_rmt + name: neopixel-enable + internal: false + restore_mode: ALWAYS_OFF diff --git a/tests/test_packages/test_packages_package1.yaml b/tests/test_packages/test_packages_package1.yaml index 0495984d42..312bbe574a 100644 --- a/tests/test_packages/test_packages_package1.yaml +++ b/tests/test_packages/test_packages_package1.yaml @@ -1,2 +1,3 @@ +--- sensor: - <<: !include ./test_uptime_sensor.yaml diff --git a/tests/test_packages/test_packages_package_wifi.yaml b/tests/test_packages/test_packages_package_wifi.yaml index 7d5d41ddab..a8c610edfd 100644 --- a/tests/test_packages/test_packages_package_wifi.yaml +++ b/tests/test_packages/test_packages_package_wifi.yaml @@ -1,4 +1,5 @@ +--- wifi: networks: - - ssid: 'WiFiFromPackage' - password: 'password1' + - ssid: "WiFiFromPackage" + password: "password1" diff --git a/tests/test_packages/test_uptime_sensor.yaml b/tests/test_packages/test_uptime_sensor.yaml index 1bf52a6d0b..f15d968fee 100644 --- a/tests/test_packages/test_uptime_sensor.yaml +++ b/tests/test_packages/test_uptime_sensor.yaml @@ -1,3 +1,4 @@ +--- # Uptime sensor. platform: uptime id: ${devicename}_uptime_pcg diff --git a/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml index ddd2156b5e..89879248aa 100644 --- a/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml +++ b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml @@ -1 +1,2 @@ +--- ${var1} diff --git a/tests/unit_tests/fixtures/yaml_util/includetest.yaml b/tests/unit_tests/fixtures/yaml_util/includetest.yaml index 959283df60..af0a4e2030 100644 --- a/tests/unit_tests/fixtures/yaml_util/includetest.yaml +++ b/tests/unit_tests/fixtures/yaml_util/includetest.yaml @@ -8,10 +8,11 @@ wifi: !include name: my_custom_ssid esphome: - # should be substituted as 'original', not overwritten by vars in the !include above + # should be substituted as 'original', + # not overwritten by vars in the !include above name: ${name} name_add_mac_suffix: true platform: esp8266 - board: !include { file: includes/scalar.yaml, vars: { var1: nodemcu } } + board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} - libraries: !include { file: includes/list.yaml, vars: { var1: Wire } } + libraries: !include {file: includes/list.yaml, vars: {var1: Wire}} diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py index 30768f9d56..20a3d190da 100644 --- a/tests/unit_tests/strategies.py +++ b/tests/unit_tests/strategies.py @@ -1,12 +1,9 @@ -from typing import Text - import hypothesis.strategies._internal.core as st from hypothesis.strategies._internal.strategies import SearchStrategy @st.defines_strategy(force_reusable_values=True) -def mac_addr_strings(): - # type: () -> SearchStrategy[Text] +def mac_addr_strings() -> SearchStrategy[str]: """A strategy for MAC address strings. This consists of six strings representing integers [0..255], diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index f883b8b44f..b98838024f 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -229,3 +229,37 @@ def test_file_compare(fixture_path, file1, file2, expected): actual = helpers.file_compare(path1, path2) assert actual == expected + + +@pytest.mark.parametrize( + "text, expected", + ( + ("foo", "foo"), + ("foo bar", "foo_bar"), + ("foo Bar", "foo_bar"), + ("foo BAR", "foo_bar"), + ("foo.bar", "foo.bar"), + ("fooBAR", "foobar"), + ("Foo-bar_EEK", "foo-bar_eek"), + (" foo", "__foo"), + ), +) +def test_snake_case(text, expected): + actual = helpers.snake_case(text) + + assert actual == expected + + +@pytest.mark.parametrize( + "text, expected", + ( + ("foo_bar", "foo_bar"), + ('!"§$%&/()=?foo_bar', "foo_bar"), + ('foo_!"§$%&/()=?bar', "foo_bar"), + ('foo_bar!"§$%&/()=?', "foo_bar"), + ), +) +def test_sanitize(text, expected): + actual = helpers.sanitize(text) + + assert actual == expected