mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	components: teleinfo: electrical counter information. (#1108)
Signed-off-by: 0hax <0hax@protonmail.com> Co-authored-by: Otto Winter <otto@otto-winter.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/teleinfo/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/teleinfo/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ['@0hax'] | ||||
							
								
								
									
										34
									
								
								esphome/components/teleinfo/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/teleinfo/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
							
								
								
									
										184
									
								
								esphome/components/teleinfo/teleinfo.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								esphome/components/teleinfo/teleinfo.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<char *>(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<char *>(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<char *>(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 | ||||
							
								
								
									
										51
									
								
								esphome/components/teleinfo/teleinfo.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/teleinfo/teleinfo.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<const TeleinfoSensorElement *> 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 | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user