mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	added tx20 wind speed sensor (#275)
* added tx20 wind speed sensor * added test * fixed lint errors * fixed more lint errors * updated tx20 * updated tx20 sensor * updated to new structure and removed static variables * removed content from __init__.py * fixing lint errors * resolved issues from review Co-authored-by: Thomas <thomas.eckerstorfer@mic-cust.com> Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
		
				
					committed by
					
						 Otto Winter
						Otto Winter
					
				
			
			
				
	
			
			
			
						parent
						
							81a070d03d
						
					
				
				
					commit
					c6512013bb
				
			
							
								
								
									
										0
									
								
								esphome/components/tx20/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/tx20/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										38
									
								
								esphome/components/tx20/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/tx20/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import sensor | ||||
| from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \ | ||||
|     CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \ | ||||
|     UNIT_EMPTY, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION | ||||
|  | ||||
| tx20_ns = cg.esphome_ns.namespace('tx20') | ||||
| Tx20Component = tx20_ns.class_('Tx20Component', cg.Component) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema({ | ||||
|     cv.GenerateID(): cv.declare_id(Tx20Component), | ||||
|     cv.Optional(CONF_WIND_SPEED): | ||||
|         sensor.sensor_schema(UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1), | ||||
|     cv.Optional(CONF_WIND_DIRECTION_DEGREES): | ||||
|         sensor.sensor_schema(UNIT_EMPTY, ICON_SIGN_DIRECTION, 1), | ||||
|     cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, | ||||
|                                   pins.validate_has_interrupt), | ||||
| }).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield cg.register_component(var, config) | ||||
|  | ||||
|     if CONF_WIND_SPEED in config: | ||||
|         conf = config[CONF_WIND_SPEED] | ||||
|         sens = yield sensor.new_sensor(conf) | ||||
|         cg.add(var.set_wind_speed_sensor(sens)) | ||||
|  | ||||
|     if CONF_WIND_DIRECTION_DEGREES in config: | ||||
|         conf = config[CONF_WIND_DIRECTION_DEGREES] | ||||
|         sens = yield sensor.new_sensor(conf) | ||||
|         cg.add(var.set_wind_direction_degrees_sensor(sens)) | ||||
|  | ||||
|     pin = yield cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     cg.add(var.set_pin(pin)) | ||||
							
								
								
									
										195
									
								
								esphome/components/tx20/tx20.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								esphome/components/tx20/tx20.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| #include "tx20.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace tx20 { | ||||
|  | ||||
| static const char *TAG = "tx20"; | ||||
| static const uint8_t MAX_BUFFER_SIZE = 41; | ||||
| static const uint16_t TX20_MAX_TIME = MAX_BUFFER_SIZE * 1200 + 5000; | ||||
| static const uint16_t TX20_BIT_TIME = 1200; | ||||
| static const char *DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", | ||||
|                                    "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; | ||||
|  | ||||
| void Tx20Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up Tx20"); | ||||
|   this->pin_->setup(); | ||||
|  | ||||
|   this->store_.buffer = new uint16_t[MAX_BUFFER_SIZE]; | ||||
|   this->store_.pin = this->pin_->to_isr(); | ||||
|   this->store_.reset(); | ||||
|  | ||||
|   this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, CHANGE); | ||||
| } | ||||
| void Tx20Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Tx20:"); | ||||
|  | ||||
|   LOG_SENSOR("  ", "Wind speed:", this->wind_speed_sensor_); | ||||
|   LOG_SENSOR("  ", "Wind direction degrees:", this->wind_direction_degrees_sensor_); | ||||
|  | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| } | ||||
| void Tx20Component::loop() { | ||||
|   if (this->store_.tx20_available) { | ||||
|     this->decode_and_publish_(); | ||||
|     this->store_.reset(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } | ||||
|  | ||||
| void Tx20Component::decode_and_publish_() { | ||||
|   ESP_LOGVV(TAG, "Decode Tx20..."); | ||||
|  | ||||
|   std::string string_buffer; | ||||
|   std::string string_buffer_2; | ||||
|   std::vector<bool> bit_buffer; | ||||
|   bool current_bit = true; | ||||
|  | ||||
|   for (int i = 1; i <= this->store_.buffer_index; i++) { | ||||
|     string_buffer_2 += to_string(this->store_.buffer[i]) + ", "; | ||||
|     uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; | ||||
|     // ignore segments at the end that were too short | ||||
|     string_buffer.append(repeat, current_bit ? '1' : '0'); | ||||
|     bit_buffer.insert(bit_buffer.end(), repeat, current_bit); | ||||
|     current_bit = !current_bit; | ||||
|   } | ||||
|   current_bit = !current_bit; | ||||
|   if (string_buffer.length() < MAX_BUFFER_SIZE) { | ||||
|     uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); | ||||
|     string_buffer_2 += to_string(remain) + ", "; | ||||
|     string_buffer.append(remain, current_bit ? '1' : '0'); | ||||
|     bit_buffer.insert(bit_buffer.end(), remain, current_bit); | ||||
|   } | ||||
|  | ||||
|   uint8_t tx20_sa = 0; | ||||
|   uint8_t tx20_sb = 0; | ||||
|   uint8_t tx20_sd = 0; | ||||
|   uint8_t tx20_se = 0; | ||||
|   uint16_t tx20_sc = 0; | ||||
|   uint16_t tx20_sf = 0; | ||||
|   uint8_t tx20_wind_direction = 0; | ||||
|   float tx20_wind_speed_kmh = 0; | ||||
|   uint8_t bit_count = 0; | ||||
|  | ||||
|   for (int i = 41; i > 0; i--) { | ||||
|     uint8_t bit = bit_buffer.at(bit_count); | ||||
|     bit_count++; | ||||
|     if (i > 41 - 5) { | ||||
|       // start, inverted | ||||
|       tx20_sa = (tx20_sa << 1) | (bit ^ 1); | ||||
|     } else if (i > 41 - 5 - 4) { | ||||
|       // wind dir, inverted | ||||
|       tx20_sb = tx20_sb >> 1 | ((bit ^ 1) << 3); | ||||
|     } else if (i > 41 - 5 - 4 - 12) { | ||||
|       // windspeed, inverted | ||||
|       tx20_sc = tx20_sc >> 1 | ((bit ^ 1) << 11); | ||||
|     } else if (i > 41 - 5 - 4 - 12 - 4) { | ||||
|       // checksum, inverted | ||||
|       tx20_sd = tx20_sd >> 1 | ((bit ^ 1) << 3); | ||||
|     } else if (i > 41 - 5 - 4 - 12 - 4 - 4) { | ||||
|       // wind dir | ||||
|       tx20_se = tx20_se >> 1 | (bit << 3); | ||||
|     } else { | ||||
|       // windspeed | ||||
|       tx20_sf = tx20_sf >> 1 | (bit << 11); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   uint8_t chk = (tx20_sb + (tx20_sc & 0xf) + ((tx20_sc >> 4) & 0xf) + ((tx20_sc >> 8) & 0xf)); | ||||
|   chk &= 0xf; | ||||
|   bool value_set = false; | ||||
|   // checks: | ||||
|   // 1. Check that the start frame is 00100 (0x04) | ||||
|   // 2. Check received checksum matches calculated checksum | ||||
|   // 3. Check that Wind Direction matches Wind Direction (Inverted) | ||||
|   // 4. Check that Wind Speed matches Wind Speed (Inverted) | ||||
|   ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); | ||||
|   ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); | ||||
|  | ||||
|   if (tx20_sa == 4) { | ||||
|     if (chk == tx20_sd) { | ||||
|       if (tx20_sf == tx20_sc) { | ||||
|         tx20_wind_speed_kmh = float(tx20_sc) * 0.36; | ||||
|         ESP_LOGV(TAG, "WindSpeed %f", tx20_wind_speed_kmh); | ||||
|         if (this->wind_speed_sensor_ != nullptr) | ||||
|           this->wind_speed_sensor_->publish_state(tx20_wind_speed_kmh); | ||||
|         value_set = true; | ||||
|       } | ||||
|       if (tx20_se == tx20_sb) { | ||||
|         tx20_wind_direction = tx20_se; | ||||
|         if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { | ||||
|           wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; | ||||
|         } | ||||
|         ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); | ||||
|         if (this->wind_direction_degrees_sensor_ != nullptr) | ||||
|           this->wind_direction_degrees_sensor_->publish_state(float(tx20_wind_direction) * 22.5f); | ||||
|         value_set = true; | ||||
|       } | ||||
|       if (!value_set) { | ||||
|         ESP_LOGW(TAG, "No value set!"); | ||||
|       } | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Checksum wrong!"); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Start wrong!"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { | ||||
|   arg->pin_state = arg->pin->digital_read(); | ||||
|   const uint32_t now = micros(); | ||||
|   if (!arg->start_time) { | ||||
|     // only detect a start if the bit is high | ||||
|     if (!arg->pin_state) { | ||||
|       return; | ||||
|     } | ||||
|     arg->buffer[arg->buffer_index] = 1; | ||||
|     arg->start_time = now; | ||||
|     arg->buffer_index++; | ||||
|     return; | ||||
|   } | ||||
|   const uint32_t delay = now - arg->start_time; | ||||
|   const uint8_t index = arg->buffer_index; | ||||
|  | ||||
|   // first delay has to be ~2400 | ||||
|   if (index == 1 && (delay > 3000 || delay < 2400)) { | ||||
|     arg->reset(); | ||||
|     return; | ||||
|   } | ||||
|   // second delay has to be ~1200 | ||||
|   if (index == 2 && (delay > 1500 || delay < 1200)) { | ||||
|     arg->reset(); | ||||
|     return; | ||||
|   } | ||||
|   // third delay has to be ~2400 | ||||
|   if (index == 3 && (delay > 3000 || delay < 2400)) { | ||||
|     arg->reset(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (arg->tx20_available || ((arg->spent_time + delay > TX20_MAX_TIME) && arg->start_time)) { | ||||
|     arg->tx20_available = true; | ||||
|     return; | ||||
|   } | ||||
|   if (index <= MAX_BUFFER_SIZE) { | ||||
|     arg->buffer[index] = delay; | ||||
|   } | ||||
|   arg->spent_time += delay; | ||||
|   arg->start_time = now; | ||||
|   arg->buffer_index++; | ||||
| } | ||||
| void ICACHE_RAM_ATTR Tx20ComponentStore::reset() { | ||||
|   tx20_available = false; | ||||
|   buffer_index = 0; | ||||
|   spent_time = 0; | ||||
|   // rearm it! | ||||
|   start_time = 0; | ||||
| } | ||||
|  | ||||
| }  // namespace tx20 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										51
									
								
								esphome/components/tx20/tx20.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/tx20/tx20.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace tx20 { | ||||
|  | ||||
| /// Store data in a class that doesn't use multiple-inheritance (vtables in flash) | ||||
| struct Tx20ComponentStore { | ||||
|   volatile uint16_t *buffer; | ||||
|   volatile uint32_t start_time; | ||||
|   volatile uint8_t buffer_index; | ||||
|   volatile uint32_t spent_time; | ||||
|   volatile bool tx20_available; | ||||
|   volatile bool pin_state; | ||||
|   ISRInternalGPIOPin *pin; | ||||
|  | ||||
|   void reset(); | ||||
|   static void gpio_intr(Tx20ComponentStore *arg); | ||||
| }; | ||||
|  | ||||
| /// This class implements support for the Tx20 Wind sensor. | ||||
| class Tx20Component : public Component { | ||||
|  public: | ||||
|   /// Get the textual representation of the wind direction ('N', 'SSE', ..). | ||||
|   std::string get_wind_cardinal_direction() const; | ||||
|  | ||||
|   void set_pin(GPIOPin *pin) { pin_ = pin; } | ||||
|   void set_wind_speed_sensor(sensor::Sensor *wind_speed_sensor) { wind_speed_sensor_ = wind_speed_sensor; } | ||||
|   void set_wind_direction_degrees_sensor(sensor::Sensor *wind_direction_degrees_sensor) { | ||||
|     wind_direction_degrees_sensor_ = wind_direction_degrees_sensor; | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void loop() override; | ||||
|  | ||||
|  protected: | ||||
|   void decode_and_publish_(); | ||||
|  | ||||
|   std::string wind_cardinal_direction_; | ||||
|   GPIOPin *pin_; | ||||
|   sensor::Sensor *wind_speed_sensor_; | ||||
|   sensor::Sensor *wind_direction_degrees_sensor_; | ||||
|   Tx20ComponentStore store_; | ||||
| }; | ||||
|  | ||||
| }  // namespace tx20 | ||||
| }  // namespace esphome | ||||
| @@ -454,6 +454,8 @@ CONF_WHITE = 'white' | ||||
| CONF_WIDTH = 'width' | ||||
| CONF_WIFI = 'wifi' | ||||
| CONF_WILL_MESSAGE = 'will_message' | ||||
| CONF_WIND_SPEED = 'wind_speed' | ||||
| CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees' | ||||
| CONF_WINDOW_SIZE = 'window_size' | ||||
| CONF_ZERO = 'zero' | ||||
|  | ||||
| @@ -481,12 +483,14 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right' | ||||
| ICON_SCALE = 'mdi:scale' | ||||
| ICON_SCREEN_ROTATION = 'mdi:screen-rotation' | ||||
| ICON_SIGNAL = 'mdi:signal' | ||||
| ICON_SIGN_DIRECTION = 'mdi:sign-direction' | ||||
| ICON_WEATHER_SUNSET = 'mdi:weather-sunset' | ||||
| ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' | ||||
| ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' | ||||
| ICON_THERMOMETER = 'mdi:thermometer' | ||||
| ICON_TIMER = 'mdi:timer' | ||||
| ICON_WATER_PERCENT = 'mdi:water-percent' | ||||
| ICON_WEATHER_WINDY = 'mdi:weather-windy' | ||||
| ICON_WIFI = 'mdi:wifi' | ||||
|  | ||||
| UNIT_AMPERE = 'A' | ||||
| @@ -498,6 +502,7 @@ UNIT_EMPTY = '' | ||||
| UNIT_HZ = 'hz' | ||||
| UNIT_HECTOPASCAL = 'hPa' | ||||
| UNIT_KELVIN = 'K' | ||||
| UNIT_KILOMETER_PER_HOUR = 'km/h' | ||||
| UNIT_LUX = 'lx' | ||||
| UNIT_METER = 'm' | ||||
| UNIT_METER_PER_SECOND_SQUARED = u'm/s²' | ||||
|   | ||||
| @@ -564,6 +564,14 @@ sensor: | ||||
|       name: CCS811 TVOC | ||||
|     update_interval: 30s | ||||
|     baseline: 0x4242 | ||||
|   - platform: tx20 | ||||
|     wind_speed: | ||||
|       name: "Windspeed" | ||||
|     wind_direction_degrees: | ||||
|       name: "Winddirection Degrees" | ||||
|     pin:  | ||||
|       number: GPIO04 | ||||
|       mode: INPUT | ||||
|   - platform: zyaura | ||||
|     clock_pin: GPIO5 | ||||
|     data_pin: GPIO4 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user