mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add Bayesian type for binary_sensor_map component (#4640)
* initial support for Bayesian type * Cast bool state of binary_sensor to uint64_t * Rename channels to observations with Bayesian * Improve/standardize comments for all types * Use black to correct sensor.py formatting * Add SUM and BAYESIAN binary sensor map tests * Remove unused variable * Update esphome/components/binary_sensor_map/binary_sensor_map.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -16,6 +16,9 @@ void BinarySensorMap::loop() { | |||||||
|     case BINARY_SENSOR_MAP_TYPE_SUM: |     case BINARY_SENSOR_MAP_TYPE_SUM: | ||||||
|       this->process_sum_(); |       this->process_sum_(); | ||||||
|       break; |       break; | ||||||
|  |     case BINARY_SENSOR_MAP_TYPE_BAYESIAN: | ||||||
|  |       this->process_bayesian_(); | ||||||
|  |       break; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -23,46 +26,51 @@ void BinarySensorMap::process_group_() { | |||||||
|   float total_current_value = 0.0; |   float total_current_value = 0.0; | ||||||
|   uint8_t num_active_sensors = 0; |   uint8_t num_active_sensors = 0; | ||||||
|   uint64_t mask = 0x00; |   uint64_t mask = 0x00; | ||||||
|   // check all binary_sensors for its state. when active add its value to total_current_value. |  | ||||||
|   // create a bitmask for the binary_sensor status on all channels |   // - check all binary_sensors for its state | ||||||
|  |   //  - if active, add its value to total_current_value. | ||||||
|  |   // - creates a bitmask for the binary_sensor states on all channels | ||||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { |   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||||
|     auto bs = this->channels_[i]; |     auto bs = this->channels_[i]; | ||||||
|     if (bs.binary_sensor->state) { |     if (bs.binary_sensor->state) { | ||||||
|       num_active_sensors++; |       num_active_sensors++; | ||||||
|       total_current_value += bs.sensor_value; |       total_current_value += bs.parameters.sensor_value; | ||||||
|       mask |= 1ULL << i; |       mask |= 1ULL << i; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   // check if the sensor map was touched |  | ||||||
|  |   // potentially update state only if a binary_sensor is active | ||||||
|   if (mask != 0ULL) { |   if (mask != 0ULL) { | ||||||
|     // did the bit_mask change or is it a new sensor touch |     // publish the average if the bitmask has changed | ||||||
|     if (this->last_mask_ != mask) { |     if (this->last_mask_ != mask) { | ||||||
|       float publish_value = total_current_value / num_active_sensors; |       float publish_value = total_current_value / num_active_sensors; | ||||||
|       this->publish_state(publish_value); |       this->publish_state(publish_value); | ||||||
|     } |     } | ||||||
|   } else if (this->last_mask_ != 0ULL) { |   } else if (this->last_mask_ != 0ULL) { | ||||||
|     // is this a new sensor release |     // no buttons are pressed and the states have changed since last run, so publish NAN | ||||||
|     ESP_LOGV(TAG, "'%s' - No binary sensor active, publishing NAN", this->name_.c_str()); |     ESP_LOGV(TAG, "'%s' - No binary sensor active, publishing NAN", this->name_.c_str()); | ||||||
|     this->publish_state(NAN); |     this->publish_state(NAN); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->last_mask_ = mask; |   this->last_mask_ = mask; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BinarySensorMap::process_sum_() { | void BinarySensorMap::process_sum_() { | ||||||
|   float total_current_value = 0.0; |   float total_current_value = 0.0; | ||||||
|   uint64_t mask = 0x00; |   uint64_t mask = 0x00; | ||||||
|  |  | ||||||
|   // - check all binary_sensor states |   // - check all binary_sensor states | ||||||
|   // - if active, add its value to total_current_value |   // - if active, add its value to total_current_value | ||||||
|   // - creates a bitmask for the binary_sensor status on all channels |   // - creates a bitmask for the binary_sensor states on all channels | ||||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { |   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||||
|     auto bs = this->channels_[i]; |     auto bs = this->channels_[i]; | ||||||
|     if (bs.binary_sensor->state) { |     if (bs.binary_sensor->state) { | ||||||
|       total_current_value += bs.sensor_value; |       total_current_value += bs.parameters.sensor_value; | ||||||
|       mask |= 1ULL << i; |       mask |= 1ULL << i; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // update state only if the binary sensor states have changed or if no state has ever been sent on boot |   // update state only if any binary_sensor states have changed or if no state has ever been sent on boot | ||||||
|   if ((this->last_mask_ != mask) || (!this->has_state())) { |   if ((this->last_mask_ != mask) || (!this->has_state())) { | ||||||
|     this->publish_state(total_current_value); |     this->publish_state(total_current_value); | ||||||
|   } |   } | ||||||
| @@ -70,15 +78,65 @@ void BinarySensorMap::process_sum_() { | |||||||
|   this->last_mask_ = mask; |   this->last_mask_ = mask; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void BinarySensorMap::process_bayesian_() { | ||||||
|  |   float posterior_probability = this->bayesian_prior_; | ||||||
|  |   uint64_t mask = 0x00; | ||||||
|  |  | ||||||
|  |   // - compute the posterior probability by taking the product of the predicate probablities for each observation | ||||||
|  |   // - create a bitmask for the binary_sensor states on all channels/observations | ||||||
|  |   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||||
|  |     auto bs = this->channels_[i]; | ||||||
|  |  | ||||||
|  |     posterior_probability *= | ||||||
|  |         this->bayesian_predicate_(bs.binary_sensor->state, posterior_probability, | ||||||
|  |                                   bs.parameters.probabilities.given_true, bs.parameters.probabilities.given_false); | ||||||
|  |  | ||||||
|  |     mask |= ((uint64_t) (bs.binary_sensor->state)) << i; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // update state only if any binary_sensor states have changed or if no state has ever been sent on boot | ||||||
|  |   if ((this->last_mask_ != mask) || (!this->has_state())) { | ||||||
|  |     this->publish_state(posterior_probability); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->last_mask_ = mask; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float BinarySensorMap::bayesian_predicate_(bool sensor_state, float prior, float prob_given_true, | ||||||
|  |                                            float prob_given_false) { | ||||||
|  |   float prob_state_source_true = prob_given_true; | ||||||
|  |   float prob_state_source_false = prob_given_false; | ||||||
|  |  | ||||||
|  |   // if sensor is off, then we use the probabilities for the observation's complement | ||||||
|  |   if (!sensor_state) { | ||||||
|  |     prob_state_source_true = 1 - prob_given_true; | ||||||
|  |     prob_state_source_false = 1 - prob_given_false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false); | ||||||
|  | } | ||||||
|  |  | ||||||
| void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) { | void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) { | ||||||
|   BinarySensorMapChannel sensor_channel{ |   BinarySensorMapChannel sensor_channel{ | ||||||
|       .binary_sensor = sensor, |       .binary_sensor = sensor, | ||||||
|       .sensor_value = value, |       .parameters{ | ||||||
|  |           .sensor_value = value, | ||||||
|  |       }, | ||||||
|   }; |   }; | ||||||
|   this->channels_.push_back(sensor_channel); |   this->channels_.push_back(sensor_channel); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BinarySensorMap::set_sensor_type(BinarySensorMapType sensor_type) { this->sensor_type_ = sensor_type; } | void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float prob_given_true, float prob_given_false) { | ||||||
|  |   BinarySensorMapChannel sensor_channel{ | ||||||
|  |       .binary_sensor = sensor, | ||||||
|  |       .parameters{ | ||||||
|  |           .probabilities{ | ||||||
|  |               .given_true = prob_given_true, | ||||||
|  |               .given_false = prob_given_false, | ||||||
|  |           }, | ||||||
|  |       }, | ||||||
|  |   }; | ||||||
|  |   this->channels_.push_back(sensor_channel); | ||||||
|  | } | ||||||
| }  // namespace binary_sensor_map | }  // namespace binary_sensor_map | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -12,51 +12,88 @@ namespace binary_sensor_map { | |||||||
| enum BinarySensorMapType { | enum BinarySensorMapType { | ||||||
|   BINARY_SENSOR_MAP_TYPE_GROUP, |   BINARY_SENSOR_MAP_TYPE_GROUP, | ||||||
|   BINARY_SENSOR_MAP_TYPE_SUM, |   BINARY_SENSOR_MAP_TYPE_SUM, | ||||||
|  |   BINARY_SENSOR_MAP_TYPE_BAYESIAN, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct BinarySensorMapChannel { | struct BinarySensorMapChannel { | ||||||
|   binary_sensor::BinarySensor *binary_sensor; |   binary_sensor::BinarySensor *binary_sensor; | ||||||
|   float sensor_value; |   union { | ||||||
|  |     float sensor_value; | ||||||
|  |     struct { | ||||||
|  |       float given_true; | ||||||
|  |       float given_false; | ||||||
|  |     } probabilities; | ||||||
|  |   } parameters; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** Class to group binary_sensors to one Sensor. | /** Class to map one or more binary_sensors to one Sensor. | ||||||
|  * |  * | ||||||
|  * Each binary sensor represents a float value in the group. |  * Each binary sensor has configured parameters that each mapping type uses to compute the single numerical result | ||||||
|  */ |  */ | ||||||
| class BinarySensorMap : public sensor::Sensor, public Component { | class BinarySensorMap : public sensor::Sensor, public Component { | ||||||
|  public: |  public: | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * The loop checks all binary_sensor states |    * The loop calls the configured type processing method | ||||||
|    * When the binary_sensor reports a true value for its state, then the float value it represents is added to the |  | ||||||
|    * total_current_value |  | ||||||
|    * |    * | ||||||
|    * Only when the total_current_value changed and at least one sensor reports an active state we publish the sensors |    * The processing method loops through all sensors and calculates the numerical result | ||||||
|    * average value. When the value changed and no sensors ar active we publish NAN. |    * The result is only published if a binary sensor state has changed or, for some types, on initial boot | ||||||
|    * */ |    */ | ||||||
|   void loop() override; |   void loop() override; | ||||||
|   float get_setup_priority() const override { return setup_priority::DATA; } |  | ||||||
|   /** Add binary_sensors to the group. |   /** | ||||||
|    * Each binary_sensor represents a float value when its state is true |    * Add binary_sensors to the group when only one parameter is needed for the configured mapping type. | ||||||
|    * |    * | ||||||
|    * @param *sensor The binary sensor. |    * @param *sensor The binary sensor. | ||||||
|    * @param value  The value this binary_sensor represents |    * @param value  The value this binary_sensor represents | ||||||
|    */ |    */ | ||||||
|   void add_channel(binary_sensor::BinarySensor *sensor, float value); |   void add_channel(binary_sensor::BinarySensor *sensor, float value); | ||||||
|   void set_sensor_type(BinarySensorMapType sensor_type); |  | ||||||
|  |   /** | ||||||
|  |    * Add binary_sensors to the group when two parameters are needed for the Bayesian mapping type. | ||||||
|  |    * | ||||||
|  |    * @param *sensor The binary sensor. | ||||||
|  |    * @param prob_given_true Probability this observation is on when the Bayesian event is true | ||||||
|  |    * @param prob_given_false Probability this observation is on when the Bayesian event is false | ||||||
|  |    */ | ||||||
|  |   void add_channel(binary_sensor::BinarySensor *sensor, float prob_given_true, float prob_given_false); | ||||||
|  |  | ||||||
|  |   void set_sensor_type(BinarySensorMapType sensor_type) { this->sensor_type_ = sensor_type; } | ||||||
|  |  | ||||||
|  |   void set_bayesian_prior(float prior) { this->bayesian_prior_ = prior; }; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   std::vector<BinarySensorMapChannel> channels_{}; |   std::vector<BinarySensorMapChannel> channels_{}; | ||||||
|   BinarySensorMapType sensor_type_{BINARY_SENSOR_MAP_TYPE_GROUP}; |   BinarySensorMapType sensor_type_{BINARY_SENSOR_MAP_TYPE_GROUP}; | ||||||
|   // this gives max 64 channels per binary_sensor_map |  | ||||||
|  |   // this allows a max of 64 channels/observations in order to keep track of binary_sensor states | ||||||
|   uint64_t last_mask_{0x00}; |   uint64_t last_mask_{0x00}; | ||||||
|  |  | ||||||
|  |   // Bayesian event prior probability before taking into account any observations | ||||||
|  |   float bayesian_prior_{}; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * methods to process the types of binary_sensor_maps |    * Methods to process the binary_sensor_maps types | ||||||
|    * GROUP: process_group_() just map to a value |    * | ||||||
|  |    * GROUP: process_group_() averages all the values | ||||||
|    * ADD: process_add_() adds all the values |    * ADD: process_add_() adds all the values | ||||||
|  |    * BAYESIAN: process_bayesian_() computes the predicate probability | ||||||
|    * */ |    * */ | ||||||
|   void process_group_(); |   void process_group_(); | ||||||
|   void process_sum_(); |   void process_sum_(); | ||||||
|  |   void process_bayesian_(); | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Computes the Bayesian predicate for a specific observation | ||||||
|  |    * If the sensor state is false, then we use the parameters' probabilities for the observatiosn complement | ||||||
|  |    * | ||||||
|  |    * @param sensor_state  State of observation | ||||||
|  |    * @param prior Prior probability before accounting for this observation | ||||||
|  |    * @param prob_given_true Probability this observation is on when the Bayesian event is true | ||||||
|  |    * @param prob_given_false Probability this observation is on when the Bayesian event is false | ||||||
|  |    * */ | ||||||
|  |   float bayesian_predicate_(bool sensor_state, float prior, float prob_given_true, float prob_given_false); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace binary_sensor_map | }  // namespace binary_sensor_map | ||||||
|   | |||||||
| @@ -20,16 +20,29 @@ BinarySensorMap = binary_sensor_map_ns.class_( | |||||||
| ) | ) | ||||||
| SensorMapType = binary_sensor_map_ns.enum("SensorMapType") | SensorMapType = binary_sensor_map_ns.enum("SensorMapType") | ||||||
|  |  | ||||||
|  | CONF_BAYESIAN = "bayesian" | ||||||
|  | CONF_PRIOR = "prior" | ||||||
|  | CONF_PROB_GIVEN_TRUE = "prob_given_true" | ||||||
|  | CONF_PROB_GIVEN_FALSE = "prob_given_false" | ||||||
|  | CONF_OBSERVATIONS = "observations" | ||||||
|  |  | ||||||
| SENSOR_MAP_TYPES = { | SENSOR_MAP_TYPES = { | ||||||
|     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, |     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, | ||||||
|     CONF_SUM: SensorMapType.BINARY_SENSOR_MAP_TYPE_SUM, |     CONF_SUM: SensorMapType.BINARY_SENSOR_MAP_TYPE_SUM, | ||||||
|  |     CONF_BAYESIAN: SensorMapType.BINARY_SENSOR_MAP_TYPE_BAYESIAN, | ||||||
| } | } | ||||||
|  |  | ||||||
| entry = { | entry_one_parameter = { | ||||||
|     cv.Required(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor), |     cv.Required(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||||
|     cv.Required(CONF_VALUE): cv.float_, |     cv.Required(CONF_VALUE): cv.float_, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | entry_bayesian_parameters = { | ||||||
|  |     cv.Required(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||||
|  |     cv.Required(CONF_PROB_GIVEN_TRUE): cv.float_range(min=0, max=1), | ||||||
|  |     cv.Required(CONF_PROB_GIVEN_FALSE): cv.float_range(min=0, max=1), | ||||||
|  | } | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.typed_schema( | CONFIG_SCHEMA = cv.typed_schema( | ||||||
|     { |     { | ||||||
|         CONF_GROUP: sensor.sensor_schema( |         CONF_GROUP: sensor.sensor_schema( | ||||||
| @@ -39,7 +52,7 @@ CONFIG_SCHEMA = cv.typed_schema( | |||||||
|         ).extend( |         ).extend( | ||||||
|             { |             { | ||||||
|                 cv.Required(CONF_CHANNELS): cv.All( |                 cv.Required(CONF_CHANNELS): cv.All( | ||||||
|                     cv.ensure_list(entry), cv.Length(min=1, max=64) |                     cv.ensure_list(entry_one_parameter), cv.Length(min=1, max=64) | ||||||
|                 ), |                 ), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
| @@ -50,7 +63,18 @@ CONFIG_SCHEMA = cv.typed_schema( | |||||||
|         ).extend( |         ).extend( | ||||||
|             { |             { | ||||||
|                 cv.Required(CONF_CHANNELS): cv.All( |                 cv.Required(CONF_CHANNELS): cv.All( | ||||||
|                     cv.ensure_list(entry), cv.Length(min=1, max=64) |                     cv.ensure_list(entry_one_parameter), cv.Length(min=1, max=64) | ||||||
|  |                 ), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         CONF_BAYESIAN: sensor.sensor_schema( | ||||||
|  |             BinarySensorMap, | ||||||
|  |             accuracy_decimals=2, | ||||||
|  |         ).extend( | ||||||
|  |             { | ||||||
|  |                 cv.Required(CONF_PRIOR): cv.float_range(min=0, max=1), | ||||||
|  |                 cv.Required(CONF_OBSERVATIONS): cv.All( | ||||||
|  |                     cv.ensure_list(entry_bayesian_parameters), cv.Length(min=1, max=64) | ||||||
|                 ), |                 ), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
| @@ -66,6 +90,17 @@ async def to_code(config): | |||||||
|     constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] |     constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] | ||||||
|     cg.add(var.set_sensor_type(constant)) |     cg.add(var.set_sensor_type(constant)) | ||||||
|  |  | ||||||
|     for ch in config[CONF_CHANNELS]: |     if config[CONF_TYPE] == CONF_BAYESIAN: | ||||||
|         input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) |         cg.add(var.set_bayesian_prior(config[CONF_PRIOR])) | ||||||
|         cg.add(var.add_channel(input_var, ch[CONF_VALUE])) |  | ||||||
|  |         for obs in config[CONF_OBSERVATIONS]: | ||||||
|  |             input_var = await cg.get_variable(obs[CONF_BINARY_SENSOR]) | ||||||
|  |             cg.add( | ||||||
|  |                 var.add_channel( | ||||||
|  |                     input_var, obs[CONF_PROB_GIVEN_TRUE], obs[CONF_PROB_GIVEN_FALSE] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |     else: | ||||||
|  |         for ch in config[CONF_CHANNELS]: | ||||||
|  |             input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) | ||||||
|  |             cg.add(var.add_channel(input_var, ch[CONF_VALUE])) | ||||||
|   | |||||||
| @@ -368,6 +368,32 @@ sensor: | |||||||
|       - binary_sensor: bin3 |       - binary_sensor: bin3 | ||||||
|         value: 100.0 |         value: 100.0 | ||||||
|  |  | ||||||
|  |   - platform: binary_sensor_map | ||||||
|  |     name: Binary Sensor Map | ||||||
|  |     type: sum | ||||||
|  |     channels: | ||||||
|  |       - binary_sensor: bin1 | ||||||
|  |         value: 10.0 | ||||||
|  |       - binary_sensor: bin2 | ||||||
|  |         value: 15.0 | ||||||
|  |       - binary_sensor: bin3 | ||||||
|  |         value: 100.0 | ||||||
|  |  | ||||||
|  |   - platform: binary_sensor_map | ||||||
|  |     name: Binary Sensor Map | ||||||
|  |     type: bayesian | ||||||
|  |     prior: 0.4 | ||||||
|  |     observations: | ||||||
|  |       - binary_sensor: bin1 | ||||||
|  |         prob_given_true: 0.9 | ||||||
|  |         prob_given_false: 0.4 | ||||||
|  |       - binary_sensor: bin2 | ||||||
|  |         prob_given_true: 0.7 | ||||||
|  |         prob_given_false: 0.05 | ||||||
|  |       - binary_sensor: bin3 | ||||||
|  |         prob_given_true: 0.8 | ||||||
|  |         prob_given_false: 0.2 | ||||||
|  |  | ||||||
|   - platform: bl0939 |   - platform: bl0939 | ||||||
|     uart_id: uart_8 |     uart_id: uart_8 | ||||||
|     voltage: |     voltage: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user