diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be82fc826..b858b40e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/CODEOWNERS b/CODEOWNERS index 82aa071dc..3032e7dd8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -110,6 +110,7 @@ esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hte501/* @Stock-M esphome/components/hydreon_rgxx/* @functionpointer +esphome/components/hyt271/* @Philippe12 esphome/components/i2c/* @esphome/core esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz @@ -139,6 +140,7 @@ esphome/components/ltr390/* @sjtrny esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger +esphome/components/max6956/* @looping40 esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz @@ -188,6 +190,7 @@ esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core +esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @hwstar esphome/components/pcf85063/* @brogon esphome/components/pid/* @OttoWinter diff --git a/docker/Dockerfile b/docker/Dockerfile index a59a47039..720241242 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN \ python3-cryptography=3.3.2-1 \ python3-venv=3.9.2-3 \ iputils-ping=3:20210202-1 \ - git=1:2.30.2-1 \ + git=1:2.30.2-1+deb11u2 \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ && rm -rf \ @@ -63,7 +63,7 @@ RUN \ COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / RUN \ pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ - && /platformio_install_deps.py /platformio.ini + && /platformio_install_deps.py /platformio.ini --libraries # ======================= docker-type image ======================= diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 97a7d6fbf..068f74315 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -429,15 +429,16 @@ void APIServer::on_shutdown() { #ifdef USE_VOICE_ASSISTANT bool APIServer::start_voice_assistant() { - bool result = false; for (auto &c : this->clients_) { - result |= c->request_voice_assistant(true); + if (c->request_voice_assistant(true)) + return true; } - return result; + return false; } void APIServer::stop_voice_assistant() { for (auto &c : this->clients_) { - c->request_voice_assistant(false); + if (c->request_voice_assistant(false)) + return; } } #endif diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index c693544a2..1d127623f 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -18,5 +18,5 @@ async def to_code(config): # https://github.com/esphome/AsyncTCP/blob/master/library.json cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") elif CORE.is_esp8266: - # https://github.com/OttoWinter/ESPAsyncTCP - cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3") + # https://github.com/esphome/ESPAsyncTCP + cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3") diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 03e8f0b0b..8f70ad341 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -29,8 +29,35 @@ BLEClientConnectTrigger = ble_client_ns.class_( BLEClientDisconnectTrigger = ble_client_ns.class_( "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) ) +BLEClientPasskeyRequestTrigger = ble_client_ns.class_( + "BLEClientPasskeyRequestTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientPasskeyNotificationTrigger = ble_client_ns.class_( + "BLEClientPasskeyNotificationTrigger", + automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), +) +BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_( + "BLEClientNumericComparisonRequestTrigger", + automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), +) + # Actions BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) +BLEPasskeyReplyAction = ble_client_ns.class_( + "BLEClientPasskeyReplyAction", automation.Action +) +BLENumericComparisonReplyAction = ble_client_ns.class_( + "BLEClientNumericComparisonReplyAction", automation.Action +) +BLERemoveBondAction = ble_client_ns.class_( + "BLEClientRemoveBondAction", automation.Action +) + +CONF_PASSKEY = "passkey" +CONF_ACCEPT = "accept" +CONF_ON_PASSKEY_REQUEST = "on_passkey_request" +CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" +CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" # Espressif platformio framework is built with MAX_BLE_CONN to 3, so # enforce this in yaml checks. @@ -56,6 +83,29 @@ CONFIG_SCHEMA = ( ), } ), + cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientPasskeyRequestTrigger + ), + } + ), + cv.Optional(CONF_ON_PASSKEY_NOTIFICATION): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientPasskeyNotificationTrigger + ), + } + ), + cv.Optional( + CONF_ON_NUMERIC_COMPARISON_REQUEST + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientNumericComparisonRequestTrigger + ), + } + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -85,13 +135,34 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema( } ) +BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean), + } +) + +BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)), + } +) + + +BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + } +) + @automation.register_action( "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA ) async def ble_write_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) + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) value = config[CONF_VALUE] if cg.is_template(value): @@ -137,6 +208,54 @@ async def ble_write_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "ble_client.numeric_comparison_reply", + BLENumericComparisonReplyAction, + BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, +) +async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + accept = config[CONF_ACCEPT] + if cg.is_template(accept): + templ = await cg.templatable(accept, args, cg.bool_) + cg.add(var.set_value_template(templ)) + else: + cg.add(var.set_value_simple(accept)) + + return var + + +@automation.register_action( + "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA +) +async def passkey_reply_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + passkey = config[CONF_PASSKEY] + if cg.is_template(passkey): + templ = await cg.templatable(passkey, args, cg.uint32) + cg.add(var.set_value_template(templ)) + else: + cg.add(var.set_value_simple(passkey)) + + return var + + +@automation.register_action( + "ble_client.remove_bond", + BLERemoveBondAction, + BLE_REMOVE_BOND_ACTION_SCHEMA, +) +async def remove_bond_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -148,3 +267,12 @@ async def to_code(config): for conf in config.get(CONF_ON_DISCONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PASSKEY_REQUEST, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PASSKEY_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) + for conf in config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 45ddba978..423f74b85 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -37,6 +37,44 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { } }; +class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + this->trigger(); + } + } +}; + +class BLEClientPasskeyNotificationTrigger : public Trigger, public BLEClientNode { + public: + explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + uint32_t passkey = param->ble_security.key_notif.passkey; + this->trigger(passkey); + } + } +}; + +class BLEClientNumericComparisonRequestTrigger : public Trigger, public BLEClientNode { + public: + explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_NC_REQ_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + uint32_t passkey = param->ble_security.key_notif.passkey; + this->trigger(passkey); + } + } +}; + class BLEWriterClientNode : public BLEClientNode { public: BLEWriterClientNode(BLEClient *ble_client) { @@ -94,6 +132,86 @@ template class BLEClientWriteAction : public Action, publ std::function(Ts...)> value_template_{}; }; +template class BLEClientPasskeyReplyAction : public Action { + public: + BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + uint32_t passkey; + if (has_simple_value_) { + passkey = this->value_simple_; + } else { + passkey = this->value_template_(x...); + } + if (passkey > 999999) + return; + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + esp_ble_passkey_reply(remote_bda, true, passkey); + } + + void set_value_template(std::function func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const uint32_t &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + BLEClient *parent_{nullptr}; + bool has_simple_value_ = true; + uint32_t value_simple_{0}; + std::function value_template_{}; +}; + +template class BLEClientNumericComparisonReplyAction : public Action { + public: + BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + if (has_simple_value_) { + esp_ble_confirm_reply(remote_bda, this->value_simple_); + } else { + esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); + } + } + + void set_value_template(std::function func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const bool &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + BLEClient *parent_{nullptr}; + bool has_simple_value_ = true; + bool value_simple_{false}; + std::function value_template_{}; +}; + +template class BLEClientRemoveBondAction : public Action { + public: + BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + esp_ble_remove_bond_device(remote_bda); + } + + private: + BLEClient *parent_{nullptr}; +}; + } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index ceca94c86..e04f4a804 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -27,7 +27,7 @@ class BLEClient; class BLEClientNode { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) = 0; + esp_ble_gattc_cb_param_t *param){}; virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} virtual void loop() {} void set_address(uint64_t address) { address_ = address; } diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index caa05c27b..5ea04b480 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -40,6 +40,7 @@ DEVICE = { NextAction = dfplayer_ns.class_("NextAction", automation.Action) PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action) +PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action) PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action) PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action) SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action) @@ -113,6 +114,25 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "dfplayer.play_mp3", + PlayMp3Action, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + }, + key=CONF_FILE, + ), +) +async def dfplayer_play_mp3_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_FILE], args, float) + cg.add(var.set_file(template_)) + return var + + @automation.register_action( "dfplayer.play", PlayFileAction, diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index e16479570..a6339dc98 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -7,10 +7,10 @@ namespace dfplayer { static const char *const TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { - if (folder < 100 && file < 256) { + if (folder <= 10 && file <= 1000) { this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); - } else if (folder <= 10 && file <= 1000) { + } else if (folder < 100 && file < 256) { this->ack_set_is_playing_ = true; this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); } else { diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index ae47cb33f..26e90fd41 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -35,6 +35,10 @@ class DFPlayer : public uart::UARTDevice, public Component { this->ack_set_is_playing_ = true; this->send_cmd_(0x02); } + void play_mp3(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x12, file); + } void play_file(uint16_t file) { this->ack_set_is_playing_ = true; this->send_cmd_(0x03, file); @@ -113,6 +117,16 @@ class DFPlayer : public uart::UARTDevice, public Component { DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) +template class PlayMp3Action : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + + void play(Ts... x) override { + auto file = this->file_.value(x...); + this->parent_->play_mp3(file); + } +}; + template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 7db6fff6b..f508cecb8 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_beacon"] CONF_BLE_ID = "ble_id" +CONF_IO_CAPABILITY = "io_capability" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -19,10 +20,21 @@ GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") +IoCapability = esp32_ble_ns.enum("IoCapability") +IO_CAPABILITY = { + "none": IoCapability.IO_CAP_NONE, + "keyboard_only": IoCapability.IO_CAP_IN, + "keyboard_display": IoCapability.IO_CAP_KBDISP, + "display_only": IoCapability.IO_CAP_OUT, + "display_yes_no": IoCapability.IO_CAP_IO, +} CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), + cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( + IO_CAPABILITY, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -39,6 +51,7 @@ FINAL_VALIDATE_SCHEMA = validate_variant async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 502399f97..21ec005e0 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -134,8 +134,7 @@ bool ESP32BLE::ble_setup_() { return false; } - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); return false; @@ -215,9 +214,31 @@ float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLE::dump_config() { const uint8_t *mac_address = esp_bt_dev_get_address(); if (mac_address) { + const char *io_capability_s; + switch (this->io_cap_) { + case ESP_IO_CAP_OUT: + io_capability_s = "display_only"; + break; + case ESP_IO_CAP_IO: + io_capability_s = "display_yes_no"; + break; + case ESP_IO_CAP_IN: + io_capability_s = "keyboard_only"; + break; + case ESP_IO_CAP_NONE: + io_capability_s = "none"; + break; + case ESP_IO_CAP_KBDISP: + io_capability_s = "keyboard_display"; + break; + default: + io_capability_s = "invalid"; + break; + } ESP_LOGCONFIG(TAG, "ESP32 BLE:"); ESP_LOGCONFIG(TAG, " MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5]); + ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s); } else { ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 5970b4368..11ae82654 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,6 +25,14 @@ typedef struct { uint16_t mtu; } conn_status_t; +enum IoCapability { + IO_CAP_OUT = ESP_IO_CAP_OUT, + IO_CAP_IO = ESP_IO_CAP_IO, + IO_CAP_IN = ESP_IO_CAP_IN, + IO_CAP_NONE = ESP_IO_CAP_NONE, + IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, +}; + class GAPEventHandler { public: virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; @@ -44,6 +52,8 @@ class GATTsEventHandler { class ESP32BLE : public Component { public: + void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void setup() override; void loop() override; void dump_config() override; @@ -72,6 +82,7 @@ class ESP32BLE : public Component { Queue ble_events_; BLEAdvertising *advertising_; + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index d27b0ca4c..4043f32dc 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -77,10 +77,12 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) { this->enrollment_done_callback_.call(this->enrollment_slot_); this->get_fingerprint_count_(); } else { - this->enrollment_failed_callback_.call(this->enrollment_slot_); + if (this->enrollment_slot_ != ENROLLMENT_SLOT_UNUSED) { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } } this->enrollment_image_ = 0; - this->enrollment_slot_ = 0; + this->enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; if (this->enrolling_binary_sensor_ != nullptr) { this->enrolling_binary_sensor_->publish_state(false); } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index fd316237f..f414146e6 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -13,6 +13,8 @@ namespace fingerprint_grow { static const uint16_t START_CODE = 0xEF01; +static const uint16_t ENROLLMENT_SLOT_UNUSED = 0xFFFF; + enum GrowPacketType { COMMAND = 0x01, DATA = 0x02, @@ -158,7 +160,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; uint8_t enrollment_image_ = 0; - uint16_t enrollment_slot_ = 0; + uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; uint32_t last_aura_led_control_ = 0; diff --git a/esphome/components/hyt271/__init__.py b/esphome/components/hyt271/__init__.py new file mode 100644 index 000000000..2e88d4f36 --- /dev/null +++ b/esphome/components/hyt271/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp new file mode 100644 index 000000000..94558fff0 --- /dev/null +++ b/esphome/components/hyt271/hyt271.cpp @@ -0,0 +1,52 @@ +#include "hyt271.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace hyt271 { + +static const char *const TAG = "hyt271"; + +static const uint8_t HYT271_ADDRESS = 0x28; + +void HYT271Component::dump_config() { + ESP_LOGCONFIG(TAG, "HYT271:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); +} +void HYT271Component::update() { + uint8_t raw_data[4]; + + if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Ask new values"); + return; + } + this->set_timeout("wait_convert", 50, [this]() { + uint8_t raw_data[4]; + if (this->read(raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Read values"); + return; + } + uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2; + uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1]; + + float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f; + float humidity = (float(raw_humidity)) * (100.0f / 16383.0f); + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + + if (this->temperature_ != nullptr) + this->temperature_->publish_state(temperature); + if (this->humidity_ != nullptr) + this->humidity_->publish_state(humidity); + this->status_clear_warning(); + }); +} +float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h new file mode 100644 index 000000000..64f32a651 --- /dev/null +++ b/esphome/components/hyt271/hyt271.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hyt271 { + +class HYT271Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + + void dump_config() override; + /// Update the sensor values (temperature+humidity). + void update() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; +}; + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py new file mode 100644 index 000000000..2ec283646 --- /dev/null +++ b/esphome/components/hyt271/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hyt271_ns = cg.esphome_ns.namespace("hyt271") +HYT271Component = hyt271_ns.class_( + "HYT271Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HYT271Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py new file mode 100644 index 000000000..77e0d37e7 --- /dev/null +++ b/esphome/components/max6956/__init__.py @@ -0,0 +1,148 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@looping40"] + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_BRIGHTNESS_MODE = "brightness_mode" +CONF_BRIGHTNESS_GLOBAL = "brightness_global" + + +max6956_ns = cg.esphome_ns.namespace("max6956") + +MAX6956 = max6956_ns.class_("MAX6956", cg.Component, i2c.I2CDevice) +MAX6956GPIOPin = max6956_ns.class_("MAX6956GPIOPin", cg.GPIOPin) + +# Actions +SetCurrentGlobalAction = max6956_ns.class_("SetCurrentGlobalAction", automation.Action) +SetCurrentModeAction = max6956_ns.class_("SetCurrentModeAction", automation.Action) + +MAX6956_CURRENTMODE = max6956_ns.enum("MAX6956CURRENTMODE") +CURRENT_MODES = { + "global": MAX6956_CURRENTMODE.GLOBAL, + "segment": MAX6956_CURRENTMODE.SEGMENT, +} + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956), + cv.Optional(CONF_BRIGHTNESS_GLOBAL, default="0"): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_BRIGHTNESS_MODE, default="global"): cv.enum( + CURRENT_MODES, lower=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_brightness_mode(config[CONF_BRIGHTNESS_MODE])) + cg.add(var.set_brightness_global(config[CONF_BRIGHTNESS_GLOBAL])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +CONF_MAX6956 = "max6956" + +MAX6956_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), + cv.Required(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MAX6956, MAX6956_PIN_SCHEMA) +async def max6956_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_MAX6956]) + + cg.add(var.set_parent(parent)) + + 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 + + +@automation.register_action( + "max6956.set_brightness_global", + SetCurrentGlobalAction, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_GLOBAL): cv.templatable( + cv.int_range(min=0, max=15) + ), + }, + key=CONF_BRIGHTNESS_GLOBAL, + ), +) +async def max6956_set_brightness_global_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_BRIGHTNESS_GLOBAL], args, float) + cg.add(var.set_brightness_global(template_)) + return var + + +@automation.register_action( + "max6956.set_brightness_mode", + SetCurrentModeAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_MODE): cv.templatable( + cv.enum(CURRENT_MODES, lower=True) + ), + }, + key=CONF_BRIGHTNESS_MODE, + ), +) +async def max6956_set_brightness_mode_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_BRIGHTNESS_MODE], args, float) + cg.add(var.set_brightness_mode(template_)) + return var diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h new file mode 100644 index 000000000..c0b491dc7 --- /dev/null +++ b/esphome/components/max6956/automation.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/max6956/max6956.h" + +namespace esphome { +namespace max6956 { + +template class SetCurrentGlobalAction : public Action { + public: + SetCurrentGlobalAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(uint8_t, brightness_global) + + void play(Ts... x) override { + this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); + this->max6956_->write_brightness_global(); + } + + protected: + MAX6956 *max6956_; +}; + +template class SetCurrentModeAction : public Action { + public: + SetCurrentModeAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) + + void play(Ts... x) override { + this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); + this->max6956_->write_brightness_mode(); + } + + protected: + MAX6956 *max6956_; +}; +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp new file mode 100644 index 000000000..c2d9ba017 --- /dev/null +++ b/esphome/components/max6956/max6956.cpp @@ -0,0 +1,170 @@ +#include "max6956.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956"; + +/// Masks for MAX6956 Configuration register +const uint32_t MASK_TRANSITION_DETECTION = 0x80; +const uint32_t MASK_INDIVIDUAL_CURRENT = 0x40; +const uint32_t MASK_NORMAL_OPERATION = 0x01; + +const uint32_t MASK_1PORT_VALUE = 0x03; +const uint32_t MASK_PORT_CONFIG = 0x03; +const uint8_t MASK_CONFIG_CURRENT = 0x40; +const uint8_t MASK_CURRENT_PIN = 0x0F; + +/************************************** + * MAX6956 * + **************************************/ +void MAX6956::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX6956..."); + uint8_t configuration; + if (!this->read_reg_(MAX6956_CONFIGURATION, &configuration)) { + this->mark_failed(); + return; + } + + write_brightness_global(); + write_brightness_mode(); + + /** TO DO : read transition detection in yaml + TO DO : read indivdual current in yaml **/ + this->read_reg_(MAX6956_CONFIGURATION, &configuration); + ESP_LOGD(TAG, "Initial reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); + configuration = configuration | MASK_NORMAL_OPERATION; + this->write_reg_(MAX6956_CONFIGURATION, configuration); + + ESP_LOGCONFIG(TAG, "Enabling normal operation"); + ESP_LOGD(TAG, "setup reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); +} + +bool MAX6956::digital_read(uint8_t pin) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return (value & MASK_1PORT_VALUE); +} + +void MAX6956::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + this->write_reg_(reg_addr, value); +} + +void MAX6956::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956_INPUT; + + if (flags == gpio::FLAG_INPUT) { + mode = MAX6956GPIOMode::MAX6956_INPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + mode = MAX6956GPIOMode::MAX6956_INPUT_PULLUP; + } else if (flags == gpio::FLAG_OUTPUT) { + mode = MAX6956GPIOMode::MAX6956_OUTPUT; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956GPIOMode::MAX6956_LED; + + if (flags == max6956::FLAG_LED) { + mode = MAX6956GPIOMode::MAX6956_LED; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_brightness_global(uint8_t current) { + if (current > 15) { + ESP_LOGE(TAG, "Global brightness out off range (%u)", current); + return; + } + global_brightness_ = current; +} + +void MAX6956::write_brightness_global() { this->write_reg_(MAX6956_GLOBAL_CURRENT, global_brightness_); } + +void MAX6956::set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode) { brightness_mode_ = brightness_mode; }; + +void MAX6956::write_brightness_mode() { + uint8_t reg_addr = MAX6956_CONFIGURATION; + uint8_t config = 0; + + this->read_reg_(reg_addr, &config); + config &= ~MASK_CONFIG_CURRENT; + config |= brightness_mode_ << 6; + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; + uint8_t config = 0; + uint8_t shift = 4 * (pin % 2); + uint8_t bright = roundf(brightness * 15); + + if (prev_bright_[pin - MAX6956_MIN] == bright) + return; + + prev_bright_[pin - MAX6956_MIN] = bright; + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_CURRENT_PIN << shift); + config |= (bright << shift); + this->write_reg_(reg_addr, config); +} + +bool MAX6956::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} + +bool MAX6956::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} + +void MAX6956::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956"); + + if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { + ESP_LOGCONFIG(TAG, "current mode: global"); + ESP_LOGCONFIG(TAG, "global brightness: %u", global_brightness_); + } else { + ESP_LOGCONFIG(TAG, "current mode: segment"); + } +} + +/************************************** + * MAX6956GPIOPin * + **************************************/ +void MAX6956GPIOPin::setup() { pin_mode(flags_); } +void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string MAX6956GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_); + return buffer; +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h new file mode 100644 index 000000000..141164ab3 --- /dev/null +++ b/esphome/components/max6956/max6956.h @@ -0,0 +1,94 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace max6956 { + +/// Modes for MAX6956 pins +enum MAX6956GPIOMode : uint8_t { + MAX6956_LED = 0x00, + MAX6956_OUTPUT = 0x01, + MAX6956_INPUT = 0x02, + MAX6956_INPUT_PULLUP = 0x03 +}; + +/// Range for MAX6956 pins +enum MAX6956GPIORange : uint8_t { + MAX6956_MIN = 4, + MAX6956_MAX = 31, +}; + +enum MAX6956GPIORegisters { + MAX6956_GLOBAL_CURRENT = 0x02, + MAX6956_CONFIGURATION = 0x04, + MAX6956_TRANSITION_DETECT_MASK = 0x06, + MAX6956_DISPLAY_TEST = 0x07, + MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4 + MAX6956_CURRENT_START = 0x12, // Current054 + MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action) + MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4–11 (data bits D0–D7) +}; + +enum MAX6956GPIOFlag { FLAG_LED = 0x20 }; + +enum MAX6956CURRENTMODE { GLOBAL = 0x00, SEGMENT = 0x01 }; + +class MAX6956 : public Component, public i2c::I2CDevice { + public: + MAX6956() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, gpio::Flags flags); + void pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags); + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_brightness_global(uint8_t current); + void set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode); + void set_pin_brightness(uint8_t pin, float brightness); + + void dump_config() override; + + void write_brightness_global(); + void write_brightness_mode(); + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + max6956::MAX6956CURRENTMODE brightness_mode_; + uint8_t global_brightness_; + + private: + int8_t prev_bright_[28] = {0}; +}; + +class MAX6956GPIOPin : public GPIOPin { + 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_parent(MAX6956 *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + MAX6956 *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/__init__.py b/esphome/components/max6956/output/__init__.py new file mode 100644 index 000000000..1caf8c8a4 --- /dev/null +++ b/esphome/components/max6956/output/__init__.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_PIN, CONF_ID +from .. import MAX6956, max6956_ns, CONF_MAX6956 + +DEPENDENCIES = ["max6956"] + +MAX6956LedChannel = max6956_ns.class_( + "MAX6956LedChannel", output.FloatOutput, cg.Component +) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956LedChannel), + cv.GenerateID(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_PIN): cv.int_range(min=4, max=31), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_MAX6956]) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_parent(parent)) diff --git a/esphome/components/max6956/output/max6956_led_output.cpp b/esphome/components/max6956/output/max6956_led_output.cpp new file mode 100644 index 000000000..5fa2dd9b3 --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.cpp @@ -0,0 +1,26 @@ +#include "max6956_led_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956_led_channel"; + +void MAX6956LedChannel::write_state(float state) { this->parent_->set_pin_brightness(this->pin_, state); } + +void MAX6956LedChannel::write_state(bool state) { this->parent_->digital_write(this->pin_, state); } + +void MAX6956LedChannel::setup() { + this->parent_->pin_mode(this->pin_, max6956::FLAG_LED); + this->turn_off(); +} + +void MAX6956LedChannel::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956 current:"); + ESP_LOGCONFIG(TAG, " MAX6956 pin: %d", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/max6956_led_output.h b/esphome/components/max6956/output/max6956_led_output.h new file mode 100644 index 000000000..b844a7cee --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/max6956/max6956.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace max6956 { + +class MAX6956; + +class MAX6956LedChannel : public output::FloatOutput, public Component { + public: + void set_parent(MAX6956 *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + void write_state(bool state) override; + + MAX6956 *parent_; + uint8_t pin_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py new file mode 100644 index 000000000..574d8dce9 --- /dev/null +++ b/esphome/components/pca6416a/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@Mat931"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +pca6416a_ns = cg.esphome_ns.namespace("pca6416a") + +PCA6416AComponent = pca6416a_ns.class_("PCA6416AComponent", cg.Component, i2c.I2CDevice) +PCA6416AGPIOPin = pca6416a_ns.class_( + "PCA6416AGPIOPin", cg.GPIOPin, cg.Parented.template(PCA6416AComponent) +) + +CONF_PCA6416A = "pca6416a" +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)}) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +PCA6416A_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(PCA6416AGPIOPin), + cv.Required(CONF_PCA6416A): cv.use_id(PCA6416AComponent), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=16), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register("pca6416a", PCA6416A_PIN_SCHEMA) +async def pca6416a_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_PCA6416A]) + + cg.add(var.set_parent(parent)) + + 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/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp new file mode 100644 index 000000000..1f4e31564 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -0,0 +1,174 @@ +#include "pca6416a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pca6416a { + +enum PCA6416AGPIORegisters { + // 0 side + PCA6416A_INPUT0 = 0x00, + PCA6416A_OUTPUT0 = 0x02, + PCA6416A_INVERT0 = 0x04, + PCA6416A_CONFIG0 = 0x06, + PCAL6416A_PULL_EN0 = 0x46, + PCAL6416A_PULL_DIR0 = 0x48, + // 1 side + PCA6416A_INPUT1 = 0x01, + PCA6416A_OUTPUT1 = 0x03, + PCA6416A_INVERT1 = 0x05, + PCA6416A_CONFIG1 = 0x07, + PCAL6416A_PULL_EN1 = 0x47, + PCAL6416A_PULL_DIR1 = 0x49, +}; + +static const char *const TAG = "pca6416a"; + +void PCA6416AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCA6416A..."); + // Test to see if device exists + uint8_t value; + if (!this->read_register_(PCA6416A_INPUT0, &value)) { + ESP_LOGE(TAG, "PCA6416A not available under 0x%02X", this->address_); + this->mark_failed(); + return; + } + + // Test to see if the device supports pull-up resistors + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == esphome::i2c::ERROR_OK) { + this->has_pullup_ = true; + } + + // No polarity inversion + this->write_register_(PCA6416A_INVERT0, 0); + this->write_register_(PCA6416A_INVERT1, 0); + // Set all pins to input + this->write_register_(PCA6416A_CONFIG0, 0xff); + this->write_register_(PCA6416A_CONFIG1, 0xff); + // Read current output register state + this->read_register_(PCA6416A_OUTPUT0, &this->output_0_); + this->read_register_(PCA6416A_OUTPUT1, &this->output_1_); + + ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void PCA6416AComponent::dump_config() { + if (this->has_pullup_) { + ESP_LOGCONFIG(TAG, "PCAL6416A:"); + } else { + ESP_LOGCONFIG(TAG, "PCA6416A:"); + } + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCA6416A failed!"); + } +} + +bool PCA6416AComponent::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; + uint8_t value = 0; + this->read_register_(reg_addr, &value); + return value & (1 << bit); +} + +void PCA6416AComponent::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; + this->update_register_(pin, value, reg_addr); +} + +void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t io_dir = pin < 8 ? PCA6416A_CONFIG0 : PCA6416A_CONFIG1; + uint8_t pull_en = pin < 8 ? PCAL6416A_PULL_EN0 : PCAL6416A_PULL_EN1; + uint8_t pull_dir = pin < 8 ? PCAL6416A_PULL_DIR0 : PCAL6416A_PULL_DIR1; + if (flags == gpio::FLAG_INPUT) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, false, pull_en); + } + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, true, pull_en); + } else { + ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors"); + } + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_register_(pin, false, io_dir); + } +} + +bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->read_register(reg, value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == PCA6416A_OUTPUT0) { + reg_value = this->output_0_; + } else if (reg_addr == PCA6416A_OUTPUT1) { + reg_value = this->output_1_; + } else { + this->read_register_(reg_addr, ®_value); + } + + if (pin_value) { + reg_value |= 1 << bit; + } else { + reg_value &= ~(1 << bit); + } + + this->write_register_(reg_addr, reg_value); + + if (reg_addr == PCA6416A_OUTPUT0) { + this->output_0_ = reg_value; + } else if (reg_addr == PCA6416A_OUTPUT1) { + this->output_1_ = reg_value; + } +} + +float PCA6416AComponent::get_setup_priority() const { return setup_priority::IO; } + +void PCA6416AGPIOPin::setup() { pin_mode(flags_); } +void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string PCA6416AGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_); + return buffer; +} + +} // namespace pca6416a +} // namespace esphome diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h new file mode 100644 index 000000000..247f443e8 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pca6416a { + +class PCA6416AComponent : public Component, public i2c::I2CDevice { + public: + PCA6416AComponent() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + protected: + bool read_register_(uint8_t reg, uint8_t *value); + bool write_register_(uint8_t reg, uint8_t value); + void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_0_{0x00}; + uint8_t output_1_{0x00}; + /// Storage for last I2C error seen + esphome::i2c::ErrorCode last_error_; + /// Only the PCAL6416A has pull-up resistors + bool has_pullup_{false}; +}; + +/// Helper class to expose a PCA6416A pin as an internal input GPIO pin. +class PCA6416AGPIOPin : public GPIOPin { + 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_parent(PCA6416AComponent *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + PCA6416AComponent *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pca6416a +} // namespace esphome diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index dc831ef6e..cc28d7078 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -81,7 +81,32 @@ void PN532::setup() { this->turn_off_rf_(); } +bool PN532::powerdown() { + updates_enabled_ = false; + requested_read_ = false; + ESP_LOGI(TAG, "Powering down PN532"); + if (!this->write_command_({PN532_COMMAND_POWERDOWN, 0b10100000})) { // enable i2c,spi wakeup + ESP_LOGE(TAG, "Error writing powerdown command to PN532"); + return false; + } + std::vector response; + if (!this->read_response(PN532_COMMAND_POWERDOWN, response)) { + ESP_LOGE(TAG, "Error reading PN532 powerdown response"); + return false; + } + if (response[0] != 0x00) { + ESP_LOGE(TAG, "Error on PN532 powerdown: %02x", response[0]); + return false; + } + ESP_LOGV(TAG, "Powerdown successful"); + delay(1); + return true; +} + void PN532::update() { + if (!updates_enabled_) + return; + for (auto *obj : this->binary_sensors_) obj->on_scan_end(); diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index fee94a29b..73b349e32 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -17,6 +17,7 @@ static const uint8_t PN532_COMMAND_SAMCONFIGURATION = 0x14; static const uint8_t PN532_COMMAND_RFCONFIGURATION = 0x32; static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; +static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; class PN532BinarySensor; @@ -30,6 +31,7 @@ class PN532 : public PollingComponent { float get_setup_priority() const override; void loop() override; + void on_shutdown() override { powerdown(); } void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } @@ -45,6 +47,7 @@ class PN532 : public PollingComponent { void clean_mode(); void format_mode(); void write_mode(nfc::NdefMessage *message); + bool powerdown(); protected: void turn_off_rf_(); @@ -79,6 +82,7 @@ class PN532 : public PollingComponent { bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); + bool updates_enabled_{true}; bool requested_read_{false}; std::vector binary_sensors_; std::vector triggers_ontag_; diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 4d9196c9c..2ef33f371 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -791,6 +791,57 @@ async def raw_action(var, config, args): cg.add(var.set_carrier_frequency(templ)) +# Drayton +( + DraytonData, + DraytonBinarySensor, + DraytonTrigger, + DraytonAction, + DraytonDumper, +) = declare_protocol("Drayton") +DRAYTON_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFF)), + cv.Required(CONF_CHANNEL): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), + } +) + + +@register_binary_sensor("drayton", DraytonBinarySensor, DRAYTON_SCHEMA) +def drayton_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DraytonData, + ("address", config[CONF_ADDRESS]), + ("channel", config[CONF_CHANNEL]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("drayton", DraytonTrigger, DraytonData) +def drayton_trigger(var, config): + pass + + +@register_dumper("drayton", DraytonDumper) +def drayton_dumper(var, config): + pass + + +@register_action("drayton", DraytonAction, DRAYTON_SCHEMA) +async def drayton_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) + cg.add(var.set_channel(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # RC5 RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol("RC5") RC5_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp new file mode 100644 index 000000000..f5eae4905 --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -0,0 +1,213 @@ +#include "drayton_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.drayton"; + +static const uint32_t BIT_TIME_US = 500; +static const uint8_t CARRIER_KHZ = 2; +static const uint8_t NBITS_PREAMBLE = 12; +static const uint8_t NBITS_SYNC = 4; +static const uint8_t NBITS_ADDRESS = 16; +static const uint8_t NBITS_CHANNEL = 5; +static const uint8_t NBITS_COMMAND = 7; +static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; + +static const uint8_t CMD_ON = 0x41; +static const uint8_t CMD_OFF = 0x02; + +/* +Drayton Protocol +Using an oscilloscope to capture the data transmitted by the Digistat two +distinct packets for 'On' and 'Off' are transmitted. Each transmitted bit +has a period of 500us, a bit rate of 2000 baud. + +Each packet consists of an initial 1010 pattern to set up the receiver bias. +The number of these bits seen at the receiver varies depending on the state +of the bias when the packet transmission starts. The receiver algoritmn takes +account of this. + +The packet appears to be Manchester encoded, with a '10' tranmitted pair +representing a '1' bit and a '01' pair representing a '0' bit. Each packet is +begun with a '1100' syncronisation symbol which breaks this rule. Following +the sync are 28 '01' or '10' pairs. + +-------------------- + +Boiler On Command as received: +101010101010110001101001010101101001010101010101100101010101101001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-1-0-0-0-0-0-1-1-0-0-1-0 + +(Where pppp represents the preamble bits and SSSS represents the sync symbol) + +28 bits of data received 01100001100000001000001 10010 (bin) or 6180832 (hex) + +Boiler Off Command as received: +101010101010110001101001010101101001010101010101010101010110011001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-0-0-0-0-0-1-0-1-0-0-1-0 + +28 bits of data received 0110000110000000000001010010 (bin) or 6180052 (hex) + +-------------------- + +I have used 'RFLink' software (RLink Firmware Version: 1.1 Revision: 48) to +capture and retransmit the Digistat packets. RFLink splits each packet into an +ID, SWITCH, and CMD field. + +0;17;Drayton;ID=c300;SWITCH=12;CMD=ON; +20;18;Drayton;ID=c300;SWITCH=12;CMD=OFF; + +-------------------- + +Spliting my received data into three parts of 16, 7 and 5 bits gives address, +channel and Command values of: + +On 6180832 0110000110000000 1000001 10010 +address: '0x6180' channel: '0x12' command: '0x41' + +Off 6180052 0110000110000000 0000010 10010 +address: '0x6180' channel: '0x12' command: '0x02' + +These values are slightly different to those used by RFLink (the RFLink +ID/Adress value is rotated/manipulated), and I don't know who's interpretation +is correct. A larger data sample would help (I have only found five different +packet captures online) or definitive information from Drayton. + +Splitting each packet in this way works well for me with esphome. Any +corrections or additional data samples would be gratefully received. + +marshn + +*/ + +void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { + uint16_t khz = CARRIER_KHZ; + dst->set_carrier_frequency(khz * 1000); + + // Preamble = 101010101010 + uint32_t out_data = 0x0AAA; + for (uint32_t mask = 1UL << (NBITS_PREAMBLE - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + // Sync = 1100 + out_data = 0x000C; + for (uint32_t mask = 1UL << (NBITS_SYNC - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + ESP_LOGD(TAG, "Send Drayton: address=%04x channel=%03x cmd=%02x", data.address, data.channel, data.command); + + out_data = data.address; + out_data <<= NBITS_COMMAND; + out_data |= data.command; + out_data <<= NBITS_CHANNEL; + out_data |= data.channel; + + ESP_LOGV(TAG, "Send Drayton: out_data %08x", out_data); + + for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + dst->space(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + dst->mark(BIT_TIME_US); + } + } +} + +optional DraytonProtocol::decode(RemoteReceiveData src) { + DraytonData out{ + .address = 0, + .channel = 0, + .command = 0, + }; + + if (src.size() < 45) { + return {}; + } + + ESP_LOGVV(TAG, "Decode Drayton: %d, %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), + src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), + src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), + src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + // If first preamble item is a space, skip it + if (src.peek_space_at_least(1)) { + src.advance(1); + } + + // Look for sync pulse, after. If sucessful index points to space of sync symbol + for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) { + ESP_LOGVV(TAG, "Decode Drayton: preamble %d %d %d", preamble, src.peek(preamble), src.peek(preamble + 1)); + if (src.peek_mark(2 * BIT_TIME_US, preamble) && + (src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { + src.advance(preamble + 1); + break; + } + } + + // Read data. Index points to space of sync symbol + // Extract first bit + // Checks next bit to leave index pointing correctly + uint32_t out_data = 0; + uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1; + if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %d", src.get_index()); + return {}; + } + + // Before/after each bit is read the index points to the transition at the start of the bit period or, + // if there is no transition at the start of the bit period, then the transition in the middle of + // the previous bit period. + while (--bit >= 1) { + ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08x", bit, out_data); + return {}; + } + } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } + ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + + out.channel = (uint8_t) (out_data & 0x1F); + out_data >>= NBITS_CHANNEL; + out.command = (uint8_t) (out_data & 0x7F); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFFFF); + + return out; +} +void DraytonProtocol::dump(const DraytonData &data) { + ESP_LOGD(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, + ((data.address << 1) & 0xffff), data.channel, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h new file mode 100644 index 000000000..f468e7b57 --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct DraytonData { + uint16_t address; + uint8_t channel; + uint8_t command; + + bool operator==(const DraytonData &rhs) const { + return address == rhs.address && channel == rhs.channel && command == rhs.command; + } +}; + +class DraytonProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DraytonData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DraytonData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Drayton) + +template class DraytonAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, address) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DraytonData data{}; + data.address = this->address_.value(x...); + data.channel = this->channel_.value(x...); + data.command = this->command_.value(x...); + DraytonProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index d49b1ba38..6aa76dcd2 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -286,7 +286,9 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( { cv.Optional(CONF_ENABLE_SWITCH): cv.maybe_simple_value( switch.switch_schema( - SprinklerControllerSwitch, entity_category=ENTITY_CATEGORY_CONFIG + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", ), key=CONF_NAME, ), @@ -333,7 +335,9 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( cv.Optional(CONF_NAME): cv.string, cv.Optional(CONF_AUTO_ADVANCE_SWITCH): cv.maybe_simple_value( switch.switch_schema( - SprinklerControllerSwitch, entity_category=ENTITY_CATEGORY_CONFIG + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", ), key=CONF_NAME, ), @@ -343,19 +347,25 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( ), cv.Optional(CONF_QUEUE_ENABLE_SWITCH): cv.maybe_simple_value( switch.switch_schema( - SprinklerControllerSwitch, entity_category=ENTITY_CATEGORY_CONFIG + 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, entity_category=ENTITY_CATEGORY_CONFIG + 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 + SprinklerControllerSwitch, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="RESTORE_DEFAULT_OFF", ), key=CONF_NAME, ), diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 6169185d6..52a6cd2af 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -1176,6 +1176,21 @@ optional Sprinkler::time_remaining_current_operation() { 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; diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 7952c4533..7a8285ae7 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -406,6 +406,12 @@ class Sprinkler : public Component { /// 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); @@ -503,7 +509,6 @@ class Sprinkler : public Component { /// 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}; diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 747794b63..d0276f119 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -39,6 +39,9 @@ WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) +WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper5P8InV2", WaveshareEPaper +) WaveshareEPaper7P5In = waveshare_epaper_ns.class_( "WaveshareEPaper7P5In", WaveshareEPaper ) @@ -80,6 +83,7 @@ MODELS = { "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), + "5.83inv2": ("b", WaveshareEPaper5P8InV2), "7.50in": ("b", WaveshareEPaper7P5In), "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 8c4b13751..42f5bc54e 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1037,6 +1037,88 @@ void WaveshareEPaper5P8In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +// ======================================================== +// 5.83in V2 +// Datasheet/Specification/Reference: +// - https://www.waveshare.com/w/upload/3/37/5.83inch_e-Paper_V2_Specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/Arduino/epd5in83_V2/epd5in83_V2.cpp +// ======================================================== +void WaveshareEPaper5P8InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + + // COMMAND POWER ON + this->command(0x04); + delay(10); + this->wait_until_idle_(); + + // PANNEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x02); + this->data(0x88); + this->data(0x01); + this->data(0xE0); + + this->command(0x15); + this->data(0x00); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + + // Do we need this? + // COMMAND PLL CONTROL + this->command(0x30); + this->data(0x3C); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ +} +void HOT WaveshareEPaper5P8InV2::display() { + // Reuse the code from WaveshareEPaper4P2In::display() + // COMMAND VCM DC SETTING REGISTER + this->command(0x82); + this->data(0x12); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x97); + + // COMMAND DATA START TRANSMISSION 1 + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 + this->command(0x13); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DISPLAY REFRESH + this->command(0x12); +} +int WaveshareEPaper5P8InV2::get_width_internal() { return 648; } +int WaveshareEPaper5P8InV2::get_height_internal() { return 480; } +void WaveshareEPaper5P8InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 5.83inv2"); + 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); +} + void WaveshareEPaper7P5InBV2::initialize() { // COMMAND POWER SETTING this->command(0x01); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index a674d3af0..1cb46bdb9 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -284,6 +284,49 @@ class WaveshareEPaper5P8In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper5P8InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x17); // border floating + + // COMMAND VCM DC SETTING + this->command(0x82); + // COMMAND PANEL SETTING + this->command(0x00); + + delay(100); // NOLINT + + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + delay(100); // NOLINT + + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper7P5In : public WaveshareEPaper { public: void initialize() override; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 80a53a751..00b2e2001 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -428,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) { diff --git a/platformio.ini b/platformio.ini index 7f301e560..da3bb9d29 100644 --- a/platformio.ini +++ b/platformio.ini @@ -88,7 +88,7 @@ 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) diff --git a/requirements.txt b/requirements.txt index 41fedc88c..a62c48e23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,8 +10,8 @@ platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.5.1 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.7.1 -zeroconf==0.56.0 +aioesphomeapi==13.7.2 +zeroconf==0.60.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_test.txt b/requirements_test.txt index b18aabe7b..55f8da245 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ 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 +pyupgrade==3.3.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py index 234041016..ed133ecb4 100755 --- a/script/platformio_install_deps.py +++ b/script/platformio_install_deps.py @@ -2,12 +2,22 @@ # This script is used to preinstall # all platformio libraries in the global storage +import argparse import configparser import subprocess -import sys config = configparser.ConfigParser(inline_comment_prefixes=(";",)) -config.read(sys.argv[1]) + +parser = argparse.ArgumentParser(description="") +parser.add_argument("file", help="Path to platformio.ini", nargs=1) +parser.add_argument("-l", "--libraries", help="Install libraries", action="store_true") +parser.add_argument("-p", "--platforms", help="Install platforms", action="store_true") +parser.add_argument("-t", "--tools", help="Install tools", action="store_true") + +args = parser.parse_args() + +config.read(args.file) + libs = [] tools = [] @@ -15,7 +25,7 @@ platforms = [] # Extract from every lib_deps key in all sections for section in config.sections(): conf = config[section] - if "lib_deps" in conf: + if "lib_deps" in conf and args.libraries: for lib_dep in conf["lib_deps"].splitlines(): if not lib_dep: # Empty line or comment @@ -28,10 +38,10 @@ for section in config.sections(): continue libs.append("-l") libs.append(lib_dep) - if "platform" in conf: + if "platform" in conf and args.platforms: platforms.append("-p") platforms.append(conf["platform"]) - if "platform_packages" in conf: + if "platform_packages" in conf and args.tools: for tool in conf["platform_packages"].splitlines(): if not tool: # Empty line or comment diff --git a/script/setup b/script/setup index 5acd1a9f1..656e95eba 100755 --- a/script/setup +++ b/script/setup @@ -15,4 +15,4 @@ pip3 install --no-use-pep517 -e . pre-commit install -script/platformio_install_deps.py platformio.ini +script/platformio_install_deps.py platformio.ini --libraries --tools --platforms diff --git a/tests/test1.yaml b/tests/test1.yaml index a235ff150..46c6bb80c 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -294,6 +294,9 @@ wled: adalight: +esp32_ble: + io_capability: keyboard_only + esp32_ble_tracker: ble_client: @@ -307,6 +310,19 @@ ble_client: on_disconnect: then: - switch.turn_on: ble1_status + on_passkey_request: + then: + - ble_client.passkey_reply: + id: ble_blah + passkey: 123456 + on_passkey_notification: + then: + - logger.log: "Passkey notification received" + on_numeric_comparison_request: + then: + - ble_client.numeric_comparison_reply: + id: ble_blah + accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client bedjet: @@ -1276,6 +1292,13 @@ sensor: name: DHT Absolute Humidity temperature: dht_temperature humidity: dht_humidity + - platform: hyt271 + i2c_id: i2c_bus + temperature: + name: "Temperature hyt271" + id: temp_etuve + humidity: + name: "Humidity hyt271" esp32_touch: setup_mode: false @@ -1449,6 +1472,13 @@ binary_sensor: number: 1 mode: INPUT inverted: true + - platform: gpio + name: PCA6416A binary sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true - platform: gpio name: MCP21 binary sensor pin: @@ -2934,6 +2964,11 @@ pca9554: address: 0x3F i2c_id: i2c_bus +pca6416a: + - id: pca6416a_hub + address: 0x21 + i2c_id: i2c_bus + mcp23017: - id: mcp23017_hub open_drain_interrupt: true diff --git a/tests/test4.yaml b/tests/test4.yaml index c78e29164..5520aad00 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -374,6 +374,16 @@ binary_sensor: on_press: - logger.log: Touched + - platform: gpio + name: MaxIn Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false + climate: - platform: tuya id: tuya_climate @@ -717,4 +727,8 @@ voice_assistant: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] +max6956: + - id: max6956_1 + address: 0x40 + es8388: