diff --git a/CODEOWNERS b/CODEOWNERS index c630db7948..d1b9ae7698 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -421,6 +421,7 @@ esphome/components/wk2204_spi/* @DrCoolZic esphome/components/wk2212_i2c/* @DrCoolZic esphome/components/wk2212_spi/* @DrCoolZic esphome/components/wl_134/* @hobbypunk90 +esphome/components/ws18x0_uart/* @Swamp-Ig esphome/components/x9c/* @EtienneMD esphome/components/xgzp68xx/* @gcormier esphome/components/xiaomi_hhccjcy10/* @fariouche diff --git a/esphome/components/ws18x0_uart/__init__.py b/esphome/components/ws18x0_uart/__init__.py new file mode 100644 index 0000000000..60765c3ddc --- /dev/null +++ b/esphome/components/ws18x0_uart/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import uart +from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID + +CODEOWNERS = ["@Swamp-Ig"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor"] +MULTI_CONF = True + +ws18x0_uart_ns = cg.esphome_ns.namespace("ws18x0_uart") +WS18x0UARTComponent = ws18x0_uart_ns.class_( + "WS18x0UARTComponent", cg.Component, uart.UARTDevice +) +WS18x0UARTTrigger = ws18x0_uart_ns.class_( + "WS18x0UARTTrigger", automation.Trigger.template(cg.uint32) +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WS18x0UARTComponent), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WS18x0UARTTrigger), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +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) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_trigger(trigger)) + await automation.build_automation(trigger, [(cg.uint32, "x")], conf) diff --git a/esphome/components/ws18x0_uart/binary_sensor.py b/esphome/components/ws18x0_uart/binary_sensor.py new file mode 100644 index 0000000000..0d9d08e000 --- /dev/null +++ b/esphome/components/ws18x0_uart/binary_sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, ws18x0_uart +from esphome.const import CONF_UID +from . import ws18x0_uart_ns + +DEPENDENCIES = ["ws18x0_uart"] + +CONF_WS18x0_UART_ID = "ws18x0_uart_id" +WS18x0UARTBinarySensor = ws18x0_uart_ns.class_( + "WS18x0UARTBinarySensor", binary_sensor.BinarySensor +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(WS18x0UARTBinarySensor).extend( + { + cv.GenerateID(CONF_WS18x0_UART_ID): cv.use_id(ws18x0_uart.WS18x0UARTComponent), + cv.Required(CONF_UID): cv.uint32_t, + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + + hub = await cg.get_variable(config[CONF_WS18x0_UART_ID]) + cg.add(hub.register_card(var)) + cg.add(var.set_id(config[CONF_UID])) diff --git a/esphome/components/ws18x0_uart/ws18x0_uart.cpp b/esphome/components/ws18x0_uart/ws18x0_uart.cpp new file mode 100644 index 0000000000..0a41c291c3 --- /dev/null +++ b/esphome/components/ws18x0_uart/ws18x0_uart.cpp @@ -0,0 +1,100 @@ +#include "ws18x0_uart.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ws18x0_uart { + +static const char *const TAG = "ws18x0_uart"; + +static const uint8_t WS18X0_UART_START_BYTE = 0x02; +static const uint8_t WS18X0_UART_END_BYTE = 0x03; + +static const int8_t WS18X0_UART_STATE_WAITING_FOR_START = -1; +static const int8_t WS18X0_UART_STATE_WAITING_FOR_LENGTH = -2; +static const int8_t WS18X0_UART_STATE_WAITING_FOR_CHECKSUM = 2; +static const int8_t WS18X0_UART_STATE_WAITING_FOR_END = 1; + +void ws18x0_uart::WS18x0UARTComponent::loop() { + while (this->available() > 0) { + uint8_t data; + if (!this->read_byte(&data)) { + ESP_LOGW(TAG, "Reading data from WS18x0UART failed!"); + this->status_set_warning(); + return; + } + + switch (this->read_state_) { + case WS18X0_UART_STATE_WAITING_FOR_START: + if (data == WS18X0_UART_START_BYTE) { + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_LENGTH; + } else { + // Not start byte, probably not synced up correctly, skip forward. + } + break; + case WS18X0_UART_STATE_WAITING_FOR_LENGTH: + if (data >= 9 && data <= 10) { + this->raw_ = data; + this->read_state_ = data - 2; + } else { + // Invalid length. Warn and resync + ESP_LOGW(TAG, "Reading data from WS18x0UART failed with length out of expected range: %d", data); + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_START; + this->status_set_warning(); + } + break; + case WS18X0_UART_STATE_WAITING_FOR_CHECKSUM: { + uint8_t checksum = 0; + for (int i = 0; i < 8; ++i) + checksum ^= (this->raw_ >> i * 8) & 0xFF; + + if (checksum == data) { + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_END; + } else { + // Invalid checksum. Warn and resync + ESP_LOGW(TAG, + "Reading data from WS18x0UART failed with invalid checksum read 0x%2X != calculated 0x%2X from raw " + "data: 0x%llX", + data, checksum, this->raw_); + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_START; + this->status_set_warning(); + } + break; + } + case WS18X0_UART_STATE_WAITING_FOR_END: + if (data != WS18X0_UART_END_BYTE) { + // Invalid end byte. Warn and resync + ESP_LOGW(TAG, "Reading data from WS18x0UART failed with invalid end byte: 0x%2X", data); + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_START; + this->status_set_warning(); + } else { + // Valid data + this->status_clear_warning(); + bool report = this->raw_ != this->last_raw_; + uint32_t result = (uint32_t) this->raw_; + + for (auto *card : this->cards_) { + if (card->process(result)) { + report = false; + } + } + for (auto *trig : this->triggers_) + trig->process(result); + + if (report) { + ESP_LOGD(TAG, "Found new tag with ID %" PRIu32, result); + } + this->last_raw_ = this->raw_; + this->read_state_ = WS18X0_UART_STATE_WAITING_FOR_START; + } + break; + default: + // Read next byte + this->raw_ = this->raw_ << 8 | data; + this->read_state_--; + break; + } + } +} + +} // namespace ws18x0_uart +} // namespace esphome diff --git a/esphome/components/ws18x0_uart/ws18x0_uart.h b/esphome/components/ws18x0_uart/ws18x0_uart.h new file mode 100644 index 0000000000..54cf07cd4a --- /dev/null +++ b/esphome/components/ws18x0_uart/ws18x0_uart.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/uart/uart.h" + +#include +#include + +namespace esphome { +namespace ws18x0_uart { + +class WS18x0UARTBinarySensor; +class WS18x0UARTTrigger; + +class WS18x0UARTComponent : public Component, public uart::UARTDevice { + public: + void loop() override; + + void register_card(WS18x0UARTBinarySensor *obj) { this->cards_.push_back(obj); } + void register_trigger(WS18x0UARTTrigger *trig) { this->triggers_.push_back(trig); } + + float get_setup_priority() const override { return setup_priority::DATA; } + + uint32_t id() { return (uint32_t) raw_; } + uint64_t raw() { return raw_; } + + protected: + int8_t read_state_{-1}; + uint64_t raw_{0}; + uint64_t last_raw_{0}; + std::vector cards_; + std::vector triggers_; +}; + +class WS18x0UARTBinarySensor : public binary_sensor::BinarySensorInitiallyOff { + public: + void set_id(uint32_t id) { id_ = id; } + + bool process(uint32_t id) { + if (this->id_ == id) { + this->publish_state(true); + yield(); + this->publish_state(false); + return true; + } + return false; + } + + protected: + uint32_t id_; +}; + +class WS18x0UARTTrigger : public Trigger { + public: + void process(uint32_t uid) { this->trigger(uid); } +}; + +} // namespace ws18x0_uart +} // namespace esphome