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 cmd_queue; + +std::vector pwr_off_query_cmds{ "PWR?", "LAMP?" }; +std::vector 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 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 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 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 data); + std::pair 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