diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e494da0
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID
+
+
+CODEOWNERS = ["@sqozz"]
+DEPENDENCIES = ["uart"]
+AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"]
+
+tw7100_ns = cg.esphome_ns.namespace("tw7100")
+tw7100 = tw7100_ns.class_("tw7100Component", cg.PollingComponent, uart.UARTDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(tw7100),
+}).extend(cv.polling_component_schema("60s")).extend(uart.UART_DEVICE_SCHEMA)
+
+FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
+    "tw7100", baud_rate=9600, require_rx=True, require_tx=True
+)
+
+
+async def to_code(config):
+  var = cg.new_Pvariable(config[CONF_ID])
+  await cg.register_component(var, config)
+  await uart.register_uart_device(var, config)
diff --git a/binary_sensor.py b/binary_sensor.py
new file mode 100644
index 0000000..648cdd6
--- /dev/null
+++ b/binary_sensor.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import (
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_CONNECTIVITY,
+    CONF_POWER,
+    CONF_ID,
+)
+
+from . import tw7100
+
+DEPENDENCIES = ["tw7100"]
+
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.use_id(tw7100),
+        cv.Optional("signal_detected"): binary_sensor.binary_sensor_schema(
+            device_class=DEVICE_CLASS_CONNECTIVITY
+        ),
+        cv.Optional(CONF_POWER): binary_sensor.binary_sensor_schema(
+            device_class=DEVICE_CLASS_POWER
+        ),
+    }
+)
+
+
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_ID])
+
+    if "signal_detected" in config:
+        sens = await binary_sensor.new_binary_sensor(config["signal_detected"])
+        cg.add(parent.set_has_signal(sens))
+
+    if CONF_POWER in config:
+        sens = await binary_sensor.new_binary_sensor(config[CONF_POWER])
+        cg.add(parent.set_powered(sens))
diff --git a/example.yaml b/example.yaml
new file mode 100644
index 0000000..f317036
--- /dev/null
+++ b/example.yaml
@@ -0,0 +1,33 @@
+external_components:
+  - source:
+      type: local
+      path: external_components
+    components:
+      - tw7100
+
+tw7100:
+  uart_id: uart_bus
+
+uart:
+  id: uart_bus
+  tx_pin: 15
+  rx_pin: 13
+  baud_rate: 9600
+
+binary_sensor:
+  - platform: tw7100
+    signal_detected:
+      name: "Signal"
+    power:
+      name: "Powered"
+
+sensor:
+  - platform: tw7100
+    hours:
+      name: "Projector lamp hours"
+    power:
+      name: "Projector power status"
+    state:
+      name: "Projector signal status"
+    illuminance:
+      name: "Projector luminance level"
diff --git a/sensor.py b/sensor.py
new file mode 100644
index 0000000..611235b
--- /dev/null
+++ b/sensor.py
@@ -0,0 +1,58 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import (
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_DURATION,
+    DEVICE_CLASS_RUNNING,
+    DEVICE_CLASS_ILLUMINANCE,
+    CONF_ILLUMINANCE,
+    CONF_HOURS,
+    CONF_STATE,
+    CONF_POWER,
+    CONF_ID,
+)
+
+from . import tw7100
+
+DEPENDENCIES = ["tw7100"]
+
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.use_id(tw7100),
+        cv.Optional(CONF_HOURS): sensor.sensor_schema(
+            device_class=DEVICE_CLASS_DURATION # Lamp hours
+        ),
+        cv.Optional(CONF_POWER): sensor.sensor_schema(
+            accuracy_decimals=0,
+            device_class=DEVICE_CLASS_POWER # Power status
+        ),
+        cv.Optional(CONF_STATE): sensor.sensor_schema(
+            device_class=DEVICE_CLASS_POWER # Signal status
+        ),
+        cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
+            device_class=DEVICE_CLASS_ILLUMINANCE # Luminance level
+        ),
+    }
+)
+
+
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_ID])
+
+    if CONF_HOURS in config:
+        sens = await sensor.new_sensor(config[CONF_HOURS])
+        cg.add(parent.set_lamp_hours(sens))
+
+    if CONF_POWER in config:
+        sens = await sensor.new_sensor(config[CONF_POWER])
+        cg.add(parent.set_power_status(sens))
+
+    if CONF_STATE in config:
+        sens = await sensor.new_sensor(config[CONF_STATE])
+        cg.add(parent.set_signal_status(sens))
+
+    if CONF_ILLUMINANCE in config:
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
+        cg.add(parent.set_luminance_level(sens))
diff --git a/tw7100.cpp b/tw7100.cpp
new file mode 100644
index 0000000..08b5af3
--- /dev/null
+++ b/tw7100.cpp
@@ -0,0 +1,228 @@
+#include "tw7100.h"
+#include "esphome/core/log.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/helpers.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace tw7100 {
+
+bool waiting_for_answer = false;
+std::vector<std::string> cmd_queue;
+
+std::vector<std::string> pwr_off_query_cmds{ "PWR?", "LAMP?" };
+std::vector<std::string> pwr_on_query_cmds{
+// Projection screen adjustment settings
+/*
+  "VKEYSTONE?",
+  "HKEYSTONE?",
+  "QC?",
+  "CORRECTMET?",
+  "ASPECT?",
+  "LUMINANCE?",
+  "OVSCAN?",
+*/
+//
+// Source/Input/Resolution settings
+  "SOURCE?",
+// Image settings
+/*
+  "BRIGHT?",
+  "CONTRAST?",
+  "DENSITY?",
+  "TINT?",
+  "CTEMP?",
+  "FCOLOR?",
+  "CMODE?",
+  "NRS?",
+  "MPEGNRS?",
+  "OFFSETR?",
+  "OFFSETG?",
+  "OFFSETB?",
+  "GAINR?",
+  "GAING?",
+  "GAINB?",
+  "GAMMA?",
+  "CSEL?",
+  "4KENHANCE?",
+  "IMGPRESET?",
+  "SHRF?",
+  "SHRS?",
+  "DERANGE?",
+  "MCFI?",
+  "CLRSPACE?",
+  "DYNRANGE?",
+  "HDRPQ?",
+  "HDRHLG?",
+  "IMGPROC?",
+*/ 
+//
+// Sound settings
+/*
+  "VOL?",
+  "AUDIOOUT?",
+*/
+//
+  "MUTE?",
+// Environment settings
+/*
+  "HREVERSE?",
+  "VREVERSE?",
+  "MSEL?",
+  "SPEED?",
+  "ILLUM?",
+  "STANDBYCONF?",
+  "PRODUCT?",
+*/
+//
+// Home Screen settings
+  "AUTOHOME?",
+// Network settings
+  "WLPWR?",
+// Bluetooth
+  "BTAUDIO?",
+// Information
+  "SIGNAL?",
+  "SOURCELIST?",
+//  "SOURCELISTA?", // same as SOURCELIST
+  "LOGTO?",
+  "SNO?"
+};
+
+void tw7100Component::setup() {
+  static const char *const TAG = "setup()";
+  ESP_LOGV(TAG, "SETUP");
+};
+
+void tw7100Component::update() {
+  static const char *const TAG = "update()";
+  if (cmd_queue.size() == 0) {
+    for (auto &cmd : pwr_off_query_cmds) {
+      cmd_queue.push_back(cmd);
+    }
+    if ((powered_->state)) {;
+      for (auto &cmd : pwr_on_query_cmds) {
+        cmd_queue.push_back(cmd);
+      }
+    }
+  }
+};
+
+void tw7100Component::loop() {
+  static const char *const TAG = "loop()";
+  unsigned long _startMillis;  // used for timeout measurement
+
+  _startMillis = millis();
+  do {
+    std::string response = readStringUntil(':');
+
+    if (response == "") { // no response on bus, we can use our time to do something else
+      if (cmd_queue.size() > 0 && !waiting_for_answer) {
+        waiting_for_answer = true;
+        std::string cmd = cmd_queue[0];
+        cmd_queue.erase(cmd_queue.begin());
+        ESP_LOGV(TAG, "sending out command: %s", cmd.c_str());
+        write_str((cmd + "\r\n").c_str());
+        flush();
+      }
+    } else { // response found, handle eventual errors
+      ESP_LOGV(TAG, "buffer content: %s", response.c_str());
+      std::pair<std::string, std::string> parsed_response = parse_response(response);
+      if (parsed_response.first == "ERR" && parsed_response.second == "ERR") {
+        if (powered_->state) {;
+          cmd_queue.push_back("ERR?"); // TODO: produces a ERR? loop on bootup if projector is turned off
+        } else {
+          ESP_LOGE(TAG, "got ERR while projector is off, check if PWR+LAMP is too much?");
+        }
+      } else if (parsed_response.first == "ERR") {
+        // TODO: Handle error
+      } else {
+        update_sensor(parsed_response);
+      }
+      waiting_for_answer = false;
+    }
+    //ESP_LOGV(TAG, "read processing finished");
+  } while (millis() - _startMillis < _max_loop_time);
+  //ESP_LOGV(TAG, "inner timed loop done");
+};
+
+void tw7100Component::dump_config() {
+  static const char *const TAG = "dump_config()";
+  ESP_LOGCONFIG(TAG, "TW7100:");
+  this->check_uart_settings(9600);
+};
+
+void tw7100Component::update_sensor(std::pair<std::string, std::string> data) {
+  static const char *const TAG = "update_sensor()";
+  std::string cmd = data.first;
+  std::string value_string = data.second;
+  if (cmd == "PWR") {
+    ESP_LOGV(TAG, "updating power sensors"); 
+    int value = std::stoi(value_string);
+    powered_->publish_state(value > 0);
+    power_status_->publish_state(value);
+  } else if (cmd == "LAMP") {
+    ESP_LOGV(TAG, "updating lamp sensors");
+    int value = std::stoi(value_string);
+    lamp_hours_->publish_state(value);
+  } else if (cmd == "SIGNAL") {
+    ESP_LOGV(TAG, "updating signal sensors");
+    int value = std::stoi(value_string);
+    has_signal_->publish_state(value > 0);
+    signal_status_->publish_state(value);
+  } else if (cmd == "LUMINANCE") {
+    ESP_LOGV(TAG, "updating luminance sensors");
+    int value = std::stoi(value_string);
+    luminance_level_->publish_state(value);
+  } else if (cmd == "SOURCELIST") {
+    for(char& c : value_string) {
+      ESP_LOGV(TAG, "%c (%02x)", c, c);
+    }
+  } else {
+    ESP_LOGW(TAG, "Command %s unknown, skipping updating sensor with value", cmd.c_str());
+  }
+}
+
+std::pair<std::string, std::string> tw7100Component::parse_response(std::string data) {
+  static const char *const TAG = "parse_response()";
+  std::string key = "";
+  std::string value = "";
+  data.erase(std::remove(data.begin(), data.end(), '\r'), data.cend());
+  data.erase(std::remove(data.begin(), data.end(), '\n'), data.cend());
+  if (data != "") {
+    key = data.substr(0, data.find("="));
+    value = data.substr(data.find("=") + 1, data.length());
+  }
+
+  ESP_LOGV(TAG, "parsed response into (key: %s, value: %s)", key.c_str(), value.c_str());
+  return std::make_pair(key, value);
+}
+
+int tw7100Component::timedRead() {
+  unsigned long _startMillis;  // used for timeout measurement
+  int c;
+  _startMillis = millis();
+  do {
+    if (this->available() > 0) {
+      c = this->read();
+      //ESP_LOGV("timedRead()", "timed read byte: %c (%x)", c, c);
+      if (c >= 0) {
+        return c;
+      }
+    }
+  } while (millis() - _startMillis < _readStringUntil_timeout);
+  return -1;  // -1 indicates timeout
+}
+
+std::string tw7100Component::readStringUntil(char terminator) {
+  std::string line="";
+  int c = timedRead();
+  while (c >= 0 && (char)c != terminator) {
+    line += (char)c;
+    c = timedRead();
+  }
+  return line;
+};
+
+}  // namespace tw7100
+}  // namespace esphome
diff --git a/tw7100.h b/tw7100.h
new file mode 100644
index 0000000..7839527
--- /dev/null
+++ b/tw7100.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/log.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace tw7100 {
+
+class tw7100Component : public PollingComponent, public uart::UARTDevice {
+  private:
+    unsigned long _max_loop_time = 30;
+    unsigned long _readStringUntil_timeout = _max_loop_time/5; // number of milliseconds to wait for the next char before aborting timed read
+
+  public:
+    tw7100Component() = default;
+
+    std::string readStringUntil(char terminator);
+    void update_sensor(std::pair<std::string, std::string> data);
+    std::pair<std::string, std::string> parse_response(std::string data);
+    int timedRead(void);
+    void setup() override;
+    void update() override;
+    void loop() override;
+    void dump_config() override;
+    float get_setup_priority() const override { return setup_priority::BUS; }
+    void set_powered(binary_sensor::BinarySensor *powered) { this->powered_ = powered; }
+    void set_has_signal(binary_sensor::BinarySensor *has_signal) { this->has_signal_ = has_signal; }
+    void set_lamp_hours(sensor::Sensor *lamp_hours) { this->lamp_hours_ = lamp_hours; }
+    void set_power_status(sensor::Sensor *power_status) { this->power_status_ = power_status; }
+    void set_signal_status(sensor::Sensor *signal_status) { this->signal_status_ = signal_status; }
+    void set_luminance_level(sensor::Sensor *luminance_level) { this->luminance_level_ = luminance_level; }
+    void set_serial(text_sensor::TextSensor *serial) { this->serial_ = serial; }
+
+  protected:
+    binary_sensor::BinarySensor *powered_{nullptr};
+    binary_sensor::BinarySensor *has_signal_{nullptr};
+    sensor::Sensor *lamp_hours_{nullptr};
+    sensor::Sensor *power_status_{nullptr};
+    sensor::Sensor *signal_status_{nullptr};
+    sensor::Sensor *luminance_level_{nullptr};
+    text_sensor::TextSensor *serial_{nullptr};
+};
+
+}  // namespace tw7100
+}  // namespace esphome