mirror of
https://github.com/esphome/esphome.git
synced 2024-11-28 17:54:13 +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:
|
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:
|
||||||
|
|
Loading…
Reference in a new issue