Add boolean sensor

This commit is contained in:
Guido Schreuder 2024-02-21 07:25:43 +01:00
parent 6fe6776d38
commit dfb0defb60
8 changed files with 136 additions and 195 deletions

View file

@ -6,12 +6,14 @@ from esphome.const import (
CONF_ID,
CONF_TX_PIN,
CONF_RX_PIN,
CONF_ADDRESS,
CONF_COMMAND,
CONF_PAYLOAD,
CONF_POSITION,
)
# TODO: make ebus primary address optional for only listening on the bus
# TODO: send identification response ewhen requested
# TODO: add binary_sensor
# TODO: send identification response when requested
# TODO: add debug mode that logs all messages on the bus
# TODO: investigate using UART component, but that does not seem to expose the UART NUM
@ -29,6 +31,21 @@ CONF_POLL_INTERVAL = "poll_interval"
CONF_UART = "uart"
CONF_NUM = "num"
CONF_TELEGRAM = "telegram"
CONF_SEND_POLL = "send_poll"
CONF_DECODE = "decode"
SYN = 0xAA
ESC = 0xA9
def validate_ebus_address(address):
if address == SYN:
raise vol.Invalid("SYN symbol (0xAA) is not a valid address")
if address == ESC:
raise vol.Invalid("ESC symbol (0xA9) is not a valid address")
return cv.hex_uint8_t(address)
def is_primary_nibble(value):
return (value & 0x0F) in {0x00, 0x01, 0x03, 0x07, 0x0F}
@ -44,6 +61,29 @@ def validate_primary_address(value):
EbusComponent = ebus_ns.class_("EbusComponent", cg.Component)
def create_decode_schema(options):
return cv.Schema(
{
cv.Optional(CONF_POSITION, default=0): cv.int_range(0, 15),
}
).extend(options)
def create_telegram_schema(decode_options):
return {
cv.GenerateID(CONF_EBUS_ID): cv.use_id(EbusComponent),
cv.Required(CONF_TELEGRAM): cv.Schema(
{
cv.Optional(CONF_SEND_POLL, default=False): cv.boolean,
cv.Optional(CONF_ADDRESS): validate_ebus_address,
cv.Required(CONF_COMMAND): cv.hex_uint16_t,
cv.Required(CONF_PAYLOAD): cv.Schema([cv.hex_uint8_t]),
cv.Optional(CONF_DECODE): create_decode_schema(decode_options),
}
),
}
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@ -80,3 +120,16 @@ async def to_code(config):
cg.add(var.set_history_queue_size(config[CONF_HISTORY_QUEUE_SIZE]))
cg.add(var.set_command_queue_size(config[CONF_COMMAND_QUEUE_SIZE]))
cg.add(var.set_update_interval(config[CONF_POLL_INTERVAL].total_milliseconds))
def sensor_base_config(sensor_base, config):
cg.add(sensor_base.set_send_poll(config[CONF_TELEGRAM][CONF_SEND_POLL]))
if CONF_ADDRESS in config[CONF_TELEGRAM]:
cg.add(sensor_base.set_address(config[CONF_TELEGRAM][CONF_ADDRESS]))
cg.add(sensor_base.set_command(config[CONF_TELEGRAM][CONF_COMMAND]))
cg.add(sensor_base.set_payload(config[CONF_TELEGRAM][CONF_PAYLOAD]))
cg.add(
sensor_base.set_response_read_position(
config[CONF_TELEGRAM][CONF_DECODE][CONF_POSITION]
)
)

View file

@ -26,8 +26,13 @@ class EbusReceiver {
class EbusSender {
public:
EbusSender() {}
virtual void set_primary_address(uint8_t) = 0;
void set_primary_address(uint8_t primary_address) { this->primary_address_ = primary_address; }
void set_address(uint8_t address) { this->address_ = Elf::to_secondary(address); }
virtual optional<SendCommand> prepare_command() = 0;
protected:
uint8_t primary_address_;
uint8_t address_ = SYN;
};
class EbusComponent : public PollingComponent {
@ -52,7 +57,7 @@ class EbusComponent : public PollingComponent {
void update() override;
protected:
uint8_t primary_address_ = SYN;
uint8_t primary_address_;
uint8_t max_tries_;
uint8_t max_lock_counter_;
uint8_t history_queue_size_;

View file

@ -1,100 +1,47 @@
import voluptuous as vol
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_SENSORS,
CONF_SOURCE,
CONF_COMMAND,
CONF_PAYLOAD,
CONF_POSITION,
CONF_BYTES,
)
from .. import EbusComponent, CONF_EBUS_ID, ebus_ns
from .. import (
CONF_EBUS_ID,
CONF_TELEGRAM,
CONF_DECODE,
ebus_ns,
create_telegram_schema,
sensor_base_config,
)
AUTO_LOAD = ["ebus"]
EbusSensor = ebus_ns.class_("EbusSensor", sensor.Sensor, cg.Component)
CONF_TELEGRAM = "telegram"
CONF_SEND_POLL = "send_poll"
CONF_ADDRESS = "address"
CONF_DECODE = "decode"
CONF_DIVIDER = "divider"
SYN = 0xAA
ESC = 0xA9
def validate_address(address):
if address == SYN:
raise vol.Invalid("SYN symbol (0xAA) is not a valid address")
if address == ESC:
raise vol.Invalid("ESC symbol (0xA9) is not a valid address")
return cv.hex_uint8_t(address)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(EbusSensor),
cv.GenerateID(CONF_EBUS_ID): cv.use_id(EbusComponent),
cv.Required(CONF_SENSORS): cv.ensure_list(
sensor.sensor_schema().extend(
{
cv.GenerateID(): cv.declare_id(EbusSensor),
cv.Required(CONF_TELEGRAM): cv.Schema(
{
cv.Optional(CONF_SEND_POLL, default=False): cv.boolean,
cv.Optional(CONF_ADDRESS): validate_address,
cv.Required(CONF_COMMAND): cv.hex_uint16_t,
cv.Required(CONF_PAYLOAD): cv.Schema([cv.hex_uint8_t]),
cv.Optional(CONF_DECODE): cv.Schema(
{
cv.Optional(CONF_POSITION, default=0): cv.int_range(
0, 15
),
cv.Required(CONF_BYTES): cv.int_range(1, 4),
cv.Required(CONF_DIVIDER): cv.float_,
}
),
}
),
}
)
),
}
)
CONFIG_SCHEMA = (
sensor.sensor_schema(EbusSensor).extend(
create_telegram_schema(
{
cv.Required(CONF_BYTES): cv.int_range(1, 4),
cv.Required(CONF_DIVIDER): cv.float_,
}
)
)
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
ebus = await cg.get_variable(config[CONF_EBUS_ID])
sens = await sensor.new_sensor(config)
for i, conf in enumerate(config[CONF_SENSORS]):
print(f"Sensor: {i}, {conf}")
sens = await sensor.new_sensor(conf)
if CONF_SOURCE in conf[CONF_TELEGRAM]:
cg.add(sens.set_source(conf[CONF_TELEGRAM][CONF_SOURCE]))
sensor_base_config(sens, config)
if CONF_ADDRESS in conf[CONF_TELEGRAM]:
cg.add(sens.set_address(conf[CONF_TELEGRAM][CONF_ADDRESS]))
cg.add(sens.set_command(conf[CONF_TELEGRAM][CONF_COMMAND]))
cg.add(sens.set_payload(conf[CONF_TELEGRAM][CONF_PAYLOAD]))
cg.add(
sens.set_response_read_position(
conf[CONF_TELEGRAM][CONF_DECODE][CONF_POSITION]
)
)
cg.add(
sens.set_response_read_bytes(conf[CONF_TELEGRAM][CONF_DECODE][CONF_BYTES])
)
cg.add(
sens.set_response_read_divider(
conf[CONF_TELEGRAM][CONF_DECODE][CONF_DIVIDER]
)
)
cg.add(ebus.add_receiver(sens))
cg.add(ebus.add_sender(sens))
cg.add(sens.set_response_read_bytes(config[CONF_TELEGRAM][CONF_DECODE][CONF_BYTES]))
cg.add(
sens.set_response_read_divider(config[CONF_TELEGRAM][CONF_DECODE][CONF_DIVIDER])
)
cg.add(ebus.add_receiver(sens))
cg.add(ebus.add_sender(sens))

View file

@ -1,43 +1,9 @@
#include "ebus_sensor.h"
// TODO: remove
#define GET_BYTE(CMD, I) ((uint8_t) (((CMD) >> 8 * (I)) & 0XFF))
namespace esphome {
namespace ebus {
void EbusSensor::dump_config() {
ESP_LOGCONFIG(TAG, "EbusSensor");
ESP_LOGCONFIG(TAG, " message:");
ESP_LOGCONFIG(TAG, " send_poll: %s", this->send_poll_ ? "true" : "false");
if (this->address_ == SYN) {
ESP_LOGCONFIG(TAG, " address: N/A");
} else {
ESP_LOGCONFIG(TAG, " address: 0x%02x", this->address_);
}
ESP_LOGCONFIG(TAG, " command: 0x%04x", this->command_);
};
void EbusSensor::set_primary_address(uint8_t primary_address) { this->primary_address_ = primary_address; }
void EbusSensor::set_send_poll(bool send_poll) { this->send_poll_ = send_poll; }
void EbusSensor::set_address(uint8_t address) { this->address_ = Elf::to_secondary(address); }
void EbusSensor::set_command(uint16_t command) { this->command_ = command; }
void EbusSensor::set_payload(const std::vector<uint8_t> &payload) { this->payload_ = payload; }
void EbusSensor::set_response_read_position(uint8_t response_position) { this->response_position_ = response_position; }
void EbusSensor::set_response_read_bytes(uint8_t response_bytes) { this->response_bytes_ = response_bytes; }
void EbusSensor::set_response_read_divider(float response_divider) { this->response_divider_ = response_divider; }
optional<SendCommand> EbusSensor::prepare_command() {
optional<SendCommand> command;
if (this->send_poll_) {
command = SendCommand( //
this->primary_address_, this->address_, GET_BYTE(this->command_, 1), GET_BYTE(this->command_, 0),
this->payload_.size(), &this->payload_[0]);
}
return command;
}
void EbusSensor::process_received(Telegram telegram) {
if (!is_mine(telegram)) {
return;
@ -45,32 +11,9 @@ void EbusSensor::process_received(Telegram telegram) {
this->publish_state(to_float(telegram, this->response_position_, this->response_bytes_, this->response_divider_));
}
uint32_t EbusSensor::get_response_bytes(Telegram &telegram, uint8_t start, uint8_t length) {
uint32_t result = 0;
for (uint8_t i = 0; i < 4 && i < length; i++) {
result = result | (telegram.get_response_byte(start + i) << (i * 8));
}
return result;
}
float EbusSensor::to_float(Telegram &telegram, uint8_t start, uint8_t length, float divider) {
return get_response_bytes(telegram, start, length) / divider;
}
bool EbusSensor::is_mine(Telegram &telegram) {
if (this->address_ != SYN && this->address_ != telegram.get_zz()) {
return false;
}
if (telegram.get_command() != this->command_) {
return false;
}
for (int i = 0; i < this->payload_.size(); i++) {
if (this->payload_[i] != telegram.get_request_byte(i)) {
return false;
}
}
return true;
}
} // namespace ebus
} // namespace esphome

View file

@ -1,42 +1,23 @@
#pragma once
#include "../ebus_component.h"
#include "../ebus_sensor_base.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace ebus {
class EbusSensor : public EbusReceiver, public EbusSender, public sensor::Sensor, public Component {
class EbusSensor : public EbusSensorBase, public sensor::Sensor {
public:
EbusSensor() {}
void dump_config() override;
void set_primary_address(uint8_t /*primary_address*/) override;
void set_send_poll(bool /*send_poll*/);
void set_address(uint8_t /*address*/);
void set_command(uint16_t /*command*/);
void set_payload(const std::vector<uint8_t> & /*payload*/);
void set_response_read_position(uint8_t /*response_position*/);
void set_response_read_bytes(uint8_t /*response_bytes*/);
void set_response_read_divider(float /*response_divider*/);
void set_response_read_bytes(uint8_t response_bytes) { this->response_bytes_ = response_bytes; }
void set_response_read_divider(float response_divider) { this->response_divider_ = response_divider; }
void process_received(Telegram /*telegram*/) override;
optional<SendCommand> prepare_command() override;
// TODO: refactor these
uint32_t get_response_bytes(Telegram &telegram, uint8_t start, uint8_t length);
float to_float(Telegram &telegram, uint8_t start, uint8_t length, float divider);
bool is_mine(Telegram &telegram);
protected:
uint8_t primary_address_;
bool send_poll_;
uint8_t address_ = SYN;
uint16_t command_;
std::vector<uint8_t> payload_{};
uint8_t response_position_;
uint8_t response_bytes_;
float response_divider_;
};

View file

@ -104,19 +104,18 @@ bool Telegram::is_request_valid() {
SendCommand::SendCommand() { this->state_ = TelegramState::endCompleted; }
SendCommand::SendCommand(uint8_t qq, uint8_t zz, uint8_t pb, uint8_t sb, uint8_t nn, uint8_t *data) {
SendCommand::SendCommand(uint8_t qq, uint8_t zz, uint16_t command, uint8_t nn, uint8_t *data) {
this->state_ = TelegramState::waitForSend;
this->push_req_data(qq);
this->push_req_data(zz);
this->push_req_data(pb);
this->push_req_data(sb);
this->push_req_data(command >> 8);
this->push_req_data(command & 0xFF);
this->push_req_data(nn);
for (int i = 0; i < nn; i++) {
this->push_req_data(data[i]);
}
this->push_req_data(this->request_rolling_crc_);
}
bool SendCommand::can_retry(int8_t max_tries) { return this->tries_count_++ < max_tries; }
uint8_t SendCommand::get_crc() { return this->request_rolling_crc_; }

View file

@ -132,7 +132,7 @@ class Telegram : public TelegramBase {
class SendCommand : public TelegramBase {
public:
SendCommand();
SendCommand(uint8_t qq, uint8_t zz, uint8_t pb, uint8_t sb, uint8_t nn, uint8_t *data);
SendCommand(uint8_t qq, uint8_t zz, uint16_t command, uint8_t nn, uint8_t *data);
bool can_retry(int8_t max_tries);
uint8_t get_crc();

View file

@ -2,7 +2,7 @@ ebus:
primary_address: 0x00
history_queue_size: 32
command_queue_size: 16
poll_interval: "15s"
poll_interval: "60s"
uart:
num: 1
tx_pin: 33
@ -10,28 +10,41 @@ ebus:
sensor:
- platform: ebus
sensors:
- name: "Water Pressure"
id: zebus_water_pressure
telegram:
destination: 0x03
command: 0xb509
payload: [0x0d, 0x02, 0x00]
decode:
position: 0
bytes: 2
divider: 1000.0
device_class: "pressure"
unit_of_measurement: bar
state_class: "measurement"
accuracy_decimals: 3
internal: false
filters:
- clamp:
min_value: 0
max_value: 4
ignore_out_of_range: true
- median:
window_size: 5
send_every: 3
send_first_at: 3
name: "Water Pressure"
id: zebus_water_pressure
telegram:
send_poll: true
address: 0x03
command: 0xb509
payload: [0x0d, 0x02, 0x00]
decode:
position: 0
bytes: 2
divider: 1000.0
device_class: "pressure"
unit_of_measurement: bar
state_class: "measurement"
accuracy_decimals: 3
internal: false
filters:
- clamp:
min_value: 0
max_value: 4
ignore_out_of_range: true
- median:
window_size: 5
send_every: 3
send_first_at: 3
binary_sensor:
- platform: ebus
name: "Thermostat in Quick Veto"
id: zebus_in_quick_veto
telegram:
send_poll: true
address: 0x10
command: 0xb509
payload: [0x0d, 0x16, 0x00]
decode:
position: 0
mask: 0x01