mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01: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:
parent
cc1eb648f9
commit
afc848bf22
4 changed files with 190 additions and 34 deletions
|
@ -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,
|
||||
.sensor_value = value,
|
||||
.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;
|
||||
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 {
|
||||
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))
|
||||
|
||||
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]))
|
||||
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:
|
||||
|
|
Loading…
Reference in a new issue