commit 419143d70a4d07917612abe272454adad67180e5 Author: T Date: Fri Mar 14 15:23:22 2025 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c40fd24 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cmake*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..843f322 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) + +project(optoma_rs232 LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17 CACHE STRING "") + +include_directories(../../..) + +add_library(optoma_rs232 SHARED + optoma_rs232.cpp +) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b7c141f --- /dev/null +++ b/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, uart +from esphome.const import CONF_ID + +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +CONF_OPTOMA_RS232_ID = "optoma_rs232_id" + +optoma_ns = cg.esphome_ns.namespace("optoma_rs232") +OptomaRS232Component = optoma_ns.class_("OptomaRS232Component", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema({ + cv.GenerateID(): cv.declare_id(OptomaRS232Component), + }) + .extend(cv.polling_component_schema("10s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +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..2325e1c --- /dev/null +++ b/binary_sensor.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import DEVICE_CLASS_POWER, ICON_POWER + +from . import CONF_OPTOMA_RS232_ID, OptomaRS232Component + +DEPENDENCIES = ["optoma_rs232"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( + icon=ICON_POWER, + device_class=DEVICE_CLASS_POWER, +).extend({ + cv.GenerateID(CONF_OPTOMA_RS232_ID): cv.use_id(OptomaRS232Component), +}) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_OPTOMA_RS232_ID]) + sens = await binary_sensor.new_binary_sensor(config) + cg.add(hub.set_beamer_power_binary_sensor(sens)) diff --git a/optoma_rs232.cpp b/optoma_rs232.cpp new file mode 100644 index 0000000..b64d3f6 --- /dev/null +++ b/optoma_rs232.cpp @@ -0,0 +1,143 @@ +#include "optoma_rs232.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace optoma_rs232 { + +[[maybe_unused]] static const char *const TAG = "optoma_rs232"; + +static const char *QUERIES[] = { + "~00150 18\r", // Temp + "~00108 1\r", // Lamp time + "~00351 1\r", // Fan 1 + "~00150 1\r", // Info +}; + +template static void publish(C *c, M m) { + if (c) + c->publish_state(m); +} + +void OptomaRS232Component::dump_config() { + ESP_LOGCONFIG(TAG, "Optoma RS232:"); + check_uart_settings(9600); +} + +void OptomaRS232Component::loop() { + if (!available()) + return; + + // pos = 0 and crlf: none + // pos > 0 and crlf: send + // pos < end and not crlf: add + // pos == end and not crlf: discard + while (available()) { + uint8_t c; + if (!read_byte(&c)) + continue; + if (!c) + continue; + + if (c == '\r' || c == '\n') { + if (cursor_ > 0) { + buffer_[cursor_] = 0; + process_line_(buffer_); + cursor_ = 0; + } + } else if (cursor_ < sizeof(buffer_) - 1) { + buffer_[cursor_++] = toupper(c); + } + } +} + +void OptomaRS232Component::update() { + last_query_ = (last_query_ + 1) % sizeof(QUERIES) / sizeof(QUERIES[0]); + write_array(reinterpret_cast(QUERIES[last_query_]), strlen(QUERIES[last_query_])); +} + +void OptomaRS232Component::process_line_(const std::string &str) { + // if we are waiting for the projector to respond to a command. + // it will respond P (pass) or F (fail) before giving us the actual response to the command. + if (str == "P" || str == "F") { + ESP_LOGD(TAG, "command response received"); + return; + } + + // assuming any commands have been dealt with above, we listen for messages from the projector + // the OK-something messages are in response to status queries, sometimes these are in caps, sometimes not + // hence the toUpperCase call earlier + // the INFO messages come in automatically when the projector changes state + if ( // x == "OK1" || // status query returned power on + str == "INFO1") { // warming up + publish(beamer_power_binary_sensor_, true); + return; + } + + if ( // x == "OK0" || // status query returned power off + str == "INFO2" || // cooling down + str == "INFO0") { // going into standby + publish(beamer_power_binary_sensor_, false); + return; + } + + if (str_startswith(str, "OK")) { + process_query_response_(str); + return; + } + + ESP_LOGD("projector", "unhandled message: %s", str.c_str()); +} + +void OptomaRS232Component::process_query_response_(const std::string &str) { + if (str.length() >= 3) { + switch (last_query_) { + case 0: + publish(beamer_temp_sensor_, strtol(str.c_str() + 2, 0, 10)); + break; + case 1: + publish(beamer_lamp_time_sensor_, strtol(str.c_str() + 2, 0, 10)); + break; + case 2: + publish(beamer_fan1_sensor_, strtol(str.c_str() + 2, 0, 10)); + break; + case 3: { + char buf[17]{}; + strncpy(buf, str.c_str(), sizeof(buf)); + publish(beamer_color_mode_sensor_, strtol(buf + 14, 0, 10)); + buf[14] = 0; + + // publish(beamer_firmware_,strtol(buf + 10, 0, 10)); + buf[10] = 0; + + int input = -1; // atol(buf + 8); BUGGY + switch (input) { + case Inputs::HDMI_1: + publish(beamer_input_text_sensor_, "HDMI 1"); + break; + case Inputs::HDMI_2: + publish(beamer_input_text_sensor_, "HDMI 2"); + break; + case Inputs::VGA: + publish(beamer_input_text_sensor_, "VGA"); + break; + default: + case Inputs::UNKNOWN: + publish(beamer_input_text_sensor_, "Unknown"); + break; + } + buf[8] = 0; + + publish(beamer_lamp_time_sensor_, strtol(buf + 3, 0, 10)); + buf[3] = 0; + + publish(beamer_power_binary_sensor_, strtol(buf + 2, 0, 10)); + break; + } + default:; + } + } +} + +} // namespace optoma_rs232 +} // namespace esphome diff --git a/optoma_rs232.h b/optoma_rs232.h new file mode 100644 index 0000000..9771d5e --- /dev/null +++ b/optoma_rs232.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_SELECT +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif + +namespace esphome { +namespace optoma_rs232 { + +// clang-format off +class DummySensor { public: template static void publish_state(M) {} }; +class DummySelect {}; +// clang-format on + +enum Inputs { + UNKNOWN = 0, + VGA = 2, + HDMI_1 = 7, + HDMI_2 = 8, +}; + +class OptomaRS232Component : public uart::UARTDevice, public PollingComponent { + public: + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + + // clang-format off +#ifndef USE_SENSOR +#define SUB_SENSOR(name) protected: DummySensor *name##_sensor_{nullptr}; public: +#endif +#ifndef USE_BINARY_SENSOR +#define SUB_BINARY_SENSOR(name) protected: DummySensor *name##_binary_sensor_{nullptr}; public: +#endif +#ifndef USE_TEXT_SENSOR +#define SUB_TEXT_SENSOR(name) protected: DummySensor *name##_text_sensor_{nullptr}; public: +#endif +#ifndef USE_SELECT +#define SUB_SELECT(name) protected: DummySelect *name##_select_{nullptr}; public: +#endif + // clang-format on + + SUB_SENSOR(beamer_temp) + SUB_SENSOR(beamer_lamp_time) + SUB_SENSOR(beamer_fan1) + SUB_SENSOR(beamer_color_mode) + SUB_BINARY_SENSOR(beamer_power) + SUB_TEXT_SENSOR(beamer_input) + SUB_SELECT(beamer_input) + + protected: + void process_line_(const std::string &str); + void process_query_response_(const std::string &str); + bool waiting_for_command_response_ = false; + int last_query_ = -1; + + private: + char buffer_[128]{}; + size_t cursor_ = 0; +}; + +} // namespace optoma_rs232 +} // namespace esphome diff --git a/select.py b/select.py new file mode 100644 index 0000000..f6f656e --- /dev/null +++ b/select.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select + +from . import CONF_OPTOMA_RS232_ID, OptomaRS232Component, optoma_ns + +DEPENDENCIES = ["optoma_rs232"] + +Inputs = optoma_ns.enum("Inputs") +INPUT_OPTIONS = { + "Unknown": 0, + "VGA": 2, + "HDMI 1": 7, + "HDMI 2": 8, +} + +CONFIG_SCHEMA = select.select_schema( +).extend({ + cv.GenerateID(CONF_OPTOMA_RS232_ID): cv.use_id(OptomaRS232Component), +}) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_OPTOMA_RS232_ID]) + sens = await select.new_select(config, options=list(INPUT_OPTIONS)) + cg.add(hub.set_beamer_input_select(sens)) diff --git a/sensor.py b/sensor.py new file mode 100644 index 0000000..8830a00 --- /dev/null +++ b/sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_DURATION, + CONF_TEMPERATURE, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_SPEED, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_CELSIUS, + UNIT_HOUR, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +from . import CONF_OPTOMA_RS232_ID, optoma_ns, OptomaRS232Component + +DEPENDENCIES = ["optoma_rs232"] + +CONF_FAN_SPEED = "fan_speed" + +CONFIG_SCHEMA = ( + cv.Schema({ + cv.GenerateID(CONF_OPTOMA_RS232_ID): cv.use_id(OptomaRS232Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DURATION): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_FAN_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SPEED, + state_class=STATE_CLASS_MEASUREMENT, + ), + }) + .extend(cv.polling_component_schema("10s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_OPTOMA_RS232_ID]) + + if cfg := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(cfg) + cg.add(hub.set_beamer_temp_sensor(sens)) + if cfg := config.get(CONF_DURATION): + sens = await sensor.new_sensor(cfg) + cg.add(hub.set_beamer_lamp_time_sensor(sens)) + if cfg := config.get(CONF_FAN_SPEED): + sens = await sensor.new_sensor(cfg) + cg.add(hub.set_beamer_fan1_sensor(sens)) + # if cfg := config.get(): + # sens = await sensor.new_sensor(cfg) + # cg.add(hub.set_beamer_color_mode_sensor(sens)) diff --git a/text_sensor.py b/text_sensor.py new file mode 100644 index 0000000..a31c3b5 --- /dev/null +++ b/text_sensor.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor + +from . import CONF_OPTOMA_RS232_ID, OptomaRS232Component + +DEPENDENCIES = ["optoma_rs232"] + +CONFIG_SCHEMA = text_sensor.text_sensor_schema( +).extend({ + cv.GenerateID(CONF_OPTOMA_RS232_ID): cv.use_id(OptomaRS232Component), +}) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_OPTOMA_RS232_ID]) + sens = await text_sensor.new_text_sensor(config) + cg.add(hub.set_beamer_input_text_sensor(sens))