From d75daa96447123d7ee8e5c9ae72094f38c23d006 Mon Sep 17 00:00:00 2001
From: X-Ryl669 <boite.pour.spam@gmail.com>
Date: Wed, 12 Apr 2023 23:16:44 +0200
Subject: [PATCH 1/9] Add always trigger stop (#4249)

* feat: Add manual control config override 1/3

* feat: Add manual control config override 2/3

* feat: Add manual control config override 3/3

* No magical number is better

Co-authored-by: Guillermo Ruffino <glm.net@gmail.com>

---------

Co-authored-by: Guillermo Ruffino <glm.net@gmail.com>
---
 esphome/components/time_based/cover.py             | 3 +++
 esphome/components/time_based/time_based_cover.cpp | 8 ++++++++
 esphome/components/time_based/time_based_cover.h   | 2 ++
 3 files changed, 13 insertions(+)

diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py
index 9625781c96..a14a08ccad 100644
--- a/esphome/components/time_based/cover.py
+++ b/esphome/components/time_based/cover.py
@@ -16,6 +16,7 @@ time_based_ns = cg.esphome_ns.namespace("time_based")
 TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component)
 
 CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop"
+CONF_MANUAL_CONTROL = "manual_control"
 
 CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
     {
@@ -26,6 +27,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
         cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
         cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
         cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
+        cv.Optional(CONF_MANUAL_CONTROL, default=False): cv.boolean,
         cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean,
     }
 ).extend(cv.COMPONENT_SCHEMA)
@@ -51,4 +53,5 @@ async def to_code(config):
     )
 
     cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
+    cg.add(var.set_manual_control(config[CONF_MANUAL_CONTROL]))
     cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp
index 522252e907..a7ba6d0595 100644
--- a/esphome/components/time_based/time_based_cover.cpp
+++ b/esphome/components/time_based/time_based_cover.cpp
@@ -79,6 +79,14 @@ void TimeBasedCover::control(const CoverCall &call) {
     auto pos = *call.get_position();
     if (pos == this->position) {
       // already at target
+      if (this->manual_control_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) {
+        // for covers with manual control switch, we can't rely on the computed position, so if
+        // the command triggered again, we'll assume it's in the opposite direction anyway.
+        auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
+        this->position = pos == COVER_CLOSED ? COVER_OPEN : COVER_CLOSED;
+        this->target_position_ = pos;
+        this->start_direction_(op);
+      }
       // for covers with built in end stop, we should send the command again
       if (this->has_built_in_endstop_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) {
         auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h
index 517ab77cb3..b7a826d237 100644
--- a/esphome/components/time_based/time_based_cover.h
+++ b/esphome/components/time_based/time_based_cover.h
@@ -21,6 +21,7 @@ class TimeBasedCover : public cover::Cover, public Component {
   void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; }
   cover::CoverTraits get_traits() override;
   void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
+  void set_manual_control(bool value) { this->manual_control_ = value; }
   void set_assumed_state(bool value) { this->assumed_state_ = value; }
 
  protected:
@@ -44,6 +45,7 @@ class TimeBasedCover : public cover::Cover, public Component {
   uint32_t last_publish_time_{0};
   float target_position_{0};
   bool has_built_in_endstop_{false};
+  bool manual_control_{false};
   bool assumed_state_{false};
   cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
 };

From 443c3c2a5671e17018654f9fa7671fe7959b444c Mon Sep 17 00:00:00 2001
From: unhold <c.unhold@gmail.com>
Date: Wed, 12 Apr 2023 23:18:09 +0200
Subject: [PATCH 2/9] Fix graph limits for negative values and other corner
 cases (#4253)

Fix lower graph limit for negative values by rounding down instead of truncating.
Consistently handle the corner cases: empty trace, min=max (e.g. single value)
by drawing the grid lines for a single grid division.
---
 esphome/components/graph/graph.cpp | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp
index daff89e0a6..c229f17dd8 100644
--- a/esphome/components/graph/graph.cpp
+++ b/esphome/components/graph/graph.cpp
@@ -122,11 +122,18 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo
   }
 
   // Adjust limits to nice y_per_div boundaries
-  int yn = int(ymin / y_per_div);
-  int ym = int(ymax / y_per_div) + int(1 * (fmodf(ymax, y_per_div) != 0));
-  ymin = yn * y_per_div;
-  ymax = ym * y_per_div;
-  yrange = ymax - ymin;
+  int yn = 0;
+  int ym = 1;
+  if (!std::isnan(ymin) && !std::isnan(ymax)) {
+    yn = (int) floorf(ymin / y_per_div);
+    ym = (int) ceilf(ymax / y_per_div);
+    if (yn == ym) {
+      ym++;
+    }
+    ymin = yn * y_per_div;
+    ymax = ym * y_per_div;
+    yrange = ymax - ymin;
+  }
 
   /// Draw grid
   if (!std::isnan(this->gridspacing_y_)) {

From b5fbe0b1451ac6f709c2aced14918977d284f584 Mon Sep 17 00:00:00 2001
From: unhold <c.unhold@gmail.com>
Date: Wed, 12 Apr 2023 23:19:52 +0200
Subject: [PATCH 3/9] Fix cut-off on 2.13" waveshare/ttgo epaper displays
 (#4255)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The controller of the 2.13" waveshare/ttgo epaper displays operates with a 128x250 buffer,
while only 122x250 pixels are shown. While this can be ignored for rotations 0/270°,
with roations 90/180°, 6 pixels are cut-off from the top or left edge.
This change introduces a distinction between object width and controller width,
resulting in pixel-perfect rotations.
---
 .../waveshare_epaper/waveshare_epaper.cpp     | 22 ++++++++++++++++---
 .../waveshare_epaper/waveshare_epaper.h       |  4 ++++
 2 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 8fd5c2e1f3..8c4b137514 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -137,7 +137,7 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color
   if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
     return;
 
-  const uint32_t pos = (x + y * this->get_width_internal()) / 8u;
+  const uint32_t pos = (x + y * this->get_width_controller()) / 8u;
   const uint8_t subpos = x & 0x07;
   // flip logic
   if (!color.is_on()) {
@@ -146,7 +146,9 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color
     this->buffer_[pos] &= ~(0x80 >> subpos);
   }
 }
-uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal() / 8u; }
+uint32_t WaveshareEPaper::get_buffer_length_() {
+  return this->get_width_controller() * this->get_height_internal() / 8u;
+}
 void WaveshareEPaper::start_command_() {
   this->dc_pin_->digital_write(false);
   this->enable();
@@ -291,7 +293,7 @@ void HOT WaveshareEPaperTypeA::display() {
       // COMMAND SET RAM X ADDRESS START END POSITION
       this->command(0x44);
       this->data(0x00);
-      this->data((this->get_width_internal() - 1) >> 3);
+      this->data((this->get_width_controller() - 1) >> 3);
       // COMMAND SET RAM Y ADDRESS START END POSITION
       this->command(0x45);
       this->data(this->get_height_internal() - 1);
@@ -392,12 +394,26 @@ int WaveshareEPaperTypeA::get_width_internal() {
     case TTGO_EPAPER_2_13_IN_B73:
     case TTGO_EPAPER_2_13_IN_B74:
     case TTGO_EPAPER_2_13_IN_B1:
+      return 122;
     case WAVESHARE_EPAPER_2_9_IN:
     case WAVESHARE_EPAPER_2_9_IN_V2:
       return 128;
   }
   return 0;
 }
+// The controller of the 2.13" displays has a buffer larger than screen size
+int WaveshareEPaperTypeA::get_width_controller() {
+  switch (this->model_) {
+    case WAVESHARE_EPAPER_2_13_IN:
+    case TTGO_EPAPER_2_13_IN:
+    case TTGO_EPAPER_2_13_IN_B73:
+    case TTGO_EPAPER_2_13_IN_B74:
+    case TTGO_EPAPER_2_13_IN_B1:
+      return 128;
+    default:
+      return this->get_width_internal();
+  }
+}
 int WaveshareEPaperTypeA::get_height_internal() {
   switch (this->model_) {
     case WAVESHARE_EPAPER_1_54_IN:
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index 848b293c45..a674d3af0c 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -54,6 +54,8 @@ class WaveshareEPaper : public PollingComponent,
     }
   }
 
+  virtual int get_width_controller() { return this->get_width_internal(); };
+
   uint32_t get_buffer_length_();
   uint32_t reset_duration_{200};
 
@@ -111,6 +113,8 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
 
   int get_height_internal() override;
 
+  int get_width_controller() override;
+
   uint32_t full_update_every_{30};
   uint32_t at_update_{0};
   WaveshareEPaperTypeAModel model_;

From 0547f2a9316fd80712515993304a1b5049f56ea7 Mon Sep 17 00:00:00 2001
From: Fabian <Fabian-Schmidt@users.noreply.github.com>
Date: Wed, 12 Apr 2023 23:22:08 +0200
Subject: [PATCH 4/9] Add KSZ8081 support. (#4668)

Co-authored-by: Your Name <you@example.com>
---
 esphome/components/ethernet/__init__.py         |  1 +
 .../components/ethernet/ethernet_component.cpp  | 17 +++++++++++++++--
 .../components/ethernet/ethernet_component.h    |  6 ++++--
 3 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index f6ca376681..bedc0a4c30 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -34,6 +34,7 @@ ETHERNET_TYPES = {
     "DP83848": EthernetType.ETHERNET_TYPE_DP83848,
     "IP101": EthernetType.ETHERNET_TYPE_IP101,
     "JL1101": EthernetType.ETHERNET_TYPE_JL1101,
+    "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
 }
 
 emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp
index 4792728a71..8eb4718f49 100644
--- a/esphome/components/ethernet/ethernet_component.cpp
+++ b/esphome/components/ethernet/ethernet_component.cpp
@@ -74,6 +74,10 @@ void EthernetComponent::setup() {
       phy = esp_eth_phy_new_jl1101(&phy_config);
       break;
     }
+    case ETHERNET_TYPE_KSZ8081: {
+      phy = esp_eth_phy_new_ksz8081(&phy_config);
+      break;
+    }
     default: {
       this->mark_failed();
       return;
@@ -140,7 +144,7 @@ void EthernetComponent::loop() {
 }
 
 void EthernetComponent::dump_config() {
-  std::string eth_type;
+  const char *eth_type;
   switch (this->type_) {
     case ETHERNET_TYPE_LAN8720:
       eth_type = "LAN8720";
@@ -158,6 +162,14 @@ void EthernetComponent::dump_config() {
       eth_type = "IP101";
       break;
 
+    case ETHERNET_TYPE_JL1101:
+      eth_type = "JL1101";
+      break;
+
+    case ETHERNET_TYPE_KSZ8081:
+      eth_type = "KSZ8081";
+      break;
+
     default:
       eth_type = "Unknown";
       break;
@@ -170,7 +182,8 @@ void EthernetComponent::dump_config() {
   }
   ESP_LOGCONFIG(TAG, "  MDC Pin: %u", this->mdc_pin_);
   ESP_LOGCONFIG(TAG, "  MDIO Pin: %u", this->mdio_pin_);
-  ESP_LOGCONFIG(TAG, "  Type: %s", eth_type.c_str());
+  ESP_LOGCONFIG(TAG, "  Type: %s", eth_type);
+  ESP_LOGCONFIG(TAG, "  PHY addr: %u", this->phy_addr_);
 }
 
 float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h
index 872ed17044..0d9ebf29a8 100644
--- a/esphome/components/ethernet/ethernet_component.h
+++ b/esphome/components/ethernet/ethernet_component.h
@@ -14,11 +14,13 @@ namespace esphome {
 namespace ethernet {
 
 enum EthernetType {
-  ETHERNET_TYPE_LAN8720 = 0,
+  ETHERNET_TYPE_UNKNOWN = 0,
+  ETHERNET_TYPE_LAN8720,
   ETHERNET_TYPE_RTL8201,
   ETHERNET_TYPE_DP83848,
   ETHERNET_TYPE_IP101,
   ETHERNET_TYPE_JL1101,
+  ETHERNET_TYPE_KSZ8081,
 };
 
 struct ManualIP {
@@ -69,7 +71,7 @@ class EthernetComponent : public Component {
   int power_pin_{-1};
   uint8_t mdc_pin_{23};
   uint8_t mdio_pin_{18};
-  EthernetType type_{ETHERNET_TYPE_LAN8720};
+  EthernetType type_{ETHERNET_TYPE_UNKNOWN};
   emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN};
   emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO};
   optional<ManualIP> manual_ip_{};

From 4eb69d6af5a67002ca2c1d5a79148d95ca8bd287 Mon Sep 17 00:00:00 2001
From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com>
Date: Wed, 12 Apr 2023 23:28:02 +0200
Subject: [PATCH 5/9] Fix restore (#4655)

* ALWAYS_OFF for fan

* ALWAYS_OFF for light

* ALWAYS_OFF for switch
---
 esphome/components/fan/__init__.py    | 2 +-
 esphome/components/light/__init__.py  | 2 +-
 esphome/components/switch/__init__.py | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py
index eb67bbcbd7..9a05bff3a0 100644
--- a/esphome/components/fan/__init__.py
+++ b/esphome/components/fan/__init__.py
@@ -63,7 +63,7 @@ FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.temp
 FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
     {
         cv.GenerateID(): cv.declare_id(Fan),
-        cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(
+        cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
             RESTORE_MODES, upper=True, space="_"
         ),
         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent),
diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py
index c397910ec4..ba3a26ebe5 100644
--- a/esphome/components/light/__init__.py
+++ b/esphome/components/light/__init__.py
@@ -60,7 +60,7 @@ LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex
     {
         cv.GenerateID(): cv.declare_id(LightState),
         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTJSONLightComponent),
-        cv.Optional(CONF_RESTORE_MODE, default="restore_default_off"): cv.enum(
+        cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
             RESTORE_MODES, upper=True, space="_"
         ),
         cv.Optional(CONF_ON_TURN_ON): auto.validate_automation(
diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py
index c9bf81982a..21cbe3dfe4 100644
--- a/esphome/components/switch/__init__.py
+++ b/esphome/components/switch/__init__.py
@@ -92,7 +92,7 @@ def switch_schema(
     device_class: str = _UNDEF,
     icon: str = _UNDEF,
     block_inverted: bool = False,
-    default_restore_mode: str = "RESTORE_DEFAULT_OFF",
+    default_restore_mode: str = "ALWAYS_OFF",
 ):
     schema = _SWITCH_SCHEMA.extend(
         {

From 04a139fe3d32f4c89fe856ed0f71feae9cdbf3f0 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 13 Apr 2023 10:13:53 +1200
Subject: [PATCH 6/9] Bump version to 2023.5.0-dev

---
 esphome/const.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/const.py b/esphome/const.py
index 534a8ade01..2f66b47b8e 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2023.4.0-dev"
+__version__ = "2023.5.0-dev"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 

From cc1eb648f9734402018baa8f6b9acdda6463cdd1 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 13 Apr 2023 10:46:19 +1200
Subject: [PATCH 7/9] Only allow 5 jobs from each CI run to be in parallel
 (#4682)

---
 .github/workflows/ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d22c2b7e03..affdf944a7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,6 +23,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
+      max-parallel: 5
       matrix:
         include:
           - id: ci-custom

From afc848bf22f93a650ea1641179de205b0ac5f3cb Mon Sep 17 00:00:00 2001
From: kahrendt <kahrendt@gmail.com>
Date: Wed, 12 Apr 2023 21:48:29 -0400
Subject: [PATCH 8/9] 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>
---
 .../binary_sensor_map/binary_sensor_map.cpp   | 82 ++++++++++++++++---
 .../binary_sensor_map/binary_sensor_map.h     | 69 ++++++++++++----
 .../components/binary_sensor_map/sensor.py    | 47 +++++++++--
 tests/test3.yaml                              | 26 ++++++
 4 files changed, 190 insertions(+), 34 deletions(-)

diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.cpp b/esphome/components/binary_sensor_map/binary_sensor_map.cpp
index 3934e0a99c..0bf6202893 100644
--- a/esphome/components/binary_sensor_map/binary_sensor_map.cpp
+++ b/esphome/components/binary_sensor_map/binary_sensor_map.cpp
@@ -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
diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.h b/esphome/components/binary_sensor_map/binary_sensor_map.h
index a1d6f95009..a07154c0e8 100644
--- a/esphome/components/binary_sensor_map/binary_sensor_map.h
+++ b/esphome/components/binary_sensor_map/binary_sensor_map.h
@@ -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
diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py
index 573cce9223..1181905f30 100644
--- a/esphome/components/binary_sensor_map/sensor.py
+++ b/esphome/components/binary_sensor_map/sensor.py
@@ -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]))
diff --git a/tests/test3.yaml b/tests/test3.yaml
index ceb9047d17..c4847725e8 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -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:

From 0643b719086f70689eb5f6de572f9d1ba6f3c54a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 13 Apr 2023 17:31:47 +1200
Subject: [PATCH 9/9] Bump aioesphomeapi from 13.5.1 to 13.7.0 (#4676)

Bumps [aioesphomeapi](https://github.com/esphome/aioesphomeapi) from 13.5.1 to 13.7.0.
- [Release notes](https://github.com/esphome/aioesphomeapi/releases)
- [Commits](https://github.com/esphome/aioesphomeapi/compare/v13.5.1...v13.7.0)

---
updated-dependencies:
- dependency-name: aioesphomeapi
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 5a57342189..5f73cf3a06 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ platformio==6.1.6  # When updating platformio, also update Dockerfile
 esptool==4.5.1
 click==8.1.3
 esphome-dashboard==20230214.0
-aioesphomeapi==13.5.1
+aioesphomeapi==13.7.0
 zeroconf==0.56.0
 
 # esp-idf requires this, but doesn't bundle it by default