mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +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: | ||||
|       this->process_sum_(); | ||||
|       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; | ||||
|   uint8_t num_active_sensors = 0; | ||||
|   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++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|     if (bs.binary_sensor->state) { | ||||
|       num_active_sensors++; | ||||
|       total_current_value += bs.sensor_value; | ||||
|       total_current_value += bs.parameters.sensor_value; | ||||
|       mask |= 1ULL << i; | ||||
|     } | ||||
|   } | ||||
|   // check if the sensor map was touched | ||||
|  | ||||
|   // potentially update state only if a binary_sensor is active | ||||
|   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) { | ||||
|       float publish_value = total_current_value / num_active_sensors; | ||||
|       this->publish_state(publish_value); | ||||
|     } | ||||
|   } 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()); | ||||
|     this->publish_state(NAN); | ||||
|   } | ||||
|  | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::process_sum_() { | ||||
|   float total_current_value = 0.0; | ||||
|   uint64_t mask = 0x00; | ||||
|  | ||||
|   // - check all binary_sensor states | ||||
|   // - 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++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|     if (bs.binary_sensor->state) { | ||||
|       total_current_value += bs.sensor_value; | ||||
|       total_current_value += bs.parameters.sensor_value; | ||||
|       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())) { | ||||
|     this->publish_state(total_current_value); | ||||
|   } | ||||
| @@ -70,15 +78,65 @@ void BinarySensorMap::process_sum_() { | ||||
|   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) { | ||||
|   BinarySensorMapChannel sensor_channel{ | ||||
|       .binary_sensor = sensor, | ||||
|       .parameters{ | ||||
|           .sensor_value = value, | ||||
|       }, | ||||
|   }; | ||||
|   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 esphome | ||||
|   | ||||
| @@ -12,51 +12,88 @@ namespace binary_sensor_map { | ||||
| enum BinarySensorMapType { | ||||
|   BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|   BINARY_SENSOR_MAP_TYPE_SUM, | ||||
|   BINARY_SENSOR_MAP_TYPE_BAYESIAN, | ||||
| }; | ||||
|  | ||||
| struct BinarySensorMapChannel { | ||||
|   binary_sensor::BinarySensor *binary_sensor; | ||||
|   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 { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   /** | ||||
|    * The loop checks all binary_sensor states | ||||
|    * When the binary_sensor reports a true value for its state, then the float value it represents is added to the | ||||
|    * total_current_value | ||||
|    * The loop calls the configured type processing method | ||||
|    * | ||||
|    * Only when the total_current_value changed and at least one sensor reports an active state we publish the sensors | ||||
|    * average value. When the value changed and no sensors ar active we publish NAN. | ||||
|    * */ | ||||
|    * The processing method loops through all sensors and calculates the numerical result | ||||
|    * The result is only published if a binary sensor state has changed or, for some types, on initial boot | ||||
|    */ | ||||
|   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 value  The value this binary_sensor represents | ||||
|    */ | ||||
|   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: | ||||
|   std::vector<BinarySensorMapChannel> channels_{}; | ||||
|   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}; | ||||
|  | ||||
|   // Bayesian event prior probability before taking into account any observations | ||||
|   float bayesian_prior_{}; | ||||
|  | ||||
|   /** | ||||
|    * methods to process the types of binary_sensor_maps | ||||
|    * GROUP: process_group_() just map to a value | ||||
|    * Methods to process the binary_sensor_maps types | ||||
|    * | ||||
|    * GROUP: process_group_() averages all the values | ||||
|    * ADD: process_add_() adds all the values | ||||
|    * BAYESIAN: process_bayesian_() computes the predicate probability | ||||
|    * */ | ||||
|   void process_group_(); | ||||
|   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 | ||||
|   | ||||
| @@ -20,16 +20,29 @@ BinarySensorMap = binary_sensor_map_ns.class_( | ||||
| ) | ||||
| 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 = { | ||||
|     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|     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_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( | ||||
|     { | ||||
|         CONF_GROUP: sensor.sensor_schema( | ||||
| @@ -39,7 +52,7 @@ CONFIG_SCHEMA = cv.typed_schema( | ||||
|         ).extend( | ||||
|             { | ||||
|                 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( | ||||
|             { | ||||
|                 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]] | ||||
|     cg.add(var.set_sensor_type(constant)) | ||||
|  | ||||
|     if config[CONF_TYPE] == CONF_BAYESIAN: | ||||
|         cg.add(var.set_bayesian_prior(config[CONF_PRIOR])) | ||||
|  | ||||
|         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 | ||||
|         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 | ||||
|     uart_id: uart_8 | ||||
|     voltage: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user