Serial Projector control with ESPHome

I am now able to serially control my video projector using the following config in ESPhome and thought I’d share it in case it helps someone in future:

This is an ESP32 board so I am using a second hardware UART

uart:
  tx_pin: GPIO12
  rx_pin: GPIO32
  baud_rate: 9600
  id: projector

And a custom Home Assistant service to send the serial data:

api:
  password: !secret api_password
  services:
    - service: write
      variables:
        command: string
      then:
        - uart.write:
            id: projector
            data: !lambda |-
              std::string str = command;
              std::vector<uint8_t> vec(str.begin(), str.end());
              return vec;

This is how it is used in a couple of scripts in Home Assistant:

projector_on:
  - service: esphome.projector_uart_write
    data:
      command: "PWR ON\r\n"

projector_menu:
  sequence:
  - service: esphome.projector_uart_write
    data:
      command: "KEY 03\r\n"

The scripts can be called by button tap actions or used in automations.

At the moment I have not implemented a receive function to check if the command was successfully executed. Though I do check the trigger output port (usually used to lower automatic screens) to check if the projector is on or off:

binary_sensor:
  - platform: gpio
    name: Projector Power
    pin: GPIO18

This is fed via a resistor voltage divider to drop the 12V out when the projector is on to 3V.

I probably wont check for valid command responses as there’s nothing I can do if the command does not execute anyway (hasn’t happened yet). But the Epson projector API document does list the minimum times between successive commands. I’ve found this to be way too conservative and have lowered the recommended 3 second gap to 1 second using a guard input_boolean in my Home Assistant scripts like so:

projector_menu:
  sequence:
  - condition: state
    entity_id: input_boolean.uart_busy
    state: 'off'
  - service: input_boolean.turn_on
    entity_id: input_boolean.uart_busy
  - service: esphome.projector_uart_write
    data:
      command: "KEY 03\r\n"
  - delay: 1
  - service: input_boolean.turn_off
    entity_id: input_boolean.uart_busy

All the command scripts have this so if one command is being executed another will be ignored until a second later (40 seconds for power on!). The default “single” mode for scripts ensures this is not interrupted either.

There are a few other niceties like a wifi connected LED, a TX in progress LED, wifi signal strength sensor etc… in the full ESP config:

esphome:
  name: projector_uart
  platform: ESP32
  board: mhetesp32minikit

wifi:
  ssid: 'WAPRU'
  password: !secret wifi_pwd
  manual_ip:
    static_ip: 10.1.1.194
    gateway: 10.1.1.1
    subnet: 255.255.255.0

api:
  password: !secret api_password
  services:
    - service: uart_write
      variables:
        command: string
      then:
        - output.turn_on: tx_led
        - uart.write:
            id: projector
            data: !lambda |-
              std::string str = command;
              std::vector<uint8_t> vec(str.begin(), str.end());
              return vec;
        - delay: 0.25sec
        - output.turn_off: tx_led
ota:
  password: !secret esp_pwd

logger:
  # level: VERY_VERBOSE

binary_sensor:
  - platform: status
    name: "Projector UART Status"

  - platform: gpio
    name: Projector Power
    pin: GPIO18

interval:
  - interval: 10s
    then:
      if:
        condition:
          wifi.connected:
        then:
          - output.turn_on: wifi_led
        else:
          - output.turn_off: wifi_led

output:
  - id: wifi_led
    platform: gpio
    pin: GPIO16

  - id: tx_led
    platform: gpio
    pin: GPIO17

sensor:
  - platform: wifi_signal
    name: "Projector UART WiFi Signal"
    update_interval: 15s
    filters:
      - sliding_window_moving_average:
          window_size: 15
          send_every: 15
          send_first_at: 1
    icon: mdi:wifi

switch:
  - platform: restart
    name: "Projector UART Restart"

uart:
  tx_pin: GPIO12
  rx_pin: GPIO32
  baud_rate: 9600
  id: projector

### TW9200 / TW8200 ###

# COMMANDS:

# "PWR ON\r\n" # power_on
# "PWR OFF\r\n" # power_off
# "KEY 20\r\n" # aspect_ratio
# "KEY 3F\r\n" # colour_mode
# "KEY 16\r\n" # enter / OK
# "KEY 05\r\n" # exit
# "KEY A5\r\n" # iris
# "KEY 8D\r\n" # interlace
# "KEY 03\r\n" # menu
# "KEY 36\r\n" # navigate_down
# "KEY 37\r\n" # navigate_left
# "KEY 38\r\n" # navigate_right
# "KEY 35\r\n" # navigte_up
# "KEY 4B\r\n" # pattern

One thing to note is that most (if not all) projectors will use real RS232 levels (+/- 12V) not the 0 to 3.3V TTL output from the ESP. Fortunately converter boards are cheap and easily available on ebay or aliexpress.

e.g. RS232 To TTL Converter Module Serial Module DB9 Connector 3.3V-5.5V Arduino、SEAU | eBay

EDIT: Wiring diagram:

EDIT 2: ESPHome now requires that services be enabled in Home Assistant.

https://esphome.io/components/api.html#configuration-variables

Screenshot 2024-03-10 at 12-39-04 Native API Component

15 Likes

That’s pretty clever! On my projector I just use a Z-Wave plug to be absolutely certain it cannot come on by itself (it’s happened a few times with power outages and the bulb was on for many hours before I noticed) and an RM Mini to send IR commands for the actual automation.

1 Like

Yikes! What model projector?

I’ve never noticed my projector come on after a power outage fortunately.

My main reason for creating this project was to rid myself of IR controls. And get some state feedback. Just waiting on a wifi board for my aircon and there will be no more IR control in my house.

I’m currently creating a switch for the projector (using the on/off scripts and state feedback from the trigger port). After hearing your story I will also create an automation that checks if the projector is on for too long without anything playing.

I have since replaced it but it was an InFocus projector with a $450 bulb and after it happened a couple of times I decided to incorporate a Z-Wave outlet device into the automation of turning on that projector. Now, with HA, I have also set up something that monitors the play state of the Apple TV in my theater and shuts everything down if there’s been no state change in an hour (so, double failsafe) as well as a forced power off of the projector power at 1am every day (another failsafe).

I think getting away from RF is an awesome idea and I love how you approached it. I have serial and USB controls on my new projector and might have to play with that now (thanks Tom, yet another project to add to the very long list, my wife will love that…). I never considered tapping into those ports this way but it is a super cool idea. After hacking pet feeders and other stuff with ESP’s this will be a fun one - and it doesn’t require cracking open the target device to trace and cut wires!

1 Like

I’m hearing you. My next project is to “smartify” my dehumidifiers. Crossing my fingers that the control board is 3.3V logic. :crossed_fingers:

A small breadboard, a couple resistors and you’re good to go - if you can find room to tuck it in there securely of course! Funny you mention the dehumidifier, I’ve been thinking of doing the same thing but on my humidifier (I live in a very arid climate) - and I know there is low voltage because it uses an LED light to light the tank, which is on by default unless toggled off, so bonus is I won’t have to toggle it off anymore when I fire up the humidifier.

I’ll solder it to Veroboard rather than using a breadboard - for vibration/shock security of the connections. Hoping I don’t have to use loads of optocouplers.

1 Like

Just set this up for a TV that can be controlled via RS232. Had the components already. Thanks for the good write up.

1 Like

I have a similar setup for an optoma projector using RS232 and smart power switch if this is useful to anyone…

1 Like

@tom_l
You got me thinking that mode: queued might be useful for your application (and wouldn’t need an input_boolean to serve as a “busy” flag).

If you create a script like this:

projector:
  mode: queued
  variables:
    commands:
      iris: "KEY A5\r\n"
      menu: "KEY 03\r\n"
  sequence:
    - service: esphome.projector_uart_write
      data:
        command: "{{ commands.get(command, 'ERROR\r\n') }}"
    - delay: 1

You can call it like this:

- service: script.projector
  data:
    command: 'iris'

Or like this if you want a non-blocking call:

    service: script.turn_on
    target:
      entity_id: script.projector
    data:
      variables:
        command: 'iris'

While the script is busy processing the ‘iris’ command, any subsequent calls to the same script are placed in a queue and will be processed in the order they’re received.

1 Like

Nice. I’ll do that. Thanks.

Though I don’t want to send anything in case the command is not found, so I’ll think of something for that.

Also this puts a spanner in the works:

projector_off:
  sequence:
  - condition: state
    entity_id: input_boolean.uart_busy
    state: 'off'
  - service: input_boolean.turn_on
    entity_id: input_boolean.uart_busy
  - service: esphome.projector_uart_write
    data:
      command: "PWR OFF\r\n"
  - delay: 10
  - service: input_boolean.turn_off
    entity_id: input_boolean.uart_busy

projector_on:
  sequence:
  - condition: state
    entity_id: input_boolean.uart_busy
    state: 'off'
  - service: input_boolean.turn_on
    entity_id: input_boolean.uart_busy
  - service: esphome.projector_uart_write
    data:
      command: "PWR ON\r\n"
  - delay: 40
  - service: input_boolean.turn_off
    entity_id: input_boolean.uart_busy

Theoretically I should not send another command after issuing the power command for 30 seconds, though in practice I’ve found that 10 works for off and 40 for on.

I’m going to move these posts to my project topic to prevent cluttering up this FR.

Improved version:

projector:
  mode: queued
  variables:
    commands:
      iris: 
        - "KEY A5\r\n"
        - 1
      menu:
        - "KEY 03\r\n"
        - 1
      on:
        - "PWR ON\r\n"
        - 40
      off:
        - "PWR OFF\r\n"
        - 10
    cmd: "{{ commands.get(command, none) }}"
  sequence:
    - condition: "{{ cmd is not none }}"
    - service: esphome.projector_uart_write
      data:
        command: "{{ cmd[0] }}"
    - delay: "{{ cmd[1] }}"
1 Like

Thanks for this writeup!

I’m going to try and adapt this to control my Samsung TV via its EXLink connector.

I am currently controlling it via ser2net, and a flask/rest container, Moving to a ESPHome device will help free up that RasPi.

I have been trying to get serial control to work with Home Assistant and I have somehow missed this post. Can a similar setup be used for RS485? And what would be the method to receive feedback?

You would need an RS485 to 3.3V TTL line driver / receiver. Then sending commands could be done the same way.

Not sure about the feedback. It would probably require a custom component.

1 Like

I’m unable to get this to work.

I dont care about the feedback and LED indicators at the moment. I just want to be able to send PWR ON for power on and PWR OFF for off, for the time being, preferably without any scripts etc if possible. Can someone help me simplify the esp code? I am unable to adapt the above for my simple use.

What strings to you have to send to your projector for the on and off commands?

Its an Epson EH-TW7100.

I found this document online https://files.support.epson.com/pdf/pltw1_/pltw1_cm.pdf

Based on that I think I need to send
PWR ON
PWR OFF

Then the simplest way would be with a template switch in ESPHome:

uart:
  tx_pin: GPIO12  # Change these settings as needed.
  rx_pin: GPIO32
  baud_rate: 9600
  id: projector_uart

switch:
  - platform: template
    name: "Projector Power"
    optimistic: true
    turn_on_action:
      - uart.write:
          id: projector_uart
          data: 'PWR ON\r\n'
    turn_off_action:
      - uart.write:
          id: projector_uart
          data: 'PWR OFF\r\n'
1 Like

Below is the uart section of my esphome

uart:
  tx_pin: GPIO17  
  rx_pin: GPIO16
  baud_rate: 9600
  id: projector_uart

switch:
  - platform: template
    name: "Projector Power"
    optimistic: true
    turn_on_action:
      - uart.write:
          id: projector_uart
          data: 'PWR ON\r\n'
    turn_off_action:
      - uart.write:
          id: projector_uart
          data: 'PWR OFF\r\n'

It creates a switch on the dashboard that I can turn on and off

My physical connections are as below. I even tried inverting the tx and rx connection on the 232 to TTL connector. But the projector does not respond to the commands :frowning: