From 98cb6555df956bf73f456eaeb74e5fa0399554e4 Mon Sep 17 00:00:00 2001
From: Anton Viktorov <anton.viktorov@live.com>
Date: Thu, 16 May 2024 05:22:40 +0200
Subject: [PATCH] SPI and I2C for ENS160 (#6369)

---
 CODEOWNERS                                    |  5 +-
 esphome/components/ens160/__init__.py         |  1 -
 esphome/components/ens160/sensor.py           | 90 ++-----------------
 esphome/components/ens160_base/__init__.py    | 78 ++++++++++++++++
 .../ens160_base.cpp}                          |  7 +-
 .../ens160.h => ens160_base/ens160_base.h}    | 12 ++-
 esphome/components/ens160_i2c/__init__.py     |  0
 esphome/components/ens160_i2c/ens160_i2c.cpp  | 32 +++++++
 esphome/components/ens160_i2c/ens160_i2c.h    | 19 ++++
 esphome/components/ens160_i2c/sensor.py       | 22 +++++
 esphome/components/ens160_spi/__init__.py     |  0
 esphome/components/ens160_spi/ens160_spi.cpp  | 59 ++++++++++++
 esphome/components/ens160_spi/ens160_spi.h    | 22 +++++
 esphome/components/ens160_spi/sensor.py       | 22 +++++
 .../components/ens160/test.esp32-c3-idf.yaml  | 13 ---
 tests/components/ens160/test.esp32-c3.yaml    | 13 ---
 tests/components/ens160/test.esp32-idf.yaml   | 13 ---
 tests/components/ens160/test.esp32.yaml       | 13 ---
 tests/components/ens160/test.esp8266.yaml     | 13 ---
 tests/components/ens160/test.rp2040.yaml      | 13 ---
 tests/components/ens160_i2c/common.yaml       | 15 ++++
 .../ens160_i2c/test.esp32-c3-idf.yaml         |  5 ++
 .../components/ens160_i2c/test.esp32-c3.yaml  |  5 ++
 .../components/ens160_i2c/test.esp32-idf.yaml |  5 ++
 tests/components/ens160_i2c/test.esp32.yaml   |  5 ++
 tests/components/ens160_i2c/test.esp8266.yaml |  5 ++
 tests/components/ens160_i2c/test.rp2040.yaml  |  5 ++
 tests/components/ens160_spi/common.yaml       | 17 ++++
 .../ens160_spi/test.esp32-c3-idf.yaml         |  7 ++
 .../components/ens160_spi/test.esp32-c3.yaml  |  7 ++
 .../components/ens160_spi/test.esp32-idf.yaml |  7 ++
 tests/components/ens160_spi/test.esp32.yaml   |  7 ++
 tests/components/ens160_spi/test.esp8266.yaml |  7 ++
 tests/components/ens160_spi/test.rp2040.yaml  |  7 ++
 tests/test3.1.yaml                            |  7 --
 35 files changed, 378 insertions(+), 180 deletions(-)
 create mode 100644 esphome/components/ens160_base/__init__.py
 rename esphome/components/{ens160/ens160.cpp => ens160_base/ens160_base.cpp} (99%)
 rename esphome/components/{ens160/ens160.h => ens160_base/ens160_base.h} (76%)
 create mode 100644 esphome/components/ens160_i2c/__init__.py
 create mode 100644 esphome/components/ens160_i2c/ens160_i2c.cpp
 create mode 100644 esphome/components/ens160_i2c/ens160_i2c.h
 create mode 100644 esphome/components/ens160_i2c/sensor.py
 create mode 100644 esphome/components/ens160_spi/__init__.py
 create mode 100644 esphome/components/ens160_spi/ens160_spi.cpp
 create mode 100644 esphome/components/ens160_spi/ens160_spi.h
 create mode 100644 esphome/components/ens160_spi/sensor.py
 delete mode 100644 tests/components/ens160/test.esp32-c3-idf.yaml
 delete mode 100644 tests/components/ens160/test.esp32-c3.yaml
 delete mode 100644 tests/components/ens160/test.esp32-idf.yaml
 delete mode 100644 tests/components/ens160/test.esp32.yaml
 delete mode 100644 tests/components/ens160/test.esp8266.yaml
 delete mode 100644 tests/components/ens160/test.rp2040.yaml
 create mode 100644 tests/components/ens160_i2c/common.yaml
 create mode 100644 tests/components/ens160_i2c/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/ens160_i2c/test.esp32-c3.yaml
 create mode 100644 tests/components/ens160_i2c/test.esp32-idf.yaml
 create mode 100644 tests/components/ens160_i2c/test.esp32.yaml
 create mode 100644 tests/components/ens160_i2c/test.esp8266.yaml
 create mode 100644 tests/components/ens160_i2c/test.rp2040.yaml
 create mode 100644 tests/components/ens160_spi/common.yaml
 create mode 100644 tests/components/ens160_spi/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/ens160_spi/test.esp32-c3.yaml
 create mode 100644 tests/components/ens160_spi/test.esp32-idf.yaml
 create mode 100644 tests/components/ens160_spi/test.esp32.yaml
 create mode 100644 tests/components/ens160_spi/test.esp8266.yaml
 create mode 100644 tests/components/ens160_spi/test.rp2040.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 65ea3b1683..4227cdb06d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -111,7 +111,10 @@ esphome/components/ee895/* @Stock-M
 esphome/components/ektf2232/touchscreen/* @jesserockz
 esphome/components/emc2101/* @ellull
 esphome/components/emmeti/* @E440QF
-esphome/components/ens160/* @vincentscode
+esphome/components/ens160/* @latonita
+esphome/components/ens160_base/* @latonita @vincentscode
+esphome/components/ens160_i2c/* @latonita
+esphome/components/ens160_spi/* @latonita
 esphome/components/ens210/* @itn3rd77
 esphome/components/esp32/* @esphome/core
 esphome/components/esp32_ble/* @Rapsssito @jesserockz
diff --git a/esphome/components/ens160/__init__.py b/esphome/components/ens160/__init__.py
index d26770a89d..e69de29bb2 100644
--- a/esphome/components/ens160/__init__.py
+++ b/esphome/components/ens160/__init__.py
@@ -1 +0,0 @@
-CODEOWNERS = ["@vincentscode"]
diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py
index 6572c4e397..f666b530b3 100644
--- a/esphome/components/ens160/sensor.py
+++ b/esphome/components/ens160/sensor.py
@@ -1,87 +1,7 @@
-import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import i2c, sensor
-from esphome.const import (
-    CONF_COMPENSATION,
-    CONF_ECO2,
-    CONF_HUMIDITY,
-    CONF_ID,
-    CONF_TEMPERATURE,
-    CONF_TVOC,
-    DEVICE_CLASS_AQI,
-    DEVICE_CLASS_CARBON_DIOXIDE,
-    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
-    ICON_CHEMICAL_WEAPON,
-    ICON_MOLECULE_CO2,
-    ICON_RADIATOR,
-    STATE_CLASS_MEASUREMENT,
-    UNIT_PARTS_PER_BILLION,
-    UNIT_PARTS_PER_MILLION,
+
+CODEOWNERS = ["@latonita"]
+
+CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
+    "The ens160 sensor component has been renamed to ens160_i2c."
 )
-
-CODEOWNERS = ["@vincentscode"]
-DEPENDENCIES = ["i2c"]
-
-ens160_ns = cg.esphome_ns.namespace("ens160")
-ENS160Component = ens160_ns.class_(
-    "ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
-)
-
-CONF_AQI = "aqi"
-UNIT_INDEX = "index"
-
-CONFIG_SCHEMA = (
-    cv.Schema(
-        {
-            cv.GenerateID(): cv.declare_id(ENS160Component),
-            cv.Required(CONF_ECO2): sensor.sensor_schema(
-                unit_of_measurement=UNIT_PARTS_PER_MILLION,
-                icon=ICON_MOLECULE_CO2,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-            cv.Required(CONF_TVOC): sensor.sensor_schema(
-                unit_of_measurement=UNIT_PARTS_PER_BILLION,
-                icon=ICON_RADIATOR,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-            cv.Required(CONF_AQI): sensor.sensor_schema(
-                icon=ICON_CHEMICAL_WEAPON,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_AQI,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-            cv.Optional(CONF_COMPENSATION): cv.Schema(
-                {
-                    cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
-                    cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
-                }
-            ),
-        }
-    )
-    .extend(cv.polling_component_schema("60s"))
-    .extend(i2c.i2c_device_schema(0x53))
-)
-
-
-async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-    await cg.register_component(var, config)
-    await i2c.register_i2c_device(var, config)
-
-    sens = await sensor.new_sensor(config[CONF_ECO2])
-    cg.add(var.set_co2(sens))
-    sens = await sensor.new_sensor(config[CONF_TVOC])
-    cg.add(var.set_tvoc(sens))
-    sens = await sensor.new_sensor(config[CONF_AQI])
-    cg.add(var.set_aqi(sens))
-
-    if CONF_COMPENSATION in config:
-        compensation_config = config[CONF_COMPENSATION]
-        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])
-        cg.add(var.set_temperature(sens))
-        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY])
-        cg.add(var.set_humidity(sens))
diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py
new file mode 100644
index 0000000000..eb6d0880af
--- /dev/null
+++ b/esphome/components/ens160_base/__init__.py
@@ -0,0 +1,78 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import (
+    CONF_COMPENSATION,
+    CONF_ECO2,
+    CONF_HUMIDITY,
+    CONF_ID,
+    CONF_TEMPERATURE,
+    CONF_TVOC,
+    DEVICE_CLASS_AQI,
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
+    ICON_CHEMICAL_WEAPON,
+    ICON_MOLECULE_CO2,
+    ICON_RADIATOR,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PARTS_PER_BILLION,
+    UNIT_PARTS_PER_MILLION,
+)
+
+CODEOWNERS = ["@vincentscode", "@latonita"]
+
+ens160_ns = cg.esphome_ns.namespace("ens160_base")
+
+CONF_AQI = "aqi"
+UNIT_INDEX = "index"
+
+CONFIG_SCHEMA_BASE = cv.Schema(
+    {
+        cv.Required(CONF_ECO2): sensor.sensor_schema(
+            unit_of_measurement=UNIT_PARTS_PER_MILLION,
+            icon=ICON_MOLECULE_CO2,
+            accuracy_decimals=0,
+            device_class=DEVICE_CLASS_CARBON_DIOXIDE,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        cv.Required(CONF_TVOC): sensor.sensor_schema(
+            unit_of_measurement=UNIT_PARTS_PER_BILLION,
+            icon=ICON_RADIATOR,
+            accuracy_decimals=0,
+            device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        cv.Required(CONF_AQI): sensor.sensor_schema(
+            icon=ICON_CHEMICAL_WEAPON,
+            accuracy_decimals=0,
+            device_class=DEVICE_CLASS_AQI,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        cv.Optional(CONF_COMPENSATION): cv.Schema(
+            {
+                cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
+                cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
+            }
+        ),
+    }
+).extend(cv.polling_component_schema("60s"))
+
+
+async def to_code_base(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    sens = await sensor.new_sensor(config[CONF_ECO2])
+    cg.add(var.set_co2(sens))
+    sens = await sensor.new_sensor(config[CONF_TVOC])
+    cg.add(var.set_tvoc(sens))
+    sens = await sensor.new_sensor(config[CONF_AQI])
+    cg.add(var.set_aqi(sens))
+
+    if compensation_config := config.get(CONF_COMPENSATION):
+        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
+
+    return var
diff --git a/esphome/components/ens160/ens160.cpp b/esphome/components/ens160_base/ens160_base.cpp
similarity index 99%
rename from esphome/components/ens160/ens160.cpp
rename to esphome/components/ens160_base/ens160_base.cpp
index c7a6ccbb73..71082c58c2 100644
--- a/esphome/components/ens160/ens160.cpp
+++ b/esphome/components/ens160_base/ens160_base.cpp
@@ -5,12 +5,12 @@
 // Implementation based on:
 //   https://github.com/sciosense/ENS160_driver
 
-#include "ens160.h"
+#include "ens160_base.h"
 #include "esphome/core/log.h"
 #include "esphome/core/hal.h"
 
 namespace esphome {
-namespace ens160 {
+namespace ens160_base {
 
 static const char *const TAG = "ens160";
 
@@ -303,7 +303,6 @@ void ENS160Component::dump_config() {
   ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_,
            this->firmware_ver_build_);
 
-  LOG_I2C_DEVICE(this);
   LOG_UPDATE_INTERVAL(this);
   LOG_SENSOR("  ", "CO2 Sensor:", this->co2_);
   LOG_SENSOR("  ", "TVOC Sensor:", this->tvoc_);
@@ -317,5 +316,5 @@ void ENS160Component::dump_config() {
   }
 }
 
-}  // namespace ens160
+}  // namespace ens160_base
 }  // namespace esphome
diff --git a/esphome/components/ens160/ens160.h b/esphome/components/ens160_base/ens160_base.h
similarity index 76%
rename from esphome/components/ens160/ens160.h
rename to esphome/components/ens160_base/ens160_base.h
index 88bc8e3501..729225a5ae 100644
--- a/esphome/components/ens160/ens160.h
+++ b/esphome/components/ens160_base/ens160_base.h
@@ -2,12 +2,11 @@
 
 #include "esphome/core/component.h"
 #include "esphome/components/sensor/sensor.h"
-#include "esphome/components/i2c/i2c.h"
 
 namespace esphome {
-namespace ens160 {
+namespace ens160_base {
 
-class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
+class ENS160Component : public PollingComponent, public sensor::Sensor {
  public:
   void set_co2(sensor::Sensor *co2) { co2_ = co2; }
   void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
@@ -44,6 +43,11 @@ class ENS160Component : public PollingComponent, public i2c::I2CDevice, public s
   bool warming_up_{false};
   bool initial_startup_{false};
 
+  virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
+  virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
+  virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
+  virtual bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
+
   uint8_t firmware_ver_major_{0};
   uint8_t firmware_ver_minor_{0};
   uint8_t firmware_ver_build_{0};
@@ -56,5 +60,5 @@ class ENS160Component : public PollingComponent, public i2c::I2CDevice, public s
   sensor::Sensor *temperature_{nullptr};
 };
 
-}  // namespace ens160
+}  // namespace ens160_base
 }  // namespace esphome
diff --git a/esphome/components/ens160_i2c/__init__.py b/esphome/components/ens160_i2c/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ens160_i2c/ens160_i2c.cpp b/esphome/components/ens160_i2c/ens160_i2c.cpp
new file mode 100644
index 0000000000..7163a5ad6e
--- /dev/null
+++ b/esphome/components/ens160_i2c/ens160_i2c.cpp
@@ -0,0 +1,32 @@
+#include <cstddef>
+#include <cstdint>
+
+#include "ens160_i2c.h"
+#include "esphome/components/i2c/i2c.h"
+#include "../ens160_base/ens160_base.h"
+
+namespace esphome {
+namespace ens160_i2c {
+
+static const char *const TAG = "ens160_i2c.sensor";
+
+bool ENS160I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
+  return I2CDevice::read_byte(a_register, data);
+};
+bool ENS160I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
+  return I2CDevice::write_byte(a_register, data);
+};
+bool ENS160I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
+  return I2CDevice::read_bytes(a_register, data, len);
+};
+bool ENS160I2CComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) {
+  return I2CDevice::write_bytes(a_register, data, len);
+};
+
+void ENS160I2CComponent::dump_config() {
+  ENS160Component::dump_config();
+  LOG_I2C_DEVICE(this);
+}
+
+}  // namespace ens160_i2c
+}  // namespace esphome
diff --git a/esphome/components/ens160_i2c/ens160_i2c.h b/esphome/components/ens160_i2c/ens160_i2c.h
new file mode 100644
index 0000000000..2df32f27bf
--- /dev/null
+++ b/esphome/components/ens160_i2c/ens160_i2c.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "esphome/components/ens160_base/ens160_base.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace ens160_i2c {
+
+class ENS160I2CComponent : public esphome::ens160_base::ENS160Component, public i2c::I2CDevice {
+  void dump_config() override;
+
+  bool read_byte(uint8_t a_register, uint8_t *data) override;
+  bool write_byte(uint8_t a_register, uint8_t data) override;
+  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
+  bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
+};
+
+}  // namespace ens160_i2c
+}  // namespace esphome
diff --git a/esphome/components/ens160_i2c/sensor.py b/esphome/components/ens160_i2c/sensor.py
new file mode 100644
index 0000000000..96cbbaa7e9
--- /dev/null
+++ b/esphome/components/ens160_i2c/sensor.py
@@ -0,0 +1,22 @@
+import esphome.codegen as cg
+from esphome.components import i2c
+from ..ens160_base import to_code_base, cv, CONFIG_SCHEMA_BASE
+
+AUTO_LOAD = ["ens160_base"]
+CODEOWNERS = ["@latonita"]
+DEPENDENCIES = ["i2c"]
+
+ens160_ns = cg.esphome_ns.namespace("ens160_i2c")
+
+ENS160I2CComponent = ens160_ns.class_(
+    "ENS160I2CComponent", cg.PollingComponent, i2c.I2CDevice
+)
+
+CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
+    i2c.i2c_device_schema(default_address=0x52)
+).extend({cv.GenerateID(): cv.declare_id(ENS160I2CComponent)})
+
+
+async def to_code(config):
+    var = await to_code_base(config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/ens160_spi/__init__.py b/esphome/components/ens160_spi/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ens160_spi/ens160_spi.cpp b/esphome/components/ens160_spi/ens160_spi.cpp
new file mode 100644
index 0000000000..fba2fdf0e4
--- /dev/null
+++ b/esphome/components/ens160_spi/ens160_spi.cpp
@@ -0,0 +1,59 @@
+#include <cstdint>
+#include <cstddef>
+
+#include "ens160_spi.h"
+#include <esphome/components/ens160_base/ens160_base.h>
+
+namespace esphome {
+namespace ens160_spi {
+
+static const char *const TAG = "ens160_spi.sensor";
+
+inline uint8_t reg_read(uint8_t reg) { return (reg << 1) | 0x01; }
+
+inline uint8_t reg_write(uint8_t reg) { return (reg << 1) & 0xFE; }
+
+void ENS160SPIComponent::setup() {
+  this->spi_setup();
+  ENS160Component::setup();
+};
+
+void ENS160SPIComponent::dump_config() {
+  ENS160Component::dump_config();
+  LOG_PIN("  CS Pin: ", this->cs_);
+}
+
+bool ENS160SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
+  this->enable();
+  this->transfer_byte(reg_read(a_register));
+  *data = this->transfer_byte(0);
+  this->disable();
+  return true;
+}
+
+bool ENS160SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
+  this->enable();
+  this->transfer_byte(reg_write(a_register));
+  this->transfer_byte(data);
+  this->disable();
+  return true;
+}
+
+bool ENS160SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
+  this->enable();
+  this->transfer_byte(reg_read(a_register));
+  this->read_array(data, len);
+  this->disable();
+  return true;
+}
+
+bool ENS160SPIComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t len) {
+  this->enable();
+  this->transfer_byte(reg_write(a_register));
+  this->transfer_array(data, len);
+  this->disable();
+  return true;
+}
+
+}  // namespace ens160_spi
+}  // namespace esphome
diff --git a/esphome/components/ens160_spi/ens160_spi.h b/esphome/components/ens160_spi/ens160_spi.h
new file mode 100644
index 0000000000..3371f37ffd
--- /dev/null
+++ b/esphome/components/ens160_spi/ens160_spi.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/components/ens160_base/ens160_base.h"
+#include "esphome/components/spi/spi.h"
+
+namespace esphome {
+namespace ens160_spi {
+
+class ENS160SPIComponent : public esphome::ens160_base::ENS160Component,
+                           public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                                 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
+  void setup() override;
+  void dump_config() override;
+
+  bool read_byte(uint8_t a_register, uint8_t *data) override;
+  bool write_byte(uint8_t a_register, uint8_t data) override;
+  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
+  bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
+};
+
+}  // namespace ens160_spi
+}  // namespace esphome
diff --git a/esphome/components/ens160_spi/sensor.py b/esphome/components/ens160_spi/sensor.py
new file mode 100644
index 0000000000..552697fe1b
--- /dev/null
+++ b/esphome/components/ens160_spi/sensor.py
@@ -0,0 +1,22 @@
+import esphome.codegen as cg
+from esphome.components import spi
+from ..ens160_base import to_code_base, cv, CONFIG_SCHEMA_BASE
+
+AUTO_LOAD = ["ens160_base"]
+CODEOWNERS = ["@latonita"]
+DEPENDENCIES = ["spi"]
+
+ens160_spi_ns = cg.esphome_ns.namespace("ens160_spi")
+
+ENS160SPIComponent = ens160_spi_ns.class_(
+    "ENS160SPIComponent", cg.PollingComponent, spi.SPIDevice
+)
+
+CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
+    {cv.GenerateID(): cv.declare_id(ENS160SPIComponent)}
+)
+
+
+async def to_code(config):
+    var = await to_code_base(config)
+    await spi.register_spi_device(var, config)
diff --git a/tests/components/ens160/test.esp32-c3-idf.yaml b/tests/components/ens160/test.esp32-c3-idf.yaml
deleted file mode 100644
index 29f48e812f..0000000000
--- a/tests/components/ens160/test.esp32-c3-idf.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 5
-    sda: 4
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160/test.esp32-c3.yaml b/tests/components/ens160/test.esp32-c3.yaml
deleted file mode 100644
index 29f48e812f..0000000000
--- a/tests/components/ens160/test.esp32-c3.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 5
-    sda: 4
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160/test.esp32-idf.yaml b/tests/components/ens160/test.esp32-idf.yaml
deleted file mode 100644
index 23f7674aef..0000000000
--- a/tests/components/ens160/test.esp32-idf.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 16
-    sda: 17
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160/test.esp32.yaml b/tests/components/ens160/test.esp32.yaml
deleted file mode 100644
index 23f7674aef..0000000000
--- a/tests/components/ens160/test.esp32.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 16
-    sda: 17
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160/test.esp8266.yaml b/tests/components/ens160/test.esp8266.yaml
deleted file mode 100644
index 29f48e812f..0000000000
--- a/tests/components/ens160/test.esp8266.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 5
-    sda: 4
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160/test.rp2040.yaml b/tests/components/ens160/test.rp2040.yaml
deleted file mode 100644
index 29f48e812f..0000000000
--- a/tests/components/ens160/test.rp2040.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-i2c:
-  - id: i2c_ens160
-    scl: 5
-    sda: 4
-
-sensor:
-  - platform: ens160
-    eco2:
-      name: ENS160 eCO2
-    tvoc:
-      name: ENS160 Total Volatile Organic Compounds
-    aqi:
-      name: ENS160 Air Quality Index
diff --git a/tests/components/ens160_i2c/common.yaml b/tests/components/ens160_i2c/common.yaml
new file mode 100644
index 0000000000..39a5b35067
--- /dev/null
+++ b/tests/components/ens160_i2c/common.yaml
@@ -0,0 +1,15 @@
+i2c:
+  - id: i2c_ens160
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+sensor:
+  - platform: ens160_i2c
+    i2c_id: i2c_ens160
+    address: 0x53
+    eco2:
+      name: "ENS160 eCO2"
+    tvoc:
+      name: "ENS160 Total Volatile Organic Compounds"
+    aqi:
+      name: "ENS160 Air Quality Index"
diff --git a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_i2c/test.esp32-c3.yaml b/tests/components/ens160_i2c/test.esp32-c3.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ens160_i2c/test.esp32-c3.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_i2c/test.esp32-idf.yaml b/tests/components/ens160_i2c/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ens160_i2c/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_i2c/test.esp32.yaml b/tests/components/ens160_i2c/test.esp32.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ens160_i2c/test.esp32.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_i2c/test.esp8266.yaml b/tests/components/ens160_i2c/test.esp8266.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ens160_i2c/test.esp8266.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_i2c/test.rp2040.yaml b/tests/components/ens160_i2c/test.rp2040.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ens160_i2c/test.rp2040.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/common.yaml b/tests/components/ens160_spi/common.yaml
new file mode 100644
index 0000000000..7250ead228
--- /dev/null
+++ b/tests/components/ens160_spi/common.yaml
@@ -0,0 +1,17 @@
+spi:
+  - id: spi_ens160
+    clk_pin: ${clk_pin}
+    mosi_pin: ${mosi_pin}
+    miso_pin: ${miso_pin}
+
+sensor:
+  - platform: ens160_spi
+    spi_id: spi_ens160
+    cs_pin: ${cs_pin}
+    eco2:
+      name: "ENS160 eCO2"
+    tvoc:
+      name: "ENS160 Total Volatile Organic Compounds"
+    aqi:
+      name: "ENS160 Air Quality Index"
+
diff --git a/tests/components/ens160_spi/test.esp32-c3-idf.yaml b/tests/components/ens160_spi/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..2415ba5dc6
--- /dev/null
+++ b/tests/components/ens160_spi/test.esp32-c3-idf.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO6
+  mosi_pin: GPIO7
+  miso_pin: GPIO5
+  cs_pin: GPIO8
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/test.esp32-c3.yaml b/tests/components/ens160_spi/test.esp32-c3.yaml
new file mode 100644
index 0000000000..2415ba5dc6
--- /dev/null
+++ b/tests/components/ens160_spi/test.esp32-c3.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO6
+  mosi_pin: GPIO7
+  miso_pin: GPIO5
+  cs_pin: GPIO8
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/test.esp32-idf.yaml b/tests/components/ens160_spi/test.esp32-idf.yaml
new file mode 100644
index 0000000000..54e027a614
--- /dev/null
+++ b/tests/components/ens160_spi/test.esp32-idf.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO16
+  mosi_pin: GPIO17
+  miso_pin: GPIO15
+  cs_pin: GPIO5
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/test.esp32.yaml b/tests/components/ens160_spi/test.esp32.yaml
new file mode 100644
index 0000000000..54e027a614
--- /dev/null
+++ b/tests/components/ens160_spi/test.esp32.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO16
+  mosi_pin: GPIO17
+  miso_pin: GPIO15
+  cs_pin: GPIO5
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/test.esp8266.yaml b/tests/components/ens160_spi/test.esp8266.yaml
new file mode 100644
index 0000000000..dbd158d030
--- /dev/null
+++ b/tests/components/ens160_spi/test.esp8266.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO14
+  mosi_pin: GPIO13
+  miso_pin: GPIO12
+  cs_pin: GPIO15
+
+<<: !include common.yaml
diff --git a/tests/components/ens160_spi/test.rp2040.yaml b/tests/components/ens160_spi/test.rp2040.yaml
new file mode 100644
index 0000000000..f6c3f1eeca
--- /dev/null
+++ b/tests/components/ens160_spi/test.rp2040.yaml
@@ -0,0 +1,7 @@
+substitutions:
+  clk_pin: GPIO2
+  mosi_pin: GPIO3
+  miso_pin: GPIO4
+  cs_pin: GPIO5
+
+<<: !include common.yaml
diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml
index 18d92289cd..a7d8dcb3f4 100644
--- a/tests/test3.1.yaml
+++ b/tests/test3.1.yaml
@@ -245,13 +245,6 @@ sensor:
       name: "ADE7953 Reactive Power B"
     update_interval: 1s
 
-  - platform: ens160
-    eco2:
-      name: "ENS160 eCO2"
-    tvoc:
-      name: "ENS160 Total Volatile Organic Compounds"
-    aqi:
-      name: "ENS160 Air Quality Index"
   - platform: tmp102
     name: TMP102 Temperature
   - platform: hm3301