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:
kahrendt 2023-04-12 21:48:29 -04:00 committed by GitHub
parent cc1eb648f9
commit afc848bf22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 190 additions and 34 deletions

View file

@ -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

View file

@ -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

View file

@ -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]))

View file

@ -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: