Skip to content

Instantly share code, notes, and snippets.

@cemizm
Created February 4, 2025 20:19
Show Gist options
  • Select an option

  • Save cemizm/57f01d8d2f23155615d28562ea2e7edc to your computer and use it in GitHub Desktop.

Select an option

Save cemizm/57f01d8d2f23155615d28562ea2e7edc to your computer and use it in GitHub Desktop.
ESPHome DL-Bus
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import (
CONF_ID,
CONF_PIN
)
dlbus_ns = cg.esphome_ns.namespace("dlbus")
DlBusComponent = dlbus_ns.class_("DlBus", cg.Component)
CONF_INPUT_ID = "input"
INPUT_IDS = {f'S{i}':i-1 for i in range(1, 17)}
CONF_OUTPUT_ID = "output"
OUTPUT_IDS = {f'A{i}':i-1 for i in range(1, 17)}
CONF_RPM_ID = "rpm"
RPM_IDS = {"A1": 0, "A2": 1, "A6": 2, "A7": 3}
CONF_POWER_ID = "power"
POWER_IDS = {"P1":0, "P2":1}
CONF_ENERGY_ID = "energy"
ENERGY_IDS = {"E1":0, "E2":1}
CONF_DLBUS_ID = "dlbus_id"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlBusComponent)
}
)
.extend({cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema)})
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS, STATE_CLASS_MEASUREMENT
from .. import dlbus_ns, DlBusComponent, CONF_DLBUS_ID, CONF_INPUT_ID, INPUT_IDS, CONF_OUTPUT_ID, OUTPUT_IDS
DlBusBinarySensor = dlbus_ns.class_("DlBusBinarySensor", cg.Component)
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(
DlBusBinarySensor
)
.extend(
{
cv.GenerateID(CONF_DLBUS_ID): cv.use_id(DlBusComponent),
cv.Optional(CONF_INPUT_ID) : cv.enum(INPUT_IDS),
cv.Optional(CONF_OUTPUT_ID) : cv.enum(OUTPUT_IDS)
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
dlbus = await cg.get_variable(config[CONF_DLBUS_ID])
cg.add(var.set_dlbus(dlbus))
if CONF_INPUT_ID in config:
cg.add(var.set_input_id(config[CONF_INPUT_ID]))
elif CONF_OUTPUT_ID in config:
cg.add(var.set_output_id(config[CONF_OUTPUT_ID]))
#include "dlbus_binary_sensor.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome
{
namespace dlbus
{
static const char *const TAG = "DlBus.BinarySensor";
// different bit length per device type
void DlBusBinarySensor::setup()
{
ESP_LOGCONFIG(TAG, "Setting up DlBusBinarySensor...");
}
void DlBusBinarySensor::dump_config()
{
ESP_LOGCONFIG(TAG, "DlBus BinarySensor:");
if(this->input_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " Input: S%d", this->input_id_ + 1);
if(this->output_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " Output: A%d", this->output_id_ + 1);
LOG_BINARY_SENSOR(" ", "DlBusBinarySensor", this);
LOG_UPDATE_INTERVAL(this);
}
void DlBusBinarySensor::update()
{
bool val = false;
DlRx::UvrSensor sensor;
if(this->input_id_ != UINT8_MAX && this->dlBus_->get_sensor(this->input_id_, sensor))
val = sensor.get_binary_value();
bool tmp;
if(this->output_id_ != UINT8_MAX && this->dlBus_->get_output(this->output_id_, tmp))
val = tmp;
publish_state(val);
}
float DlBusBinarySensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace duty_cycle
} // namespace esphome
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/dlbus/dlbus.h"
namespace esphome
{
namespace dlbus
{
class DlBusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent
{
public:
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override;
void set_dlbus(DlBus *dlBus) { this->dlBus_ = dlBus; }
void set_input_id(uint8_t input_id) { this->input_id_ = input_id; }
void set_output_id(uint8_t output_id) { this->output_id_ = output_id; }
protected:
DlBus *dlBus_{nullptr};
uint8_t input_id_{UINT8_MAX};
uint8_t output_id_{UINT8_MAX};
};
} // namespace duty_cycle
} // namespace esphome
#include "dlbus.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome
{
namespace dlbus
{
static const char *const TAG = "DlBus";
// different bit length per device type
void DlBus::setup()
{
ESP_LOGCONFIG(TAG, "Setting up DlBus Component...");
dlrx_.set_frame_handler([this](uint8_t *data, uint8_t size)
{
memcpy(this->frame_.buffer, data, size);
dataValid = true; });
dlrx_.setup(pin_);
}
void DlBus::dump_config()
{
ESP_LOGCONFIG(TAG, "DlBus Component:");
LOG_PIN(" Pin: ", this->pin_);
}
bool DlBus::get_sensor(uint8_t index, DlRx::UvrSensor &sensor)
{
if (!dataValid)
return false;
if (index > 16)
return false;
sensor = this->frame_.UVR1611.sensor[index];
return true;
}
bool DlBus::get_rpm(uint8_t index, float &rpm)
{
if (!dataValid)
return false;
if (index > 3)
return false;
rpm = this->frame_.UVR1611.rpm_output[index];
return true;
}
bool DlBus::get_heatmeter(uint8_t index, DlRx::UvrPower &heat)
{
if (!dataValid)
return false;
if (index > 1)
return false;
heat = this->frame_.UVR1611.heatmeter[index];
return true;
}
bool DlBus::get_output(uint8_t index, bool &state)
{
if (!dataValid)
return false;
if (index > 16)
return false;
uint16_t pos = (0x01 << index);
state = (this->frame_.UVR1611.actor_states & pos) == pos;
return true;
}
float DlBus::get_setup_priority() const { return setup_priority::DATA; }
} // namespace duty_cycle
} // namespace esphome
#pragma once
#include "DlRx.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome
{
namespace dlbus
{
class DlBus : public Component
{
public:
void setup() override;
float get_setup_priority() const override;
void dump_config() override;
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void update_frame(uint8_t *data, uint8_t size);
bool get_sensor(uint8_t index, DlRx::UvrSensor &sensor);
bool get_output(uint8_t index, bool &state);
bool get_rpm(uint8_t index, float &value);
bool get_heatmeter(uint8_t index, DlRx::UvrPower &heat);
protected:
InternalGPIOPin *pin_;
DlRx dlrx_;
DlRx::Frame frame_;
bool dataValid{false};
};
} // namespace duty_cycle
} // namespace esphome
#include "DlRx.h"
namespace esphome
{
namespace dlbus
{
static constexpr uint16_t INIT_SAMPLES_MAX = 2000u; // number of rising edges to collect for
static constexpr uint16_t INIT_SAMPLES_MIN = 100u; // collect at least this
static constexpr float INIT_SAMPLES_RANGE = 0.03f; // Range of a valid bit period
static constexpr uint8_t CLOCK_SYNC_SAMPLES = 16u; // number of clock synchronization bits
void DlRx::setup(InternalGPIOPin *pin)
{
pin->setup();
this->pin_ = pin->to_isr();
pin->attach_interrupt(DlRx::pin_isr, this, gpio::INTERRUPT_ANY_EDGE);
}
void IRAM_ATTR DlRx::pin_isr(DlRx *arg)
{
const bool new_level = arg->pin_.digital_read();
if (new_level == arg->last_level_)
return;
arg->last_level_ = new_level;
const uint32_t now = micros();
const uint32_t tick_period = now - arg->last_tick_;
bool sync_lost = false;
// used for transition
RxState new_state = arg->state_;
// run state machine
switch (arg->state_)
{
case RxState::Initialize:
// only initialize on rising edges
if (new_level)
{
// first samples are maybe invalid, since we do not now where we started
if (arg->init_samples_ > INIT_SAMPLES_MIN)
{
arg->tick_period_ = std::min(arg->tick_period_, tick_period);
}
arg->init_samples_++;
// if we collect enough samples, calculate range for a valid tick period
if (arg->init_samples_ == INIT_SAMPLES_MAX)
{
arg->calculate_valid_tick_range();
new_state = RxState::Synchronize;
}
arg->last_tick_ = now;
}
break;
case RxState::Synchronize:
// only synchronize on rising edges
if (new_level)
{
if (arg->valid_tick(tick_period))
{
arg->clock_sync_samples_++;
}
else
{
arg->clock_sync_samples_ = 0;
}
// did we receive 16 rising edges (synchronization bits)
if (arg->clock_sync_samples_ == CLOCK_SYNC_SAMPLES)
{
new_state = RxState::Data;
}
arg->last_tick_ = now;
}
break;
case RxState::Data:
if (arg->valid_tick(tick_period))
{
if (arg->set_next_bit(new_level))
{
if (arg->byte_complete())
{
if ((arg->data_byte_position_ == 0) &&
(arg->device_type_ == DeviceType::Unknown))
{
const DeviceType device_type = static_cast<DeviceType>(arg->data_current_byte_);
sync_lost = !arg->set_device_type(device_type);
}
if (arg->update_next_byte())
{
// frame complete
new_state = RxState::Synchronize;
if(arg->frame_handler_) {
arg->frame_handler_(arg->data_buffer_, arg->data_byte_position_);
}
}
}
}
else
{
sync_lost = true;
}
arg->last_tick_ = now;
}
else if (arg->receive_timeout(tick_period))
{
sync_lost = true;
}
if (sync_lost)
{
arg->clock_sync_lost++;
new_state = RxState::Synchronize;
}
break;
}
arg->set_state(new_state);
}
} // namespace dlbus
} // namespace esphome
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <functional>
namespace esphome
{
namespace dlbus
{
static constexpr uint8_t BUFFER_SIZE = 128u;
static constexpr float VALID_TICK_OFFSET = 0.05f;
static constexpr uint8_t DATA_BIT_POSITION_START = 0; // start bit
static constexpr uint8_t DATA_BIT_POSITION_DATA = 1; // start bit
static constexpr uint8_t DATA_BIT_POSITION_STOP = 9; // stop bit
enum class DeviceType : uint8_t
{
Unknown = 0x00,
UVR42 = 0x10u,
UVR64 = 0x20u,
UVR31 = 0x30u,
TFM66 = 0x40u,
EEG30 = 0x50u,
HZR65 = 0x60u,
ESR21 = 0x70u,
UVR1611 = 0x80u,
UVR613 = 0x90u,
};
enum class RxState : uint8_t
{
Initialize = 0u, // reconstruct clock from data
Synchronize = 1u, // synchronize sample time (valid bit time)
Data = 2u // read data
};
using FrameReceived = std::function<void(uint8_t *data, uint8_t size)>;
class DlRx
{
public:
void setup(InternalGPIOPin *pin);
DeviceType get_device_type() { return this->device_type_; }
RxState get_state() { return this->state_; }
uint32_t get_error() { return this->clock_sync_lost; }
enum class UvrSensorType : uint8_t
{
Unused = 0,
Digital = 1,
Temperature = 2,
Flowmeter = 3,
Radiation = 6,
RoomTemperature = 7,
};
struct UvrSensor
{
int16_t raw;
float get_value()
{
int16_t value = get_sign() ? (raw | 0x7000) : (raw & 0x0FFF);
switch (get_val_type())
{
case UvrSensorType::Temperature:
case UvrSensorType::RoomTemperature:
return value * .1f;
case UvrSensorType::Flowmeter:
return value * .4f;
case UvrSensorType::Radiation:
return value * 1.f;
default:
return 0;
}
}
bool get_binary_value()
{
return (get_val_type() == UvrSensorType::Digital) && get_sign();
}
bool get_sign()
{
return (raw & 0x8000) == 0x8000;
}
UvrSensorType get_val_type()
{
return static_cast<UvrSensorType>((raw >> 12) & 0x7);
}
};
static_assert(sizeof(UvrSensor) == 2u);
struct UvrPower
{
uint8_t power[4];
uint16_t energy_kwh;
uint16_t energy_mwh;
float get_energy()
{
return ((energy_mwh * 1000.f) + (energy_kwh * .1f));
}
float get_power()
{
float value = NAN;
int32_t high = 0x10000 * power[3] + 0x100 * power[2] + power[1];
float low = (power[0] * 100.f) / 256.f;
if (!(power[3] & 0x80))
{ // sign positive
value = static_cast<float>(100.f * high + low);
}
else
{ // sign negative
value = static_cast<float>(100.f * (high - 0x10000) - low);
}
return value;
}
};
static_assert(sizeof(UvrPower) == 8u);
union Frame
{
uint8_t buffer[BUFFER_SIZE];
struct __attribute__((__packed__))
{
DeviceType device_type;
uint8_t device_type_inverted;
uint8_t reserved;
uint8_t time_minute;
uint8_t time_hour;
uint8_t time_day;
uint8_t time_month;
uint8_t time_year;
UvrSensor sensor[16];
uint16_t actor_states;
uint8_t rpm_output[4];
uint8_t power_active;
UvrPower heatmeter[2];
uint8_t cheksum;
} UVR1611;
};
static_assert(sizeof(Frame::UVR1611) == 64u);
void set_frame_handler(FrameReceived frame_received) { this->frame_handler_ = frame_received; }
protected:
ISRInternalGPIOPin pin_;
FrameReceived frame_handler_;
RxState state_{RxState::Initialize};
bool last_level_{false};
uint32_t last_tick_{0};
// reconstructed tick period (usaually 2048us or 20ms)
uint32_t tick_period_{UINT32_MAX};
uint32_t min_tick_period_{UINT32_MAX};
uint32_t max_tick_period_{UINT32_MAX};
uint16_t init_samples_{0};
// clock synchronization times
uint8_t clock_sync_samples_{0};
uint32_t clock_sync_lost{0};
// data sampling
uint8_t data_bit_position_{0};
uint8_t data_byte_position_{0};
uint8_t data_current_byte_{0};
uint8_t expected_data_size_{0};
uint8_t data_buffer_[BUFFER_SIZE]{};
DeviceType device_type_{DeviceType::Unknown};
static void pin_isr(DlRx *arg);
void set_state(RxState state)
{
if (this->state_ == state)
return;
switch (state)
{
case RxState::Initialize:
this->tick_period_ = UINT32_MAX;
this->min_tick_period_ = UINT32_MAX;
this->max_tick_period_ = UINT32_MAX;
this->init_samples_ = 0;
this->device_type_ = DeviceType::Unknown;
break;
case RxState::Synchronize:
this->clock_sync_samples_ = 0;
break;
case RxState::Data:
this->data_bit_position_ = 0;
this->data_byte_position_ = 0;
this->data_current_byte_ = 0;
break;
}
state_ = state;
}
bool set_device_type(DeviceType device_type)
{
if (this->device_type_ == device_type)
return true;
switch (device_type)
{
case DeviceType::UVR1611:
this->device_type_ = device_type;
this->expected_data_size_ = 64u;
return true;
default:
return false;
}
return false;
}
bool valid_tick(const uint32_t tick_period) const
{
if (this->state_ == RxState::Initialize)
return false;
if (tick_period < this->min_tick_period_)
return false;
if (tick_period > this->max_tick_period_)
return false;
return true;
}
void calculate_valid_tick_range()
{
this->min_tick_period_ = this->tick_period_ - (this->tick_period_ * VALID_TICK_OFFSET);
this->max_tick_period_ = this->tick_period_ + (this->tick_period_ * VALID_TICK_OFFSET);
}
bool receive_timeout(const uint32_t tick_period) const
{
return tick_period > this->max_tick_period_;
}
bool set_next_bit(bool level)
{
bool result = true;
// start bit
if (this->data_bit_position_ == DATA_BIT_POSITION_START)
{
result = (level == false);
}
else if (this->data_bit_position_ == DATA_BIT_POSITION_STOP)
{
result = (level == true);
}
else
{
const uint8_t position = this->data_bit_position_ - DATA_BIT_POSITION_DATA;
const uint8_t bit = level ? 1 : 0;
this->data_current_byte_ |= level << position;
}
this->data_bit_position_++;
return result;
}
bool byte_complete() const
{
return (this->data_bit_position_ > DATA_BIT_POSITION_STOP);
}
bool update_next_byte()
{
this->data_buffer_[this->data_byte_position_] = this->data_current_byte_;
this->data_current_byte_ = 0;
this->data_bit_position_ = 0;
this->data_byte_position_++;
return this->data_byte_position_ == 64;
}
};
} // namespace dlbus
} // namespace esphome
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS, STATE_CLASS_MEASUREMENT
from .. import dlbus_ns, DlBusComponent, CONF_DLBUS_ID, CONF_INPUT_ID, INPUT_IDS, CONF_POWER_ID, POWER_IDS, CONF_RPM_ID, RPM_IDS, CONF_ENERGY_ID, ENERGY_IDS
DlBusSensor = dlbus_ns.class_("DlBusSensor", cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(
DlBusSensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_DLBUS_ID): cv.use_id(DlBusComponent),
cv.Optional(CONF_INPUT_ID) : cv.enum(INPUT_IDS),
cv.Optional(CONF_POWER_ID) : cv.enum(POWER_IDS),
cv.Optional(CONF_RPM_ID) : cv.enum(RPM_IDS),
cv.Optional(CONF_ENERGY_ID) : cv.enum(ENERGY_IDS)
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
dlbus = await cg.get_variable(config[CONF_DLBUS_ID])
cg.add(var.set_dlbus(dlbus))
if CONF_INPUT_ID in config:
cg.add(var.set_input_id(config[CONF_INPUT_ID]))
elif CONF_POWER_ID in config:
cg.add(var.set_power_id(config[CONF_POWER_ID]))
elif CONF_RPM_ID in config:
cg.add(var.set_rpm_id(config[CONF_RPM_ID]))
elif CONF_ENERGY_ID in config:
cg.add(var.set_energy_id(config[CONF_ENERGY_ID]))
#include "dlbus_sensor.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome
{
namespace dlbus
{
static const char *const TAG = "DlBus.sensor";
// different bit length per device type
void DlBusSensor::setup()
{
ESP_LOGCONFIG(TAG, "Setting up DlBusSensor...");
}
void DlBusSensor::dump_config()
{
ESP_LOGCONFIG(TAG, "DlBus Sensor:");
if (this->input_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " Input: S%d", this->input_id_ + 1);
if (this->rpm_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " RPM: R%d", this->rpm_id_ + 1);
if (this->energy_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " Energy: E%d", this->energy_id_ + 1);
if (this->power_id_ != UINT8_MAX)
ESP_LOGCONFIG(TAG, " Power: P%d", this->power_id_ + 1);
LOG_SENSOR(" ", "DlBusSensor", this);
LOG_UPDATE_INTERVAL(this);
}
void DlBusSensor::update()
{
float value = NAN;
if (this->input_id_ != UINT8_MAX)
{
if (this->dlBus_->get_sensor(this->input_id_, this->sensor))
value = this->sensor.get_value();
}
if (this->rpm_id_ != UINT8_MAX)
{
if (!this->dlBus_->get_rpm(this->rpm_id_, value))
value = NAN;
}
if (this->energy_id_ != UINT8_MAX)
{
if (this->dlBus_->get_heatmeter(this->energy_id_, this->power))
value = this->power.get_energy();
}
if (this->power_id_ != UINT8_MAX)
{
if (this->dlBus_->get_heatmeter(this->power_id_, this->power))
value = this->power.get_power();
}
if (!std::isnan(value))
{
publish_state(value);
}
}
float DlBusSensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace duty_cycle
} // namespace esphome
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/dlbus/dlbus.h"
namespace esphome
{
namespace dlbus
{
class DlBusSensor : public sensor::Sensor, public PollingComponent
{
public:
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override;
void set_dlbus(DlBus *dlBus) { this->dlBus_ = dlBus; }
void set_input_id(uint8_t input_id) { this->input_id_ = input_id; }
void set_rpm_id(uint8_t rpm_id) { this->rpm_id_ = rpm_id; }
void set_power_id(uint8_t power_id) { this->power_id_ = power_id; }
void set_energy_id(uint8_t energy_id) { this->energy_id_ = energy_id; }
protected:
DlBus *dlBus_{nullptr};
uint8_t input_id_{UINT8_MAX};
uint8_t rpm_id_{UINT8_MAX};
uint8_t power_id_{UINT8_MAX};
uint8_t energy_id_{UINT8_MAX};
DlRx::UvrSensor sensor;
DlRx::UvrPower power;
};
} // namespace duty_cycle
} // namespace esphome
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment