diff --git a/CODEOWNERS b/CODEOWNERS index e6832d1580..505df40f04 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,6 +61,7 @@ esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core esphome/components/tcl112/* @glmnet +esphome/components/teleinfo/* @0hax esphome/components/time/* @OttoWinter esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py new file mode 100644 index 0000000000..00ca592272 --- /dev/null +++ b/esphome/components/teleinfo/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@0hax'] diff --git a/esphome/components/teleinfo/sensor.py b/esphome/components/teleinfo/sensor.py new file mode 100644 index 0000000000..54b50a9921 --- /dev/null +++ b/esphome/components/teleinfo/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_ID, CONF_SENSOR, ICON_FLASH, UNIT_WATT_HOURS + +DEPENDENCIES = ['uart'] + +teleinfo_ns = cg.esphome_ns.namespace('teleinfo') +TeleInfo = teleinfo_ns.class_('TeleInfo', cg.PollingComponent, uart.UARTDevice) + +CONF_TAG_NAME = "tag_name" +TELEINFO_TAG_SCHEMA = cv.Schema({ + cv.Required(CONF_TAG_NAME): cv.string, + cv.Required(CONF_SENSOR): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0) +}) + +CONF_TAGS = "tags" +CONF_HISTORICAL_MODE = "historical_mode" +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TeleInfo), + cv.Optional(CONF_HISTORICAL_MODE, default=False): cv.boolean, + cv.Optional(CONF_TAGS): cv.ensure_list(TELEINFO_TAG_SCHEMA), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_TAGS in config: + for tag in config[CONF_TAGS]: + sens = yield sensor.new_sensor(tag[CONF_SENSOR]) + cg.add(var.register_teleinfo_sensor(tag[CONF_TAG_NAME], sens)) diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp new file mode 100644 index 0000000000..7c0a83d103 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -0,0 +1,184 @@ +#include "teleinfo.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace teleinfo { + +static const char *TAG = "teleinfo"; + +/* Helpers */ +static int get_field(char *dest, char *buf_start, char *buf_end, int sep) { + char *field_end; + int len; + + field_end = static_cast(memchr(buf_start, sep, buf_end - buf_start)); + if (!field_end) + return 0; + len = field_end - buf_start; + strncpy(dest, buf_start, len); + dest[len] = '\0'; + + return len; +} +/* TeleInfo methods */ +bool TeleInfo::check_crc_(const char *grp, const char *grp_end) { + int grp_len = grp_end - grp; + uint8_t raw_crc = grp[grp_len - 1]; + uint8_t crc_tmp = 0; + int i; + + for (i = 0; i < grp_len - checksum_area_end_; i++) + crc_tmp += grp[i]; + + crc_tmp &= 0x3F; + crc_tmp += 0x20; + if (raw_crc != crc_tmp) { + ESP_LOGE(TAG, "bad crc: got %d except %d", raw_crc, crc_tmp); + return false; + } + + return true; +} +bool TeleInfo::read_chars_until_(bool drop, uint8_t c) { + uint8_t received; + int j = 0; + + while (available() > 0 && j < 128) { + j++; + received = read(); + if (received == c) + return true; + if (drop) + continue; + /* + * Internal buffer is full, switch to OFF mode. + * Data will be retrieved on next update. + */ + if (buf_index_ >= (MAX_BUF_SIZE - 1)) { + ESP_LOGW(TAG, "Internal buffer full"); + state_ = OFF; + return false; + } + buf_[buf_index_++] = received; + } + + return false; +} +void TeleInfo::setup() { state_ = OFF; } +void TeleInfo::update() { + if (state_ == OFF) { + buf_index_ = 0; + state_ = ON; + } +} +void TeleInfo::loop() { + switch (state_) { + case OFF: + break; + case ON: + /* Dequeue chars until start frame (0x2) */ + if (read_chars_until_(true, 0x2)) + state_ = START_FRAME_RECEIVED; + break; + case START_FRAME_RECEIVED: + /* Dequeue chars until end frame (0x3) */ + if (read_chars_until_(false, 0x3)) + state_ = END_FRAME_RECEIVED; + break; + case END_FRAME_RECEIVED: + char *buf_finger; + char *grp_end; + char *buf_end; + int field_len; + + buf_finger = buf_; + buf_end = buf_ + buf_index_; + + /* Each frame is composed of multiple groups starting by 0xa(Line Feed) and ending by + * 0xd ('\r'). + * + * Historical mode: each group contains tag, data and a CRC separated by 0x20 (Space) + * 0xa | Tag | 0x20 | Data | 0x20 | CRC | 0xd + * ^^^^^^^^^^^^^^^^^^^^ + * Checksum is computed on the above in historical mode. + * + * Standard mode: each group contains tag, data and a CRC separated by 0x9 (\t) + * 0xa | Tag | 0x9 | Data | 0x9 | CRC | 0xd + * ^^^^^^^^^^^^^^^^^^^^^^^^^ + * Checksum is computed on the above in standard mode. + */ + while ((buf_finger = static_cast(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) && + ((buf_finger - buf_) < buf_index_)) { + /* Point to the first char of the group after 0xa */ + buf_finger += 1; + + /* Group len */ + grp_end = static_cast(memchr(buf_finger, 0xd, buf_end - buf_finger)); + if (!grp_end) { + ESP_LOGE(TAG, "No group found"); + break; + } + + if (!check_crc_(buf_finger, grp_end)) + break; + + /* Get tag */ + field_len = get_field(tag_, buf_finger, grp_end, separator_); + if (!field_len || field_len >= MAX_TAG_SIZE) { + ESP_LOGE(TAG, "Invalid tag."); + break; + } + + /* Advance buf_finger to after the tag and the separator. */ + buf_finger += field_len + 1; + + /* Get value (after next separator) */ + field_len = get_field(val_, buf_finger, grp_end, separator_); + if (!field_len || field_len >= MAX_VAL_SIZE) { + ESP_LOGE(TAG, "Invalid Value"); + break; + } + + /* Advance buf_finger to end of group */ + buf_finger += field_len + 1 + 1 + 1; + + publish_value_(std::string(tag_), std::string(val_)); + } + state_ = OFF; + break; + } +} +void TeleInfo::publish_value_(std::string tag, std::string val) { + /* It will return 0 if tag is not a float. */ + auto newval = parse_float(val); + for (auto element : teleinfo_sensors_) + if (tag == element->tag) + element->sensor->publish_state(*newval); +} +void TeleInfo::dump_config() { + ESP_LOGCONFIG(TAG, "TeleInfo:"); + for (auto element : teleinfo_sensors_) + LOG_SENSOR(" ", element->tag, element->sensor); + this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7); +} +TeleInfo::TeleInfo(bool historical_mode) { + if (historical_mode) { + /* + * Historical mode doesn't contain last separator between checksum and data. + */ + checksum_area_end_ = 2; + separator_ = 0x20; + baud_rate_ = 1200; + } else { + checksum_area_end_ = 1; + separator_ = 0x9; + baud_rate_ = 9600; + } +} +void TeleInfo::register_teleinfo_sensor(const char *tag, sensor::Sensor *sensor) { + const TeleinfoSensorElement *teleinfo_sensor = new TeleinfoSensorElement{tag, sensor}; + teleinfo_sensors_.push_back(teleinfo_sensor); +} + +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h new file mode 100644 index 0000000000..de9cf646c4 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace teleinfo { +/* + * 198 bytes should be enough to contain a full session in historical mode with + * three phases. But go with 1024 just to be sure. + */ +static const uint8_t MAX_TAG_SIZE = 64; +static const uint16_t MAX_VAL_SIZE = 256; +static const uint16_t MAX_BUF_SIZE = 1024; + +struct TeleinfoSensorElement { + const char *tag; + sensor::Sensor *sensor; +}; + +class TeleInfo : public PollingComponent, public uart::UARTDevice { + public: + TeleInfo(bool historical_mode); + void register_teleinfo_sensor(const char *tag, sensor::Sensor *sensors); + void loop() override; + void setup() override; + void update() override; + void dump_config() override; + std::vector teleinfo_sensors_{}; + + protected: + uint32_t baud_rate_; + int checksum_area_end_; + int separator_; + char buf_[MAX_BUF_SIZE]; + uint32_t buf_index_{0}; + char tag_[MAX_TAG_SIZE]; + char val_[MAX_VAL_SIZE]; + enum State { + OFF, + ON, + START_FRAME_RECEIVED, + END_FRAME_RECEIVED, + } state_{OFF}; + bool read_chars_until_(bool drop, uint8_t c); + bool check_crc_(const char *grp, const char *grp_end); + void publish_value_(std::string tag, std::string val); +}; +} // namespace teleinfo +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index c80ab924d1..4bb8e1c176 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -762,6 +762,25 @@ sensor: aqi: name: "AQI" calculation_type: "CAQI" + - platform: teleinfo + tags: + - tag_name: "HCHC" + sensor: + name: "hchc" + unit_of_measurement: "Wh" + icon: mdi:flash + - tag_name: "HCHP" + sensor: + name: "hchp" + unit_of_measurement: "Wh" + icon: mdi:flash + - tag_name: "PAPP" + sensor: + name: "papp" + unit_of_measurement: "VA" + icon: mdi:flash + update_interval: 60s + historical_mode: true - platform: mcp9808 name: "MCP9808 Temperature" update_interval: 15s