Z pewną nieśmiałością tworzę pierwszy wątek. Ale coś mi się udało wystrugać, może komuś się przyda a może ktoś podpowie co i jak poprawić.
Do rzeczy: mój projektor Optoma USH38x nie ma żadnych funkcji sieciowych. Sterowałem nim z HA za pośrednictwem uniwersalnego pilota IR, ale takie rozwiązanie ma dla mnie kilka istotnych wad, przede wszystkim brak potwierdzenia wykonania danej komendy oraz brak możliwości weryfikowania aktualnego stanu urządzenia. W końcu choroba uwięziła mnie w domu, więc zabrałem się za działanie. Posiłkowałem się podpowiedziami ChatGPT, ale to dość kapryśne i w sumie mało bystre narzędzie. Ma sporą wiedzę, ale bystrością musiałem wykazać się ja
Ponieważ projektor jest podpięty do wzmacniacza stereo z obsługą HDMI na projektorze nie uruchamiam żadnych funkcji, tylko i wyłącznie ON/OFF, i tylko to implementowałem w tym zadaniu. Po drodze dodałem jeszcze zgrubną weryfikację pozostałego okresu żywotności lampy. Nie ma zmian źródła czy głośności - nic. Ale to już proste, to w razie potrzeby można dodać samodzielnie.
ESP32 jest za pośrednictwem konwertera RS232-TTL podpięty do portu RS-232 projektora i przez ten port wydaje komendy i odpytuje projektor o bieżący stan.
Sprzęt użyty w projekcie
- Projektor: Optoma UHD38x
- Płytka ESP32: ESP32 DevKit V1, 30-pin
- Konwerter RS232-TTL: Waveshare RS232 do TTL MAX3232
- Zasilanie ESP32: przez port USB-C
Ogólny opis działania
Sterowanie projektorem Optoma UHD38x poprzez UART/RS232 oraz integrację z Home Assistant. System:
- cyklicznie odczytuje projektor o stan ON/OFF oraz o godziny pracy lampy,
- umożliwia włączanie/wyłączanie urządzenia,
- aktualizuje sensory (stan ON/OFF, godziny pracy lampy, żywotność lampy).
Kilka uwag do pliku YAML
Sekcja “logger” - przede wszystkim wyłączenie logowania wysyłanego na RS-232 projektora (baud_rate: 0), wysyłanie loga na RS232 może ogłupiać projektor.
Sekcja “interval” - dwa harmonogramy cykliczne:
- co 300 sekund: poll_power_state – odczyt stanu ON/OFF,
- co 24 godziny: poll_lamp_hours – aktualizacja danych o lampie.
Włączenie/wyłączenie projektora pilotem IR może być niezauważone przez czas odpowiadający interwałowi wywołania poll_power_state. Obecnie już wydłużyłem interwał dla poll_power_state do 15 minut by zredukować liczbę zapytań do projektora.
Sekcja “switch” - przycisk włącz/wyłącz projektor:
- wysyła odpowiednią komendę przez RS232,
- czeka: 90s przy włączeniu, 45s przy wyłączeniu,
- potem uruchamia poll_power_state, który sprawdza rzeczywisty stan.
W kliknięciu przełącznik chwilowo wraca do poprzedniego stanu, do czasu zakończenia badania stanu projektora. Obecnie już zredukowałem oczekiwanie przy włączeniu do 75s i przy wyłączeniu do 35s, ale jest jeszcze możliwość kolejnej redukcji.
Sekcja “sensor”
- Optoma Lamp Hours: pokazuje liczbę przepracowanych godzin
- Optoma Lamp Remaining %: przeliczony procent pozostałego czasu pracy lampy
Obliczanie żywotności lampy
Przyjąłem model dwóch trybów pracy lampy (bo tak jest u mnie):
- Bright (pełna jasność) - żywotność 4000 godzin,
- Dynamic (tryb eco/dynamiczny) - żywotność 15000 godzin.
Zmienna bright_hours_initial pozwala ręcznie uwzględnić, że pierwsze godziny pracy lampy odbyły się w trybie Bright - tutaj dwie godziny. W praktyce nie ma to żadnego znaczenia (jeżeli to cały czas tylko dwie godziny).
Planowane ulepszenia
- Zastąpienie cyklicznego odpytywania rozwiązaniem opartym na wyjściu 12V projektora (przeznaczonym do automatycznego rozwijania ekranu).
- Po podłączeniu wyjścia 12V (poprzez np. optoizolator) do ESP32 możliwe będzie szybkie i niezawodne wykrywanie faktycznego stanu włączenia projektora - niezależnie od tego, czy został włączony przez Home Assistant, czy fizycznym pilotem IR (obecnie zastosowanie pilota, a dokładnie wywołana tym zmiana stanu projektora, będzie zauważona ze znacznym opóźnieniem).
- To może całkowicie zastąpić skrypt poll_power_state, zmniejszając opóźnienie wykrywania i zużycie zasobów UART.
Planowane komponenty: optoizolator 1-kanałowy, zasilany z pinu VIN na ESP32, wykrywający obecność napięcia 12V.
No i na koniec najważniejsze, plik .yaml
esphome:
name: esp32-optoma
friendly_name: Optoma UHD38x
on_boot:
priority: -100
then:
- script.execute: poll_lamp_hours
esp32:
board: esp32dev
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
logger:
level: DEBUG # po testach warto zmniejszyć poziom logowania
baud_rate: 0
api:
ota:
platform: esphome
uart:
id: uart_bus
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 9600
stop_bits: 1
parity: NONE
globals:
- id: lamp_hours
type: int
restore_value: yes
- id: bright_hours_initial
type: int
initial_value: '2' # Pierwsze 2h pracy w trybie Bright
interval:
- interval: 300s
then:
- script.execute: poll_power_state
- interval: 24h
then:
- script.execute: poll_lamp_hours
script:
- id: poll_power_state
mode: queued
then:
- lambda: |-
uint8_t d;
while (id(uart_bus).available())
id(uart_bus).read_byte(&d);
- lambda: |-
id(uart_bus).write_str("~00124 1\r");
- delay: 300ms
- lambda: |-
std::string buf;
uint8_t c;
while (id(uart_bus).available()) {
if (!id(uart_bus).read_byte(&c)) break;
if (c == '\r' || c == '\n') continue;
buf.push_back(static_cast<char>(c));
}
if (!buf.empty()) {
id(optoma_response).publish_state(buf.c_str());
for (auto &ch : buf) ch = tolower(ch);
if (buf.find("ok1") != std::string::npos || buf.find("info1") != std::string::npos) {
id(projector_power_state).publish_state(true);
} else {
id(projector_power_state).publish_state(false);
}
}
- id: poll_lamp_hours
mode: queued
then:
- lambda: |-
uint8_t d;
while (id(uart_bus).available())
id(uart_bus).read_byte(&d);
- lambda: |-
id(uart_bus).write_str("~00150 1\r");
- delay: 500ms
- lambda: |-
std::string buf;
uint8_t c;
while (id(uart_bus).available()) {
if (!id(uart_bus).read_byte(&c)) break;
if (c == '\r' || c == '\n') continue;
buf.push_back(static_cast<char>(c));
}
ESP_LOGD("optoma", "Lamp RAW → '%s'", buf.c_str());
if (buf.rfind("OK", 0) == 0 && buf.length() >= 8) {
id(lamp_hours) = std::stoi(buf.substr(2, 6));
id(optoma_lamp_hours).publish_state(id(lamp_hours));
float total = id(lamp_hours);
float bright = id(bright_hours_initial);
if (total < bright) total = bright;
float dyn = total - bright;
float wear = bright / 4000.0 + dyn / 15000.0;
if (wear > 1.0) wear = 1.0;
id(optoma_lamp_remaining).publish_state(round((1.0 - wear) * 100.0));
}
text_sensor:
- platform: template
name: "Optoma Response"
id: optoma_response
binary_sensor:
- platform: template
name: "Optoma Power State"
id: projector_power_state
device_class: power
switch:
- platform: template
name: "Optoma Power"
id: optoma_power
optimistic: false
lambda: |-
return id(projector_power_state).state;
turn_on_action:
- lambda: |-
ESP_LOGD("optoma", "Sending POWER ON toggle");
id(uart_bus).write_str("~0000 1\r");
- delay: 90s
- script.execute: poll_power_state
turn_off_action:
- lambda: |-
ESP_LOGD("optoma", "Sending POWER OFF toggle");
id(uart_bus).write_str("~0000 0\r");
- delay: 45s
- script.execute: poll_power_state
sensor:
- platform: template
name: "Optoma Lamp Hours"
id: optoma_lamp_hours
unit_of_measurement: "h"
accuracy_decimals: 0
update_interval: never
- platform: template
name: "Optoma Lamp Remaining %"
id: optoma_lamp_remaining
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: never
Będę wdzięczny za ewentualne uwagi i wskazówki co i jak poprawić. Całość aktualnie działa od wczoraj, jestem w fazie testowania. Na razie wszystko działa prawidłowo.
EDIT: kilka poprawek stylistycznych i info o zmianie niektórych parametrów czasowych w konfiguracji
EDIT2: niezbędne będą poprawki - wyłączony projektor czasem (nie wiem na jakiej zasadzie i od czego to zależy) raportuje złą liczbę godzin pracy lampy. Obecnie testuję odczytywanie tej wartości podczas startu i wyłączania projektora (wtedy kiedy już albo jeszcze jest włączony) a nie w stałym interwale 24h. Jak będę gotowy wkleję poprawioną wersję pliku .yaml.