From 2e34b9ad1025dc7e6ad9f62fb1cae76fc13401d5 Mon Sep 17 00:00:00 2001 From: jota29 Date: Sat, 11 Mar 2023 07:03:50 +0000 Subject: [PATCH] Add support for Viessmann heating components via Optolink adapter --- esphome/components/optolink/__init__.py | 92 ++++++++++ esphome/components/optolink/binary_sensor.py | 31 ++++ esphome/components/optolink/number.py | 59 +++++++ esphome/components/optolink/optolink.cpp | 72 ++++++++ esphome/components/optolink/optolink.h | 49 ++++++ .../optolink/optolink_binary_sensor.h | 28 +++ .../optolink/optolink_device_info_sensor.h | 43 +++++ .../components/optolink/optolink_number.cpp | 20 +++ esphome/components/optolink/optolink_number.h | 26 +++ .../components/optolink/optolink_select.cpp | 41 +++++ esphome/components/optolink/optolink_select.h | 39 +++++ esphome/components/optolink/optolink_sensor.h | 25 +++ .../optolink/optolink_sensor_base.cpp | 163 ++++++++++++++++++ .../optolink/optolink_sensor_base.h | 47 +++++ .../optolink/optolink_state_sensor.h | 28 +++ .../components/optolink/optolink_switch.cpp | 20 +++ esphome/components/optolink/optolink_switch.h | 29 ++++ .../optolink/optolink_text_sensor.cpp | 25 +++ .../optolink/optolink_text_sensor.h | 31 ++++ esphome/components/optolink/select.py | 80 +++++++++ esphome/components/optolink/sensor.py | 51 ++++++ esphome/components/optolink/switch.py | 33 ++++ esphome/components/optolink/text_sensor.py | 50 ++++++ 23 files changed, 1082 insertions(+) create mode 100644 esphome/components/optolink/__init__.py create mode 100644 esphome/components/optolink/binary_sensor.py create mode 100644 esphome/components/optolink/number.py create mode 100644 esphome/components/optolink/optolink.cpp create mode 100755 esphome/components/optolink/optolink.h create mode 100644 esphome/components/optolink/optolink_binary_sensor.h create mode 100644 esphome/components/optolink/optolink_device_info_sensor.h create mode 100644 esphome/components/optolink/optolink_number.cpp create mode 100644 esphome/components/optolink/optolink_number.h create mode 100644 esphome/components/optolink/optolink_select.cpp create mode 100644 esphome/components/optolink/optolink_select.h create mode 100644 esphome/components/optolink/optolink_sensor.h create mode 100644 esphome/components/optolink/optolink_sensor_base.cpp create mode 100644 esphome/components/optolink/optolink_sensor_base.h create mode 100644 esphome/components/optolink/optolink_state_sensor.h create mode 100644 esphome/components/optolink/optolink_switch.cpp create mode 100644 esphome/components/optolink/optolink_switch.h create mode 100644 esphome/components/optolink/optolink_text_sensor.cpp create mode 100644 esphome/components/optolink/optolink_text_sensor.h create mode 100644 esphome/components/optolink/select.py create mode 100644 esphome/components/optolink/sensor.py create mode 100644 esphome/components/optolink/switch.py create mode 100644 esphome/components/optolink/text_sensor.py diff --git a/esphome/components/optolink/__init__.py b/esphome/components/optolink/__init__.py new file mode 100644 index 0000000000..bd409e4a09 --- /dev/null +++ b/esphome/components/optolink/__init__.py @@ -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) diff --git a/esphome/components/optolink/binary_sensor.py b/esphome/components/optolink/binary_sensor.py new file mode 100644 index 0000000000..57f51f2c3e --- /dev/null +++ b/esphome/components/optolink/binary_sensor.py @@ -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])) diff --git a/esphome/components/optolink/number.py b/esphome/components/optolink/number.py new file mode 100644 index 0000000000..2de32e186a --- /dev/null +++ b/esphome/components/optolink/number.py @@ -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])) diff --git a/esphome/components/optolink/optolink.cpp b/esphome/components/optolink/optolink.cpp new file mode 100644 index 0000000000..3fbad9732d --- /dev/null +++ b/esphome/components/optolink/optolink.cpp @@ -0,0 +1,72 @@ +#include "esphome/core/defines.h" +#include "esphome/components/optolink/optolink.h" +#include + +VitoWiFiClass 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 diff --git a/esphome/components/optolink/optolink.h b/esphome/components/optolink/optolink.h new file mode 100755 index 0000000000..3e64c62de4 --- /dev/null +++ b/esphome/components/optolink/optolink.h @@ -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 + +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 diff --git a/esphome/components/optolink/optolink_binary_sensor.h b/esphome/components/optolink/optolink_binary_sensor.h new file mode 100644 index 0000000000..22d8dd5878 --- /dev/null +++ b/esphome/components/optolink/optolink_binary_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_device_info_sensor.h b/esphome/components/optolink/optolink_device_info_sensor.h new file mode 100644 index 0000000000..7611459807 --- /dev/null +++ b/esphome/components/optolink/optolink_device_info_sensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/components/text_sensor/text_sensor.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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(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 diff --git a/esphome/components/optolink/optolink_number.cpp b/esphome/components/optolink/optolink_number.cpp new file mode 100644 index 0000000000..1032645009 --- /dev/null +++ b/esphome/components/optolink/optolink_number.cpp @@ -0,0 +1,20 @@ +#include "optolink_number.h" +#include "optolink.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_number.h b/esphome/components/optolink/optolink_number.h new file mode 100644 index 0000000000..2cc73a46db --- /dev/null +++ b/esphome/components/optolink/optolink_number.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "optolink_sensor_base.h" +#include "optolink.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_select.cpp b/esphome/components/optolink/optolink_select.cpp new file mode 100644 index 0000000000..f7c53077f6 --- /dev/null +++ b/esphome/components/optolink/optolink_select.cpp @@ -0,0 +1,41 @@ +#include "optolink_select.h" +#include "optolink.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_select.h b/esphome/components/optolink/optolink_select.h new file mode 100644 index 0000000000..63b379c1e5 --- /dev/null +++ b/esphome/components/optolink/optolink_select.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include "esphome/components/select/select.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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 *mapping) { + mapping_ = mapping; + std::vector 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 *mapping_ = nullptr; +}; + +} // namespace optolink +} // namespace esphome diff --git a/esphome/components/optolink/optolink_sensor.h b/esphome/components/optolink/optolink_sensor.h new file mode 100644 index 0000000000..b8613fc470 --- /dev/null +++ b/esphome/components/optolink/optolink_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_sensor_base.cpp b/esphome/components/optolink/optolink_sensor_base.cpp new file mode 100644 index 0000000000..599f4b791d --- /dev/null +++ b/esphome/components/optolink/optolink_sensor_base.cpp @@ -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(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(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(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(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(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(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(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 diff --git a/esphome/components/optolink/optolink_sensor_base.h b/esphome/components/optolink/optolink_sensor_base.h new file mode 100644 index 0000000000..93074ea2f2 --- /dev/null +++ b/esphome/components/optolink/optolink_sensor_base.h @@ -0,0 +1,47 @@ + +#pragma once + +#include "esphome/core/log.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_state_sensor.h b/esphome/components/optolink/optolink_state_sensor.h new file mode 100644 index 0000000000..78d99ac19d --- /dev/null +++ b/esphome/components/optolink/optolink_state_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/text_sensor/text_sensor.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_switch.cpp b/esphome/components/optolink/optolink_switch.cpp new file mode 100644 index 0000000000..226dd3d484 --- /dev/null +++ b/esphome/components/optolink/optolink_switch.cpp @@ -0,0 +1,20 @@ +#include "optolink_switch.h" +#include "optolink.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_switch.h b/esphome/components/optolink/optolink_switch.h new file mode 100644 index 0000000000..faf283dacf --- /dev/null +++ b/esphome/components/optolink/optolink_switch.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "optolink_sensor_base.h" +#include "optolink.h" +#include + +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 diff --git a/esphome/components/optolink/optolink_text_sensor.cpp b/esphome/components/optolink/optolink_text_sensor.cpp new file mode 100644 index 0000000000..f226f4f5d0 --- /dev/null +++ b/esphome/components/optolink/optolink_text_sensor.cpp @@ -0,0 +1,25 @@ +#include "optolink_text_sensor.h" +#include "optolink.h" +#include + +namespace esphome { +namespace optolink { + +void OptolinkTextSensor::setup() { + if (!raw_) { + setup_datapoint(); + } else { + datapoint_ = new Datapoint(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: ", dp.getGroup(), dp.getName()); + uint8_t buffer[bytes_ + 1]; + dpValue.getRaw(buffer); + buffer[bytes_] = 0x0; + publish_state((char *) buffer); + }); + } +}; + +} // namespace optolink +} // namespace esphome diff --git a/esphome/components/optolink/optolink_text_sensor.h b/esphome/components/optolink/optolink_text_sensor.h new file mode 100644 index 0000000000..bb181c3965 --- /dev/null +++ b/esphome/components/optolink/optolink_text_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/text_sensor/text_sensor.h" +#include "optolink.h" +#include "optolink_sensor_base.h" +#include + +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 diff --git a/esphome/components/optolink/select.py b/esphome/components/optolink/select.py new file mode 100644 index 0000000000..34c3482206 --- /dev/null +++ b/esphome/components/optolink/select.py @@ -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])) diff --git a/esphome/components/optolink/sensor.py b/esphome/components/optolink/sensor.py new file mode 100644 index 0000000000..4991ac110d --- /dev/null +++ b/esphome/components/optolink/sensor.py @@ -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])) diff --git a/esphome/components/optolink/switch.py b/esphome/components/optolink/switch.py new file mode 100644 index 0000000000..32540cdf0f --- /dev/null +++ b/esphome/components/optolink/switch.py @@ -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])) diff --git a/esphome/components/optolink/text_sensor.py b/esphome/components/optolink/text_sensor.py new file mode 100644 index 0000000000..662474236e --- /dev/null +++ b/esphome/components/optolink/text_sensor.py @@ -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]))