mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +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_WIDTH = 'width' | ||||||
| CONF_WIFI = 'wifi' | CONF_WIFI = 'wifi' | ||||||
| CONF_WILL_MESSAGE = 'will_message' | CONF_WILL_MESSAGE = 'will_message' | ||||||
|  | CONF_WIND_SPEED = 'wind_speed' | ||||||
|  | CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees' | ||||||
| CONF_WINDOW_SIZE = 'window_size' | CONF_WINDOW_SIZE = 'window_size' | ||||||
| CONF_ZERO = 'zero' | CONF_ZERO = 'zero' | ||||||
|  |  | ||||||
| @@ -481,12 +483,14 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right' | |||||||
| ICON_SCALE = 'mdi:scale' | ICON_SCALE = 'mdi:scale' | ||||||
| ICON_SCREEN_ROTATION = 'mdi:screen-rotation' | ICON_SCREEN_ROTATION = 'mdi:screen-rotation' | ||||||
| ICON_SIGNAL = 'mdi:signal' | ICON_SIGNAL = 'mdi:signal' | ||||||
|  | ICON_SIGN_DIRECTION = 'mdi:sign-direction' | ||||||
| ICON_WEATHER_SUNSET = 'mdi:weather-sunset' | ICON_WEATHER_SUNSET = 'mdi:weather-sunset' | ||||||
| ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' | ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' | ||||||
| ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' | ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' | ||||||
| ICON_THERMOMETER = 'mdi:thermometer' | ICON_THERMOMETER = 'mdi:thermometer' | ||||||
| ICON_TIMER = 'mdi:timer' | ICON_TIMER = 'mdi:timer' | ||||||
| ICON_WATER_PERCENT = 'mdi:water-percent' | ICON_WATER_PERCENT = 'mdi:water-percent' | ||||||
|  | ICON_WEATHER_WINDY = 'mdi:weather-windy' | ||||||
| ICON_WIFI = 'mdi:wifi' | ICON_WIFI = 'mdi:wifi' | ||||||
|  |  | ||||||
| UNIT_AMPERE = 'A' | UNIT_AMPERE = 'A' | ||||||
| @@ -498,6 +502,7 @@ UNIT_EMPTY = '' | |||||||
| UNIT_HZ = 'hz' | UNIT_HZ = 'hz' | ||||||
| UNIT_HECTOPASCAL = 'hPa' | UNIT_HECTOPASCAL = 'hPa' | ||||||
| UNIT_KELVIN = 'K' | UNIT_KELVIN = 'K' | ||||||
|  | UNIT_KILOMETER_PER_HOUR = 'km/h' | ||||||
| UNIT_LUX = 'lx' | UNIT_LUX = 'lx' | ||||||
| UNIT_METER = 'm' | UNIT_METER = 'm' | ||||||
| UNIT_METER_PER_SECOND_SQUARED = u'm/s²' | UNIT_METER_PER_SECOND_SQUARED = u'm/s²' | ||||||
|   | |||||||
| @@ -564,6 +564,14 @@ sensor: | |||||||
|       name: CCS811 TVOC |       name: CCS811 TVOC | ||||||
|     update_interval: 30s |     update_interval: 30s | ||||||
|     baseline: 0x4242 |     baseline: 0x4242 | ||||||
|  |   - platform: tx20 | ||||||
|  |     wind_speed: | ||||||
|  |       name: "Windspeed" | ||||||
|  |     wind_direction_degrees: | ||||||
|  |       name: "Winddirection Degrees" | ||||||
|  |     pin:  | ||||||
|  |       number: GPIO04 | ||||||
|  |       mode: INPUT | ||||||
|   - platform: zyaura |   - platform: zyaura | ||||||
|     clock_pin: GPIO5 |     clock_pin: GPIO5 | ||||||
|     data_pin: GPIO4 |     data_pin: GPIO4 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user