diff --git a/CODEOWNERS b/CODEOWNERS index 0f43cd9749..b752c9c5ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -145,6 +145,7 @@ esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its +esphome/components/ds248x/* @tomwellnitz esphome/components/dsmr/* @glmnet @PolarGoose esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M diff --git a/esphome/components/ds248x/__init__.py b/esphome/components/ds248x/__init__.py new file mode 100644 index 0000000000..5a26ceab50 --- /dev/null +++ b/esphome/components/ds248x/__init__.py @@ -0,0 +1,112 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE + +CODEOWNERS = ["@tomwellnitz"] +MULTI_CONF = True +DEPENDENCIES = ["i2c"] + +CONF_DS248X_ID = "ds248x_id" +CONF_BUS_SLEEP = "bus_sleep" +CONF_HUB_SLEEP = "hub_sleep" +CONF_ACTIVE_PULLUP = "active_pullup" + +CONF_RESET_LOW_TIME = "reset_low_time" +CONF_MASTER_SAMPLE_TIME = "master_sample_time" +CONF_WRITE_0_LOW_TIME = "write_0_low_time" +CONF_RECOVERY_TIME = "recovery_time" +CONF_ACTIVE_PULLUP_RESISTANCE = "active_pullup_resistance" + +TYPE_DS2482_100 = "ds2482-100" +TYPE_DS2482_101 = "ds2482-101" +TYPE_DS2482_800 = "ds2482-800" +TYPE_DS2484 = "ds2484" + +CHANNEL_COUNTS = { + TYPE_DS2482_100: 1, + TYPE_DS2482_101: 1, + TYPE_DS2482_800: 8, + TYPE_DS2484: 1, +} + +ds248x_ns = cg.esphome_ns.namespace("ds248x") +DS248xComponent = ds248x_ns.class_("DS248xComponent", cg.Component, i2c.I2CDevice) + + +def _component_schema(*extras): + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DS248xComponent), + cv.Optional(CONF_ACTIVE_PULLUP, default=False): cv.boolean, + } + ) + for extra in extras: + schema = schema.extend(extra) + return schema.extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x18)) + + +SLEEP_SCHEMA = { + cv.Optional(CONF_SLEEP_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_BUS_SLEEP, default=False): cv.boolean, + cv.Optional(CONF_HUB_SLEEP, default=False): cv.boolean, +} + +DS2484_SCHEMA = { + cv.Optional(CONF_RESET_LOW_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_MASTER_SAMPLE_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_WRITE_0_LOW_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_RECOVERY_TIME): cv.int_range(min=0, max=15), + cv.Optional(CONF_ACTIVE_PULLUP_RESISTANCE): cv.enum( + { + # DS2484 Table 7: value codes 0-5 map to 500 ohm, 6-15 map to 1000 ohm. + "500ohm": 0, + "1000ohm": 6, + } + ), +} + +CONFIG_SCHEMA = cv.typed_schema( + { + TYPE_DS2482_100: _component_schema(), + TYPE_DS2482_101: _component_schema(SLEEP_SCHEMA), + TYPE_DS2482_800: _component_schema(), + TYPE_DS2484: _component_schema(SLEEP_SCHEMA, DS2484_SCHEMA), + }, + key=CONF_TYPE, + lower=True, +) + + +def get_channel_count(config): + return CHANNEL_COUNTS[config[CONF_TYPE]] + + +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_active_pullup(config[CONF_ACTIVE_PULLUP])) + cg.add(var.set_channel_count(get_channel_count(config))) + + if CONF_BUS_SLEEP in config: + cg.add(var.set_bus_sleep(config[CONF_BUS_SLEEP])) + if CONF_HUB_SLEEP in config: + cg.add(var.set_hub_sleep(config[CONF_HUB_SLEEP])) + + if CONF_RESET_LOW_TIME in config: + cg.add(var.set_val_trstl(config[CONF_RESET_LOW_TIME])) + if CONF_MASTER_SAMPLE_TIME in config: + cg.add(var.set_val_tmsp(config[CONF_MASTER_SAMPLE_TIME])) + if CONF_WRITE_0_LOW_TIME in config: + cg.add(var.set_val_tw0l(config[CONF_WRITE_0_LOW_TIME])) + if CONF_RECOVERY_TIME in config: + cg.add(var.set_val_trec0(config[CONF_RECOVERY_TIME])) + if CONF_ACTIVE_PULLUP_RESISTANCE in config: + cg.add(var.set_val_rwpu(config[CONF_ACTIVE_PULLUP_RESISTANCE])) + + if CONF_SLEEP_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) + cg.add(var.set_sleep_pin(pin)) diff --git a/esphome/components/ds248x/ds248x.cpp b/esphome/components/ds248x/ds248x.cpp new file mode 100644 index 0000000000..c8f7395119 --- /dev/null +++ b/esphome/components/ds248x/ds248x.cpp @@ -0,0 +1,320 @@ +#include "ds248x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome::ds248x { + +static const char *const TAG = "ds248x"; + +void DS248xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS248x..."); + + // Wake up device if sleep pin is configured + if (this->sleep_pin_) { + this->sleep_pin_->setup(); + this->sleep_pin_->pin_mode(esphome::gpio::FLAG_OUTPUT); + this->sleep_pin_->digital_write(true); // Wake up + delay(1); // DS2482-101 Datasheet: tOSCWUP = 100μs (using 10x margin) + } + + // Probe device + ESP_LOGD(TAG, "Probing DS248x..."); + uint8_t status = 0; + if (this->read(&status, 1) == i2c::ERROR_OK) { + ESP_LOGD(TAG, "Device responded! Status: 0x%02x", status); + } else { + ESP_LOGW(TAG, "Device did not respond. Trying reset anyway..."); + } + + if (!this->device_reset_()) { + ESP_LOGW(TAG, "DS248x reset failed during setup!"); + } + + // Configure device + if (!this->device_configure_()) { + ESP_LOGE(TAG, "DS248x configuration failed!"); + this->mark_failed(); + return; + } + + // Reset to Channel 0 + this->select_channel(0); + + ESP_LOGI(TAG, "DS248x initialized successfully."); +} + +void DS248xComponent::on_shutdown() { + if (this->sleep_pin_ && (this->hub_sleep_ || this->bus_sleep_)) { + this->sleep_pin_->digital_write(false); // Sleep + } +} + +void DS248xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "DS248x:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Channel Count: %d", this->channel_count_); + ESP_LOGCONFIG(TAG, " Active Pullup: %s", YESNO(this->active_pullup_)); + if (this->ds2484_mode_) { + ESP_LOGCONFIG(TAG, " DS2484 Mode: enabled"); + } +} + +// --- Internal Helpers --- + +// Datasheet command durations are sub-2ms; allow a little margin before forcing recovery. +static constexpr uint32_t BUSY_TIMEOUT_MS = 5; + +bool DS248xComponent::set_read_pointer_(uint8_t ptr) { return this->write_byte(DS248X_COMMAND_SETREADPTR, ptr); } + +bool DS248xComponent::wait_busy_() { + uint32_t start = millis(); + do { + uint8_t status; + if (this->read(&status, 1) == i2c::ERROR_OK && !(status & DS248X_STATUS_BUSY)) + return true; + delayMicroseconds(100); + } while (millis() - start < BUSY_TIMEOUT_MS); + ESP_LOGW(TAG, "DS248x busy timeout"); + bool recovered = this->device_reset_() && this->device_configure_(); + this->current_channel_ = -1; + if (!recovered) { + ESP_LOGE(TAG, "DS248x recovery failed after busy timeout"); + this->mark_failed(); + } + return false; +} + +bool DS248xComponent::device_reset_() { + ESP_LOGD(TAG, "Resetting device..."); + uint8_t cmd = DS248X_COMMAND_RESET; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + uint8_t status; + if (this->read(&status, 1) != i2c::ERROR_OK) + return false; + + if (!(status & DS248X_STATUS_RST)) { + ESP_LOGW(TAG, "Device reset failed (RST bit not set)"); + return false; + } + + this->current_channel_ = -1; + return true; +} + +bool DS248xComponent::device_configure_() { + ESP_LOGD(TAG, "Configuring device..."); + + if (!this->write_config_()) { + ESP_LOGW(TAG, "Config write/verify failed"); + return false; + } + + ESP_LOGD(TAG, "Configured successfully"); + + // DS2484 Configuration + if (this->ds2484_mode_) { + if (this->ds2484_trstl_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TRSTL, this->ds2484_trstl_)) + return false; + if (this->ds2484_tmsp_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TMSP, this->ds2484_tmsp_)) + return false; + if (this->ds2484_tw0l_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TW0L, this->ds2484_tw0l_)) + return false; + if (this->ds2484_trec0_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_TREC0, this->ds2484_trec0_)) + return false; + if (this->ds2484_rwpu_ != DS2484_PARAM_UNSET && + !this->configure_ds2484_port_(DS2484_PORT_PARAM_RWPU, this->ds2484_rwpu_)) + return false; + } + + return true; +} + +bool DS248xComponent::configure_ds2484_port_(uint8_t param, uint8_t val) { + uint8_t cmd = DS2484_COMMAND_ADJUSTPORT; + // Control Byte format (DS2484 Table 6): P[2:0] in bits 7:5, OD in bit 4, VAL[3:0] in bits 3:0 + uint8_t data = ((param & 0x07) << 5) | (val & 0x0F); + + // The DS2484 always acknowledges the Adjust 1-Wire Port control byte (datasheet "Adjust + // 1-Wire Port"), so a successful write confirms the update. We deliberately do not read + // back to verify: a single read of the Port Configuration register always returns the + // fixed 8-byte report starting at Byte 1 (tRSTL standard speed), not the parameter that + // was just written, so a per-parameter readback comparison would spuriously fail for + // tMSP/tW0L/tREC0/RWPU. + if (!this->write_byte(cmd, data)) { + ESP_LOGW(TAG, "DS2484 port config failed (param %d)", param); + return false; + } + + return this->set_read_pointer_(DS248X_POINTER_STATUS); +} + +bool DS248xComponent::write_config_() { + uint8_t config = 0; + if (this->active_pullup_) + config |= DS248X_CONFIG_ACTIVE_PULLUP; + + // The DS248x only accepts the config byte if the upper nibble is the one's-complement of the lower nibble. + uint8_t config_byte = (config & 0x0F) | ((~config & 0x0F) << 4); + + if (!this->write_byte(DS248X_COMMAND_WRITECONFIG, config_byte)) { + ESP_LOGW(TAG, "Failed to write config byte"); + return false; + } + + if (!this->set_read_pointer_(DS248X_POINTER_CONFIG)) { + return false; + } + + uint8_t read_config; + if (this->read(&read_config, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Failed to read back config byte"); + return false; + } + + if ((read_config & 0x0F) != (config_byte & 0x0F)) { + ESP_LOGW(TAG, "Config mismatch! Wrote 0x%02x, Read 0x%02x", config_byte, read_config); + return false; + } + + return this->set_read_pointer_(DS248X_POINTER_STATUS); +} + +// --- Channel Selection --- + +// Channel select codes: write code -> expected read code +static constexpr uint8_t CHANNEL_WRITE_CODES[8] = {0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87}; +static constexpr uint8_t CHANNEL_READ_CODES[8] = {0xB8, 0xB1, 0xAA, 0xA3, 0x9C, 0x95, 0x8E, 0x87}; + +bool DS248xComponent::select_channel(uint8_t channel) { + if (this->channel_count_ <= 1) + return true; + if (channel >= this->channel_count_) + return false; + + if (this->current_channel_ == channel) + return true; + + if (!this->write_byte(DS248X_COMMAND_CHANNELSELECT, CHANNEL_WRITE_CODES[channel])) { + this->current_channel_ = -1; + return false; + } + + uint8_t read_code; + if (this->read(&read_code, 1) != i2c::ERROR_OK) { + this->current_channel_ = -1; + return false; + } + + if (read_code != CHANNEL_READ_CODES[channel]) { + ESP_LOGW(TAG, "Channel select failed! Expected 0x%02x, got 0x%02x", CHANNEL_READ_CODES[channel], read_code); + this->current_channel_ = -1; + return false; + } + + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + this->current_channel_ = channel; + return true; +} + +// --- 1-Wire Bus Operations --- + +bool DS248xComponent::ow_reset(bool &presence) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + uint8_t cmd = DS248X_COMMAND_RESETWIRE; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "ow_reset: wait busy failed"); + return false; + } + + uint8_t status; + if (this->read(&status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "ow_reset: read status failed"); + return false; + } + + if (status & DS248X_STATUS_SD) { + ESP_LOGW(TAG, "Short detected on 1-Wire bus!"); + return false; + } + + presence = (status & DS248X_STATUS_PPD); + return true; +} + +bool DS248xComponent::ow_write_byte(uint8_t byte) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "Device busy before writing byte 0x%02x", byte); + return false; + } + + uint8_t cmd[2] = {DS248X_COMMAND_WRITEBYTE, byte}; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C write failed for byte 0x%02x", byte); + return false; + } + + if (!this->wait_busy_()) { + ESP_LOGW(TAG, "Timeout waiting for write byte to complete!"); + return false; + } + + return true; +} + +bool DS248xComponent::ow_read_byte(uint8_t &byte) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + uint8_t cmd = DS248X_COMMAND_READBYTE; + if (this->write(&cmd, 1) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) + return false; + + if (!this->set_read_pointer_(DS248X_POINTER_DATA)) + return false; + + if (this->read(&byte, 1) != i2c::ERROR_OK) + return false; + + return true; +} + +bool DS248xComponent::search_triplet(bool search_direction, uint8_t &status) { + if (!this->set_read_pointer_(DS248X_POINTER_STATUS)) + return false; + + // DS248x Datasheet: 1-Wire Triplet command requires 2 bytes: + // Byte 1: Command code 0x78 + // Byte 2: Direction byte (bit 7 = V, search direction if discrepancy) + uint8_t buffer[2] = {DS248X_COMMAND_TRIPLET, static_cast(search_direction ? 0x80 : 0x00)}; + if (this->write(buffer, 2) != i2c::ERROR_OK) + return false; + + if (!this->wait_busy_()) + return false; + + if (this->read(&status, 1) != i2c::ERROR_OK) + return false; + + return true; +} + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x.h b/esphome/components/ds248x/ds248x.h new file mode 100644 index 0000000000..0873ee0e2a --- /dev/null +++ b/esphome/components/ds248x/ds248x.h @@ -0,0 +1,133 @@ +#pragma once + +// DS248x I2C-to-1-Wire Bridge Family +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-100.pdf +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-800.pdf +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2484.pdf + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::ds248x { + +// DS248x I2C Commands +static constexpr uint8_t DS248X_COMMAND_RESET = 0xF0; +static constexpr uint8_t DS248X_COMMAND_SETREADPTR = 0xE1; +static constexpr uint8_t DS248X_COMMAND_WRITECONFIG = 0xD2; +static constexpr uint8_t DS248X_COMMAND_CHANNELSELECT = 0xC3; +static constexpr uint8_t DS248X_COMMAND_RESETWIRE = 0xB4; +static constexpr uint8_t DS248X_COMMAND_WRITEBYTE = 0xA5; +static constexpr uint8_t DS248X_COMMAND_READBYTE = 0x96; +static constexpr uint8_t DS248X_COMMAND_TRIPLET = 0x78; +static constexpr uint8_t DS2484_COMMAND_ADJUSTPORT = 0xC3; + +// DS2484 "Adjust 1-Wire Port" parameter codes (datasheet Table 6, control byte P[2:0]) +static constexpr uint8_t DS2484_PORT_PARAM_TRSTL = 0x0; +static constexpr uint8_t DS2484_PORT_PARAM_TMSP = 0x1; +static constexpr uint8_t DS2484_PORT_PARAM_TW0L = 0x2; +static constexpr uint8_t DS2484_PORT_PARAM_TREC0 = 0x3; +static constexpr uint8_t DS2484_PORT_PARAM_RWPU = 0x4; + +// DS248x Status Register Bits +static constexpr uint8_t DS248X_STATUS_BUSY = 0x01; +static constexpr uint8_t DS248X_STATUS_PPD = 0x02; +static constexpr uint8_t DS248X_STATUS_SD = 0x04; +static constexpr uint8_t DS248X_STATUS_RST = 0x10; +static constexpr uint8_t DS248X_STATUS_SBR = 0x20; +static constexpr uint8_t DS248X_STATUS_TSB = 0x40; +static constexpr uint8_t DS248X_STATUS_DIR = 0x80; + +// DS248x Register Pointers +static constexpr uint8_t DS248X_POINTER_STATUS = 0xF0; +static constexpr uint8_t DS248X_POINTER_DATA = 0xE1; +static constexpr uint8_t DS248X_POINTER_CONFIG = 0xC3; + +// DS248x Configuration Bits +static constexpr uint8_t DS248X_CONFIG_ACTIVE_PULLUP = 0x01; + +/** + * @brief DS248x I2C-to-1-Wire Bridge Component. + * + * This component manages the DS248x chip (DS2482-100, DS2482-800, DS2484). + * It provides low-level 1-Wire bus operations via I2C. + * + * Usage: Configure DS248xOneWireBus instances for each channel. + * These buses implement the one_wire::OneWireBus interface for compatibility + * with all existing 1-Wire device components (dallas_temp, etc.). + */ +class DS248xComponent : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void on_shutdown() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_sleep_pin(InternalGPIOPin *pin) { this->sleep_pin_ = pin; } + void set_bus_sleep(bool enabled) { this->bus_sleep_ = enabled; } + void set_hub_sleep(bool enabled) { this->hub_sleep_ = enabled; } + void set_channel_count(uint8_t count) { this->channel_count_ = count; } + void set_active_pullup(bool enabled) { this->active_pullup_ = enabled; } + + // DS2484 Timing Parameters + void set_val_trstl(uint8_t val) { + this->ds2484_trstl_ = val; + this->ds2484_mode_ = true; + } + void set_val_tmsp(uint8_t val) { + this->ds2484_tmsp_ = val; + this->ds2484_mode_ = true; + } + void set_val_tw0l(uint8_t val) { + this->ds2484_tw0l_ = val; + this->ds2484_mode_ = true; + } + void set_val_trec0(uint8_t val) { + this->ds2484_trec0_ = val; + this->ds2484_mode_ = true; + } + void set_val_rwpu(uint8_t val) { + this->ds2484_rwpu_ = val; + this->ds2484_mode_ = true; + } + + /// Get the channel count (1 for DS2482-100/DS2484, 8 for DS2482-800) + uint8_t get_channel_count() const { return this->channel_count_; } + + // --- Core 1-Wire API (used by DS248xOneWireBus) --- + bool select_channel(uint8_t channel); + bool ow_reset(bool &presence); + bool ow_write_byte(uint8_t byte); + bool ow_read_byte(uint8_t &byte); + + // --- Search support (used by DS248xOneWireBus) --- + bool search_triplet(bool search_direction, uint8_t &status); + + protected: + InternalGPIOPin *sleep_pin_{nullptr}; + uint8_t channel_count_ = 1; + bool bus_sleep_{false}; + bool hub_sleep_{false}; + bool active_pullup_ = false; + + // DS2484 Config + bool ds2484_mode_ = false; + static constexpr uint8_t DS2484_PARAM_UNSET = 0xFF; + uint8_t ds2484_trstl_{DS2484_PARAM_UNSET}; + uint8_t ds2484_tmsp_{DS2484_PARAM_UNSET}; + uint8_t ds2484_tw0l_{DS2484_PARAM_UNSET}; + uint8_t ds2484_trec0_{DS2484_PARAM_UNSET}; + uint8_t ds2484_rwpu_{DS2484_PARAM_UNSET}; + + int8_t current_channel_{-1}; + + // Internal helpers + bool set_read_pointer_(uint8_t ptr); + bool wait_busy_(); + bool device_reset_(); + bool device_configure_(); + bool configure_ds2484_port_(uint8_t param, uint8_t val); + bool write_config_(); +}; + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x_one_wire_bus.cpp b/esphome/components/ds248x/ds248x_one_wire_bus.cpp new file mode 100644 index 0000000000..ed5b05bab7 --- /dev/null +++ b/esphome/components/ds248x/ds248x_one_wire_bus.cpp @@ -0,0 +1,171 @@ +#include "ds248x_one_wire_bus.h" +#include "ds248x.h" +#include "esphome/core/log.h" + +namespace esphome::ds248x { + +static const char *const TAG = "ds248x.one_wire"; + +void DS248xOneWireBus::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS248x 1-Wire Bus (Channel %d)...", this->channel_); + + // Parent setup happens in DS248xComponent::setup() + // We just need to scan for devices on this channel + if (!this->ensure_channel_()) { + ESP_LOGE(TAG, "Failed to select channel %d during setup", this->channel_); + this->mark_failed(); + return; + } + + // Perform device search on this channel + this->search(); + + ESP_LOGCONFIG(TAG, "Found %zu devices on channel %d", this->devices_.size(), this->channel_); +} + +void DS248xOneWireBus::dump_config() { + ESP_LOGCONFIG(TAG, "DS248x 1-Wire Bus (Channel %d):", this->channel_); + this->dump_devices_(TAG); +} + +bool DS248xOneWireBus::ensure_channel_() { + if (this->parent_ == nullptr) { + ESP_LOGE(TAG, "Parent not set!"); + return false; + } + return this->parent_->select_channel(this->channel_); +} + +int DS248xOneWireBus::reset_int() { + if (!this->ensure_channel_()) { + return -1; + } + + bool presence = false; + if (!this->parent_->ow_reset(presence)) { + return -1; + } + return presence ? 1 : 0; +} + +void DS248xOneWireBus::write8(uint8_t val) { + if (!this->ensure_channel_()) { + return; + } + if (!this->parent_->ow_write_byte(val)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X on channel %d", val, this->channel_); + } +} + +void DS248xOneWireBus::write64(uint64_t val) { + if (!this->ensure_channel_()) { + return; + } + for (uint8_t i = 0; i < 8; i++) { + uint8_t byte = static_cast(val >> (i * 8)); + if (!this->parent_->ow_write_byte(byte)) { + ESP_LOGE(TAG, "Failed to write byte %d/8 (0x%02X) on channel %d - aborting write64", i + 1, byte, this->channel_); + return; // Stop writing to prevent sending corrupted data + } + } +} + +uint8_t DS248xOneWireBus::read8() { + if (!this->ensure_channel_()) { + return 0; + } + uint8_t value = 0; + if (!this->parent_->ow_read_byte(value)) { + ESP_LOGE(TAG, "Failed to read byte on channel %d", this->channel_); + } + return value; +} + +uint64_t DS248xOneWireBus::read64() { + if (!this->ensure_channel_()) { + return 0; + } + uint64_t value = 0; + for (uint8_t i = 0; i < 8; i++) { + uint8_t byte = 0; + if (!this->parent_->ow_read_byte(byte)) { + ESP_LOGE(TAG, "Failed to read byte %d/8 on channel %d - returning partial data", i + 1, this->channel_); + return value; // Return partial data to avoid blocking, caller should validate + } + value |= (static_cast(byte) << (i * 8)); + } + return value; +} + +void DS248xOneWireBus::reset_search() { + this->search_last_discrepancy_ = 0; + this->search_last_device_flag_ = false; + this->search_address_ = 0; +} + +uint64_t DS248xOneWireBus::search_int() { + if (!this->ensure_channel_()) { + return 0; + } + + if (this->search_last_device_flag_) { + return 0; + } + + uint8_t last_zero = 0; + uint64_t address = this->search_address_; + + // Iterate through all 64 bits + for (uint8_t bit_number = 1; bit_number <= 64; bit_number++) { + uint64_t bit_mask = 1ULL << (bit_number - 1); + + // Determine search direction + bool search_direction; + if (bit_number < this->search_last_discrepancy_) { + search_direction = (address & bit_mask) != 0; + } else { + search_direction = (bit_number == this->search_last_discrepancy_); + } + + // Perform triplet operation + uint8_t status = 0; + if (!this->parent_->search_triplet(search_direction, status)) { + ESP_LOGW(TAG, "1-Wire triplet failed at bit %d on channel %d - aborting search", bit_number, this->channel_); + this->reset_search(); + return 0; + } + + bool id_bit = (status & DS248X_STATUS_SBR) != 0; + bool cmp_id_bit = (status & DS248X_STATUS_TSB) != 0; + bool dir_taken = (status & DS248X_STATUS_DIR) != 0; + + if (id_bit && cmp_id_bit) { + // No devices participating + this->reset_search(); + return 0; + } + + if (!id_bit && !cmp_id_bit && !dir_taken) { + // Discrepancy, went 0 - record position + last_zero = bit_number; + } + + // Update address based on direction taken + if (dir_taken) { + address |= bit_mask; + } else { + address &= ~bit_mask; + } + } + + // Search successful + this->search_last_discrepancy_ = last_zero; + if (last_zero == 0) { + this->search_last_device_flag_ = true; + } + this->search_address_ = address; + + return address; +} + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/ds248x_one_wire_bus.h b/esphome/components/ds248x/ds248x_one_wire_bus.h new file mode 100644 index 0000000000..0591796d60 --- /dev/null +++ b/esphome/components/ds248x/ds248x_one_wire_bus.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/one_wire/one_wire_bus.h" + +namespace esphome::ds248x { + +class DS248xComponent; + +/** + * @brief OneWireBus implementation for DS248x I2C-to-1-Wire bridges. + * + * This class wraps the DS248xComponent to provide the one_wire::OneWireBus interface, + * enabling compatibility with all existing 1-Wire device components (dallas_temp, etc.). + * + * For DS2482-800, multiple instances of this class can be created (one per channel). + * For DS2482-100/DS2484, a single instance is used. + */ +class DS248xOneWireBus : public one_wire::OneWireBus, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS - 1.0f; } + + /// Set the parent DS248x component + void set_parent(DS248xComponent *parent) { this->parent_ = parent; } + + /// Set the 1-Wire channel (0-7, only relevant for DS2482-800) + void set_channel(uint8_t channel) { this->channel_ = channel; } + + /// Get the channel number + uint8_t get_channel() const { return this->channel_; } + + // OneWireBus interface implementation + int reset_int() override; + void write8(uint8_t val) override; + void write64(uint64_t val) override; + uint8_t read8() override; + uint64_t read64() override; + + protected: + void reset_search() override; + uint64_t search_int() override; + + /// Select the channel on the DS248x before any 1-Wire operation + bool ensure_channel_(); + + DS248xComponent *parent_{nullptr}; + uint8_t channel_{0}; + + // Search state + uint64_t search_address_{0}; + uint8_t search_last_discrepancy_{0}; + bool search_last_device_flag_{false}; +}; + +} // namespace esphome::ds248x diff --git a/esphome/components/ds248x/one_wire.py b/esphome/components/ds248x/one_wire.py new file mode 100644 index 0000000000..19861eae36 --- /dev/null +++ b/esphome/components/ds248x/one_wire.py @@ -0,0 +1,56 @@ +"""DS248x 1-Wire Bus Platform. + +This platform creates one_wire bus instances backed by a DS248x I2C-to-1-Wire bridge. +It supports DS2482-100/101 (single channel), DS2482-800 (8 channels), and DS2484 (single channel). + +For multi-channel devices (DS2482-800), create one platform entry per channel. +Each entry becomes a separate one_wire bus that can be used by dallas_temp and other 1-Wire devices. +""" + +from esphome import final_validate as fv +import esphome.codegen as cg +from esphome.components.one_wire import OneWireBus +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID + +from . import CONF_DS248X_ID, DS248xComponent, ds248x_ns, get_channel_count + +CODEOWNERS = ["@tomwellnitz"] +DEPENDENCIES = ["ds248x"] + +DS248xOneWireBus = ds248x_ns.class_("DS248xOneWireBus", OneWireBus, cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DS248xOneWireBus), + cv.GenerateID(CONF_DS248X_ID): cv.use_id(DS248xComponent), + cv.Optional(CONF_CHANNEL, default=0): cv.int_range(min=0, max=7), + } +).extend(cv.COMPONENT_SCHEMA) + + +def _final_validate(config): + """Validate that the channel is within the parent's channel count.""" + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1] + parent_config = fconf.get_config_for_path(path) + channel_count = get_channel_count(parent_config) + channel = config[CONF_CHANNEL] + + if channel >= channel_count: + raise cv.Invalid( + f"Channel {channel} is invalid for DS248x with {channel_count} channel(s). " + f"Valid range: 0-{channel_count - 1}" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + parent = await cg.get_variable(config[CONF_DS248X_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/tests/components/ds248x/common.yaml b/tests/components/ds248x/common.yaml new file mode 100644 index 0000000000..53ae56f20a --- /dev/null +++ b/tests/components/ds248x/common.yaml @@ -0,0 +1,115 @@ +# Combined DS248x test covering all chip variants and options: +# - DS2482-100: active pullup, multiple sensors + index access +# - DS2482-101: sleep pin, bus_sleep / hub_sleep +# - DS2482-800: all 8 channels +# - DS2484: adjustable 1-Wire timing + RWPU pullup resistor selection +ds248x: + - id: ds2482_100 + address: 0x18 + type: ds2482-100 + active_pullup: true + - id: ds2482_101 + address: 0x19 + type: ds2482-101 + active_pullup: true + sleep_pin: + number: GPIO12 + inverted: false + bus_sleep: true + hub_sleep: true + - id: ds2482_800 + address: 0x1a + type: ds2482-800 + active_pullup: true + - id: ds2484_hub + address: 0x1b + type: ds2484 + active_pullup: true + # DS2484-specific 1-Wire timing parameters (optional fine-tuning) + reset_low_time: 8 # tRSTL: Reset low time + master_sample_time: 8 # tMSP: Master sample point + write_0_low_time: 8 # tW0L: Write-0 low time + recovery_time: 8 # tREC0: Recovery time + active_pullup_resistance: 1000ohm # RWPU: weak pullup resistor selection + +one_wire: + - platform: ds248x + ds248x_id: ds2482_100 + channel: 0 + id: ow_100 + - platform: ds248x + ds248x_id: ds2482_101 + channel: 0 + id: ow_101 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 0 + id: ow_800_0 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 1 + id: ow_800_1 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 2 + id: ow_800_2 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 3 + id: ow_800_3 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 4 + id: ow_800_4 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 5 + id: ow_800_5 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 6 + id: ow_800_6 + - platform: ds248x + ds248x_id: ds2482_800 + channel: 7 + id: ow_800_7 + - platform: ds248x + ds248x_id: ds2484_hub + channel: 0 + id: ow_2484 + +sensor: + # DS2482-100: explicit address + index-based access on the same bus + - platform: dallas_temp + one_wire_id: ow_100 + address: 0x1c0000031edd2a28 + name: Temp 100 by address + resolution: 12 + - platform: dallas_temp + one_wire_id: ow_100 + index: 0 + name: Temp 100 by index + # DS2482-101 (sleep variant) + - platform: dallas_temp + one_wire_id: ow_101 + address: 0x578295491f64ff28 + name: Temp 101 + # DS2482-800: sensors on a few of the eight channels + - platform: dallas_temp + one_wire_id: ow_800_0 + address: 0x1c0000031edd2a28 + name: Temp 800 CH0 + - platform: dallas_temp + one_wire_id: ow_800_3 + index: 0 + name: Temp 800 CH3 by index + - platform: dallas_temp + one_wire_id: ow_800_7 + address: 0x2800000123456789 + name: Temp 800 CH7 + # DS2484 (adjustable timing) + - platform: dallas_temp + one_wire_id: ow_2484 + address: 0x1c0000031edd2a28 + name: Temp 2484 + resolution: 12 diff --git a/tests/components/ds248x/test.esp32-ard.yaml b/tests/components/ds248x/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/ds248x/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.esp32-idf.yaml b/tests/components/ds248x/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/ds248x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.esp8266-ard.yaml b/tests/components/ds248x/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/ds248x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ds248x/test.rp2040-ard.yaml b/tests/components/ds248x/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/ds248x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml