Add support for Viessmann heating components via Optolink adapter

This commit is contained in:
jota29 2023-03-11 07:03:50 +00:00 committed by j0ta29
parent d642aeba0f
commit 112439b485
23 changed files with 1082 additions and 0 deletions

View file

@ -0,0 +1,92 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import text_sensor as ts
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_LOGGER,
CONF_PROTOCOL,
CONF_RX_PIN,
CONF_STATE,
CONF_TX_PIN,
)
from esphome.core import CORE
DEPENDENCIES = []
AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor", "number", "select", "switch"]
MULTI_CONF = False
CONF_DEVICE_INFO = "device_info"
optolink_ns = cg.esphome_ns.namespace("optolink")
OptolinkComponent = optolink_ns.class_("Optolink", cg.Component)
StateSensor = optolink_ns.class_(
"OptolinkStateSensor", ts.TextSensor, cg.PollingComponent
)
STATE_SENSOR_ID = "state_sensor_id"
DeviceInfoSensor = optolink_ns.class_(
"OptolinkDeviceInfoSensor", ts.TextSensor, cg.PollingComponent
)
DEVICE_INFO_SENSOR_ID = "device_info_sensor_id"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(OptolinkComponent),
cv.GenerateID(STATE_SENSOR_ID): cv.declare_id(StateSensor),
cv.GenerateID(DEVICE_INFO_SENSOR_ID): cv.declare_id(DeviceInfoSensor),
cv.Required(CONF_PROTOCOL): cv.one_of("P300", "KW"),
cv.Optional(CONF_LOGGER, default=False): cv.boolean,
cv.Optional(CONF_STATE): cv.string,
cv.Optional(CONF_DEVICE_INFO): cv.string,
}
).extend(cv.COMPONENT_SCHEMA)
if CORE.is_esp32:
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_schema,
}
)
)
async def to_code(config):
cg.add_library("VitoWiFi", "1.0.2")
cg.add_define(
"VITOWIFI_PROTOCOL", cg.RawExpression(f"Optolink{config[CONF_PROTOCOL]}")
)
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_logger_enabled(config[CONF_LOGGER]))
if CONF_STATE in config:
debugSensor = cg.new_Pvariable(config[STATE_SENSOR_ID], config[CONF_STATE], var)
await ts.register_text_sensor(
debugSensor,
{
"id": config[STATE_SENSOR_ID],
"name": config[CONF_STATE],
"disabled_by_default": "false",
},
)
await cg.register_component(debugSensor, config)
if CONF_DEVICE_INFO in config:
debugSensor = cg.new_Pvariable(
config[DEVICE_INFO_SENSOR_ID], config[CONF_DEVICE_INFO], var
)
await ts.register_text_sensor(
debugSensor,
{
"id": config[DEVICE_INFO_SENSOR_ID],
"name": config[CONF_DEVICE_INFO],
"disabled_by_default": "false",
},
)
await cg.register_component(debugSensor, config)
if CORE.is_esp32:
cg.add(var.set_rx_pin(config[CONF_RX_PIN]["number"]))
cg.add(var.set_tx_pin(config[CONF_TX_PIN]["number"]))
await cg.register_component(var, config)

View file

@ -0,0 +1,31 @@
from esphome import core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_ID, CONF_ADDRESS, CONF_UPDATE_INTERVAL
from . import optolink_ns, OptolinkComponent
OptolinkBinarySensor = optolink_ns.class_(
"OptolinkBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
)
CONF_OPTOLINK_ID = "optolink_id"
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(OptolinkBinarySensor).extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.Required(CONF_ADDRESS): cv.hex_uint32_t,
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)),
),
}
)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await binary_sensor.register_binary_sensor(var, config)
cg.add(var.set_address(config[CONF_ADDRESS]))

View file

@ -0,0 +1,59 @@
from esphome import core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
from esphome.components.optolink.sensor import SENSOR_BASE_SCHEMA
from esphome.const import (
CONF_ADDRESS,
CONF_BYTES,
CONF_DIV_RATIO,
CONF_ID,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_STEP,
CONF_UPDATE_INTERVAL,
)
from . import OptolinkComponent, optolink_ns
OptolinkNumber = optolink_ns.class_(
"OptolinkNumber", number.Number, cg.PollingComponent
)
CONF_OPTOLINK_ID = "optolink_id"
CONFIG_SCHEMA = (
number.NUMBER_SCHEMA.extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.GenerateID(): cv.declare_id(OptolinkNumber),
cv.Required(CONF_MAX_VALUE): cv.float_,
cv.Required(CONF_MIN_VALUE): cv.float_range(min=0.0),
cv.Required(CONF_STEP): cv.float_,
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
),
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(SENSOR_BASE_SCHEMA)
)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_bytes(config[CONF_BYTES]))
cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))

View file

@ -0,0 +1,72 @@
#include "esphome/core/defines.h"
#include "esphome/components/optolink/optolink.h"
#include <VitoWiFi.h>
VitoWiFiClass<VITOWIFI_PROTOCOL> VitoWiFi;
namespace esphome {
namespace optolink {
void Optolink::_comm() {
ESP_LOGD("Optolink", "enter _comm");
VitoWiFi.readAll();
ESP_LOGD("Optolink", "exit _comm");
}
void Optolink::setup() {
ESP_LOGI("Optolink", "setup");
if (logger_enabled_) {
VitoWiFi.setLogger(this);
VitoWiFi.enableLogger();
}
#if defined(USE_ESP32)
VitoWiFi.setup(&Serial, rx_pin_, tx_pin_);
#elif defined(USE_ESP8266)
VitoWiFi.setup(&Serial);
#endif
// set_interval("Optolink_comm", 10000, std::bind(&Optolink::_comm, this));
}
void Optolink::loop() { VitoWiFi.loop(); }
void Optolink::set_error(const std::string &format, ...) {
va_list args;
va_start(args, format);
char buffer[128];
size_t n = std::vsnprintf(buffer, sizeof(buffer), format.c_str(), args);
va_end(args);
error_ = buffer;
}
void Optolink::read_value(IDatapoint *datapoint) {
if (datapoint != nullptr) {
ESP_LOGI("Optolink", " read value of datapoint %s", datapoint->getName());
VitoWiFi.readDatapoint(*datapoint);
}
}
void Optolink::write_value(IDatapoint *datapoint, DPValue dpValue) {
if (datapoint != nullptr) {
char buffer[64];
dpValue.getString(buffer, sizeof(buffer));
ESP_LOGI("Optolink", " write value %s of datapoint %s", buffer, datapoint->getName());
VitoWiFi.writeDatapoint(*datapoint, dpValue);
}
}
size_t Optolink::write(uint8_t ch) {
if (ch == '\n') {
ESP_LOGD("VitoWifi", "%s", log_buffer_.c_str());
log_buffer_.clear();
} else {
log_buffer_.push_back(ch);
}
return 1;
}
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include <VitoWiFi.h>
using namespace esphome;
using namespace sensor;
using namespace binary_sensor;
using namespace text_sensor;
namespace esphome {
namespace optolink {
// '00' ='WW' '01' ='RED' '02' ='NORM' '03' ='H+WW' '04' ='H+WW FS' '05' ='ABSCHALT'
//=====================================================================================================================
class Optolink : public esphome::Component, public Print {
protected:
std::string error_ = "OK";
std::string log_buffer_;
bool logger_enabled_ = false;
int rx_pin_;
int tx_pin_;
void _comm();
public:
void setup() override;
void loop() override;
size_t write(uint8_t ch) override;
void set_logger_enabled(bool logger_enabled) { logger_enabled_ = logger_enabled; }
void set_rx_pin(int rx_pin) { rx_pin_ = rx_pin; }
void set_tx_pin(int tx_pin) { tx_pin_ = tx_pin; }
void write_value(IDatapoint *datapoint, DPValue dpValue);
void read_value(IDatapoint *datapoint);
void set_error(const std::string &format, ...);
std::string get_error() { return error_; }
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkBinarySensor : public OptolinkSensorBase,
public esphome::binary_sensor::BinarySensor,
public esphome::PollingComponent {
public:
OptolinkBinarySensor(Optolink *optolink) : OptolinkSensorBase(optolink) {
bytes_ = 1;
div_ratio_ = 1;
}
protected:
void setup() override { setup_datapoint(); }
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override { publish_state(state); };
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,43 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkDeviceInfoSensor : public esphome::text_sensor::TextSensor, public esphome::PollingComponent {
public:
OptolinkDeviceInfoSensor(std::string name, Optolink *optolink) {
optolink_ = optolink;
set_name(name);
set_update_interval(1800000);
set_entity_category(esphome::ENTITY_CATEGORY_DIAGNOSTIC);
}
protected:
void setup() override {
datapoint_ = new Datapoint<conv4_1_UL>(get_name().c_str(), "optolink", 0x00f8, false);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
uint32_t value = dpValue.getU32();
ESP_LOGD("OptolinkTextSensor", "Datapoint %s - %s: %d", dp.getGroup(), dp.getName(), value);
uint8_t *bytes = (uint8_t *) &value;
uint16_t tmp = esphome::byteswap(*((uint16_t *) bytes));
std::string geraetekennung = esphome::format_hex_pretty(&tmp, 1);
std::string hardware_revision = esphome::format_hex_pretty((uint8_t *) bytes + 2, 1);
std::string software_index = esphome::format_hex_pretty((uint8_t *) bytes + 3, 1);
publish_state("Device ID: " + geraetekennung + "|Hardware Revision: " + hardware_revision +
"|Software Index: " + software_index);
});
}
void update() override { optolink_->read_value(datapoint_); }
private:
Optolink *optolink_;
IDatapoint *datapoint_;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,20 @@
#include "optolink_number.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
void OptolinkNumber::control(float value) {
if (value > traits.get_max_value() || value < traits.get_min_value()) {
optolink_->set_error("datapoint value of number %s not in allowed range", get_sensor_name().c_str());
ESP_LOGE("OptolinkNumber", "datapoint value of number %s not in allowed range", get_sensor_name().c_str());
} else {
ESP_LOGI("OptolinkNumber", "control of number %s to value %f", get_sensor_name().c_str(), value);
update_datapoint(value);
publish_state(value);
}
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,26 @@
#pragma once
#include "esphome/components/number/number.h"
#include "optolink_sensor_base.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkNumber : public OptolinkSensorBase, public esphome::number::Number, public esphome::PollingComponent {
public:
OptolinkNumber(Optolink *optolink) : OptolinkSensorBase(optolink, true) {}
protected:
void setup() override { setup_datapoint(); }
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override { publish_state(state); };
void control(float value) override;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,41 @@
#include "optolink_select.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
void OptolinkSelect::control(const std::string &value) {
for (auto it = mapping_->begin(); it != mapping_->end(); ++it) {
if (it->second == value) {
ESP_LOGI("OptolinkSelect", "control of select %s to value %s", get_sensor_name().c_str(), it->first.c_str());
update_datapoint(std::stof(it->first));
publish_state(it->second);
break;
}
if (it == mapping_->end()) {
optolink_->set_error("unknown value %s of select %s", value.c_str(), get_sensor_name().c_str());
ESP_LOGE("OptolinkSelect", "unknown value %s of select %s", value.c_str(), get_sensor_name().c_str());
}
}
};
void OptolinkSelect::value_changed(float state) {
std::string key;
if (div_ratio_ == 1) {
key = std::to_string((int) state);
} else {
key = std::to_string(state);
}
auto pos = mapping_->find(key);
if (pos == mapping_->end()) {
optolink_->set_error("value %s not found in select %s", key.c_str(), get_sensor_name().c_str());
ESP_LOGE("OptolinkSelect", "value %s not found in select %s", key.c_str(), get_sensor_name().c_str());
} else {
publish_state(pos->second);
}
//-----------------------------------------------publish_state(state);
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,39 @@
#pragma once
#include <map>
#include "esphome/components/select/select.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkSelect : public OptolinkSensorBase, public esphome::select::Select, public esphome::PollingComponent {
public:
OptolinkSelect(Optolink *optolink) : OptolinkSensorBase(optolink, true) {}
void set_map(std::map<std::string, std::string> *mapping) {
mapping_ = mapping;
std::vector<std::string> values;
for (auto it = mapping->begin(); it != mapping->end(); ++it) {
values.push_back(it->second);
}
traits.set_options(values);
};
protected:
void setup() override { setup_datapoint(); }
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override;
void control(const std::string &value) override;
private:
std::map<std::string, std::string> *mapping_ = nullptr;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,25 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkSensor : public OptolinkSensorBase, public esphome::sensor::Sensor, public esphome::PollingComponent {
public:
OptolinkSensor(Optolink *optolink) : OptolinkSensorBase(optolink) {
set_state_class(esphome::sensor::STATE_CLASS_MEASUREMENT);
}
protected:
void setup() { setup_datapoint(); }
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override { publish_state(state); };
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,163 @@
#include "optolink_sensor_base.h"
#include "optolink.h"
namespace esphome {
namespace optolink {
void OptolinkSensorBase::update_datapoint(float value) {
if (!writeable_) {
optolink_->set_error("try to control not writable number %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "try to control not writable number %s", get_sensor_name().c_str());
} else if (datapoint_ != nullptr) {
switch (bytes_) {
case 1:
switch (div_ratio_) {
case 1:
optolink_->write_value(datapoint_, DPValue((uint8_t) value));
break;
case 10:
optolink_->write_value(datapoint_, DPValue((float) value));
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for number %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for number %s",
get_sensor_name().c_str());
break;
}
break;
case 2:
switch (div_ratio_) {
case 1:
optolink_->write_value(datapoint_, DPValue((uint16_t) value));
break;
case 10:
optolink_->write_value(datapoint_, DPValue((float) value));
break;
case 100:
optolink_->write_value(datapoint_, DPValue((float) value));
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for number %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for number %s",
get_sensor_name().c_str());
break;
}
break;
case 4:
switch (div_ratio_) {
case 1:
optolink_->write_value(datapoint_, DPValue((uint32_t) value));
break;
case 3600:
optolink_->write_value(datapoint_, DPValue((float) value));
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for number %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for number %s",
get_sensor_name().c_str());
break;
}
break;
default:
optolink_->set_error("Unknown byte value for number %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte value for number %s", get_sensor_name().c_str());
break;
}
}
}
void OptolinkSensorBase::setup_datapoint() {
switch (bytes_) {
case 1:
switch (div_ratio_) {
case 1:
datapoint_ = new Datapoint<conv1_1_US>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %d", dp.getGroup(), dp.getName(), dpValue.getU8());
value_changed(dpValue.getU8());
});
break;
case 10:
datapoint_ = new Datapoint<conv1_10_F>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %f", dp.getGroup(), dp.getName(), dpValue.getFloat());
value_changed(dpValue.getFloat());
});
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
break;
}
break;
case 2:
switch (div_ratio_) {
case 1:
datapoint_ = new Datapoint<conv2_1_US>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %d", dp.getGroup(), dp.getName(), dpValue.getU16());
value_changed(dpValue.getU16());
});
break;
case 10:
datapoint_ = new Datapoint<conv2_10_F>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %f", dp.getGroup(), dp.getName(), dpValue.getFloat());
value_changed(dpValue.getFloat());
});
break;
case 100:
datapoint_ = new Datapoint<conv2_100_F>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %f", dp.getGroup(), dp.getName(), dpValue.getFloat());
value_changed(dpValue.getFloat());
});
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
break;
}
break;
case 4:
switch (div_ratio_) {
case 1:
datapoint_ = new Datapoint<conv4_1_UL>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %d", dp.getGroup(), dp.getName(), dpValue.getU32());
value_changed(dpValue.getU32());
});
break;
case 3600:
datapoint_ = new Datapoint<conv4_3600_F>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: %f", dp.getGroup(), dp.getName(), dpValue.getFloat());
value_changed(dpValue.getFloat());
});
break;
default:
optolink_->set_error("Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte/div_ratio combination for sensor %s", get_sensor_name().c_str());
break;
}
break;
default:
optolink_->set_error("Unknown byte value for sensor %s", get_sensor_name().c_str());
ESP_LOGE("OptolinkSensorBase", "Unknown byte value for sensor %s", get_sensor_name().c_str());
break;
}
}
void conv2_100_F::encode(uint8_t *out, DPValue in) {
int16_t tmp = floor((in.getFloat() * 100) + 0.5);
out[1] = tmp >> 8;
out[0] = tmp & 0xFF;
}
DPValue conv2_100_F::decode(const uint8_t *in) {
int16_t tmp = in[1] << 8 | in[0];
DPValue out(tmp / 100.0f);
return out;
}
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/log.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class Optolink;
class OptolinkSensorBase {
protected:
Optolink *optolink_;
bool writeable_;
IDatapoint *datapoint_ = nullptr;
uint32_t address_;
int bytes_;
int div_ratio_ = 1;
void setup_datapoint();
void update_datapoint(float value);
public:
OptolinkSensorBase(Optolink *optolink, bool writeable = false) {
optolink_ = optolink;
writeable_ = writeable;
}
void set_address(uint32_t address) { address_ = address; }
void set_bytes(int bytes) { bytes_ = bytes; }
void set_div_ratio(int div_ratio) { div_ratio_ = div_ratio; }
protected:
virtual const std::string &get_sensor_name() = 0;
virtual void value_changed(float state) = 0;
};
class conv2_100_F : public DPType {
public:
void encode(uint8_t *out, DPValue in);
DPValue decode(const uint8_t *in);
const size_t getLength() const { return 2; }
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkStateSensor : public esphome::text_sensor::TextSensor, public esphome::PollingComponent {
public:
OptolinkStateSensor(std::string name, Optolink *optolink) {
optolink_ = optolink;
set_name(name);
set_update_interval(1000);
set_entity_category(esphome::ENTITY_CATEGORY_DIAGNOSTIC);
}
protected:
void setup() override{};
void update() override { publish_state(optolink_->get_error()); }
private:
Optolink *optolink_;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,20 @@
#include "optolink_switch.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
void OptolinkSwitch::write_state(bool value) {
if (value != 0 && value != 1) {
optolink_->set_error("datapoint value of switch %s not 0 or 1", get_sensor_name().c_str());
ESP_LOGE("OptolinkSwitch", "datapoint value of switch %s not 0 or 1", get_sensor_name().c_str());
} else {
ESP_LOGI("OptolinkSwitch", "control of switch %s to value %d", get_sensor_name().c_str(), value);
update_datapoint(value);
publish_state(value);
}
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "optolink_sensor_base.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkSwitch : public OptolinkSensorBase, public esphome::switch_::Switch, public esphome::PollingComponent {
public:
OptolinkSwitch(Optolink *optolink) : OptolinkSensorBase(optolink, true) {
bytes_ = 1;
div_ratio_ = 1;
}
protected:
void setup() override { setup_datapoint(); }
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override { publish_state(state); };
void write_state(bool state) override;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,25 @@
#include "optolink_text_sensor.h"
#include "optolink.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
void OptolinkTextSensor::setup() {
if (!raw_) {
setup_datapoint();
} else {
datapoint_ = new Datapoint<convRaw>(get_sensor_name().c_str(), "optolink", address_, writeable_);
datapoint_->setLength(bytes_);
datapoint_->setCallback([this](const IDatapoint &dp, DPValue dpValue) {
ESP_LOGD("OptolinkSensorBase", "Datapoint %s - %s: <raw>", dp.getGroup(), dp.getName());
uint8_t buffer[bytes_ + 1];
dpValue.getRaw(buffer);
buffer[bytes_] = 0x0;
publish_state((char *) buffer);
});
}
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,31 @@
#pragma once
#include "esphome/components/text_sensor/text_sensor.h"
#include "optolink.h"
#include "optolink_sensor_base.h"
#include <VitoWiFi.h>
namespace esphome {
namespace optolink {
class OptolinkTextSensor : public OptolinkSensorBase,
public esphome::text_sensor::TextSensor,
public esphome::PollingComponent {
public:
OptolinkTextSensor(Optolink *optolink) : OptolinkSensorBase(optolink) {}
void set_raw(bool raw) { raw_ = raw; }
protected:
void setup() override;
void update() override { optolink_->read_value(datapoint_); }
const std::string &get_sensor_name() override { return get_name(); }
void value_changed(float state) override { publish_state(std::to_string(state)); };
private:
bool raw_ = false;
};
} // namespace optolink
} // namespace esphome

View file

@ -0,0 +1,80 @@
from esphome import core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import select
from esphome.components.optolink.sensor import SENSOR_BASE_SCHEMA
from esphome.const import (
CONF_ADDRESS,
CONF_BYTES,
CONF_DIV_RATIO,
CONF_FROM,
CONF_ID,
CONF_TO,
CONF_UPDATE_INTERVAL,
)
from . import OptolinkComponent, optolink_ns
OptolinkSelect = optolink_ns.class_(
"OptolinkSelect", select.Select, cg.PollingComponent
)
def validate_mapping(value):
if not isinstance(value, dict):
value = cv.string(value)
if "->" not in value:
raise cv.Invalid("Mapping must contain '->'")
a, b = value.split("->", 1)
value = {CONF_FROM: a.strip(), CONF_TO: b.strip()}
return cv.Schema(
{cv.Required(CONF_FROM): cv.string, cv.Required(CONF_TO): cv.string}
)(value)
CONF_OPTOLINK_ID = "optolink_id"
CONF_MAP = "map"
MAP_ID = "mappings"
CONFIG_SCHEMA = (
select.SELECT_SCHEMA.extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.GenerateID(): cv.declare_id(OptolinkSelect),
cv.GenerateID(MAP_ID): cv.declare_id(
cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
),
cv.Required(CONF_MAP): cv.ensure_list(validate_mapping),
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
),
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(SENSOR_BASE_SCHEMA)
)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await select.register_select(
var,
config,
options=[],
)
map_type_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
map_var = cg.new_Pvariable(
config[MAP_ID],
map_type_([(item[CONF_FROM], item[CONF_TO]) for item in config[CONF_MAP]]),
)
cg.add(var.set_map(map_var))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_bytes(config[CONF_BYTES]))
cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))

View file

@ -0,0 +1,51 @@
from esphome import core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ID,
CONF_ADDRESS,
CONF_BYTES,
CONF_DIV_RATIO,
CONF_UPDATE_INTERVAL,
)
from . import optolink_ns, OptolinkComponent
OptolinkSensor = optolink_ns.class_(
"OptolinkSensor", sensor.Sensor, cg.PollingComponent
)
CONF_OPTOLINK_ID = "optolink_id"
SENSOR_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.hex_uint32_t,
cv.Required(CONF_BYTES): cv.one_of(1, 2, 4, int=True),
cv.Optional(CONF_DIV_RATIO, default=1): cv.one_of(1, 10, 100, 3600, int=True),
}
)
CONFIG_SCHEMA = (
sensor.sensor_schema(OptolinkSensor)
.extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
),
),
}
)
.extend(SENSOR_BASE_SCHEMA)
)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_bytes(config[CONF_BYTES]))
cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))

View file

@ -0,0 +1,33 @@
from esphome import core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_UPDATE_INTERVAL
from . import OptolinkComponent, optolink_ns
OptolinkSwitch = optolink_ns.class_(
"OptolinkSwitch", switch.Switch, cg.PollingComponent
)
CONF_OPTOLINK_ID = "optolink_id"
CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.GenerateID(): cv.declare_id(OptolinkSwitch),
cv.Required(CONF_ADDRESS): cv.hex_uint32_t,
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)),
),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await switch.register_switch(var, config)
cg.add(var.set_address(config[CONF_ADDRESS]))

View file

@ -0,0 +1,50 @@
from esphome import core
import esphome.codegen as cg
from esphome.components.optolink.sensor import SENSOR_BASE_SCHEMA
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import (
CONF_ADDRESS,
CONF_BYTES,
CONF_DIV_RATIO,
CONF_ID,
CONF_RAW,
CONF_UPDATE_INTERVAL,
)
from . import optolink_ns, OptolinkComponent
OptolinkTextSensor = optolink_ns.class_(
"OptolinkTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
CONF_OPTOLINK_ID = "optolink_id"
CONFIG_SCHEMA = cv.All(
text_sensor.text_sensor_schema(OptolinkTextSensor)
.extend(
{
cv.GenerateID(CONF_OPTOLINK_ID): cv.use_id(OptolinkComponent),
cv.Optional(CONF_UPDATE_INTERVAL, default="10s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
),
),
cv.Optional(CONF_RAW, default=False): cv.boolean,
}
)
.extend(SENSOR_BASE_SCHEMA)
.extend({cv.Required(CONF_BYTES): cv.int_}),
)
async def to_code(config):
component = await cg.get_variable(config[CONF_OPTOLINK_ID])
var = cg.new_Pvariable(config[CONF_ID], component)
await cg.register_component(var, config)
await text_sensor.register_text_sensor(var, config)
cg.add(var.set_raw(config[CONF_RAW]))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_bytes(config[CONF_BYTES]))
cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))