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/sun/* @OttoWinter | ||||||
| esphome/components/switch/* @esphome/core | esphome/components/switch/* @esphome/core | ||||||
| esphome/components/tcl112/* @glmnet | esphome/components/tcl112/* @glmnet | ||||||
|  | esphome/components/teleinfo/* @0hax | ||||||
| esphome/components/time/* @OttoWinter | esphome/components/time/* @OttoWinter | ||||||
| esphome/components/tm1637/* @glmnet | esphome/components/tm1637/* @glmnet | ||||||
| esphome/components/tmp102/* @timsavage | 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: |     aqi: | ||||||
|       name: "AQI" |       name: "AQI" | ||||||
|       calculation_type: "CAQI" |       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 |   - platform: mcp9808 | ||||||
|     name: "MCP9808 Temperature" |     name: "MCP9808 Temperature" | ||||||
|     update_interval: 15s |     update_interval: 15s | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user