Ever since I’ve been working from home, I’ve wanted an indicator light outside my office door to let my wife and kids know whether they can bother me:
- Green: Come in
- Yellow: In a meeting, but on mute. Come in if it’s urgent but be quick.
- Red: In a meeting; either not on mute or webcam is active. Don’t bother me.
My requirements were that it needed to be mains powered (I didn’t want to change batteries), it needed to be locally operated (I avoid cloud devices where possible), and it needed to be automated. In order to automate it, I had to have something running on my work laptop to monitor meeting, mute, and camera status. Unfortunately my work laptop is often connected through a VPN and I also don’t have admin access to install programs. Thus began a many-months-long process to implement a solution.
Status Light: I spent a lot of time looking for an indicator, and I first came across an Aqara M1 hub which looked nice but I wasn’t sure about ignoring/disabling all the features to use only the light functionality. Right before I pulled the trigger, I happened across the Globe Electric Smart Ambient light which was a bit cheaper and had only the functionality I desired. Once I received it, I tried to flash local-Tuya but discovered it wasn’t compatible. (2023 edit: this is now supported and you can flash it with ESPHome without opening it up. See Digiblur’s walkthrough.) So as any normal person would do, instead of opting for cloud control, I took a hammer and prybar to it.
Turns out it had a WB3S chip which is pin-for-pin compatible with an ESP12, so that’s the road I went down. Kid’s play-doh worked out well as a heat shield / heat sink for the heat gun.
Here’s my awful soldering job, and with the 10k pull-up for the enable pin (on the top of the chip), and 10k pull-down to GPIO15 (on the bottom side):
Below is a simplified version of the ESPhome code I used; this enables the basic functionality of the light, motion sensor, and button. This light is actually a RGBWW light where you can change the white color temperature because there are both warm and cool white LEDs, but mine had a failed transistor for the green color circuit so my light had the green LEDs illuminated all the time. I think I must have ruined it somehow during my ESP12 installation. Anyway, to fix it, I took the transistor from the warm LED circuit. So now my warm LEDs are non-functional. And that’s why my code doesn’t utilize the warm LED output. If someone replicates this, you can use a RGBWW platform in the code below instead.
substitutions:
# Device Naming
devicename: office-status-light
friendly_name: Office Status Light
device_description: Status LED, Night Light, Motion Detector
esphome:
name: $devicename
comment: ${device_description}
esp8266:
board: esp01_1m
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
password: "some_complicated_password"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: $devicename
password: "some_other_complicated_password"
captive_portal:
web_server:
port: 80
light:
- platform: rgbw
name: ${friendly_name}
id: rgbw_led
red: red_output
green: green_output
blue: blue_output
white: cool_output
restore_mode: ALWAYS_OFF
color_interlock: true
gamma_correct: 1.0
output:
- platform: esp8266_pwm
id: red_output
pin: GPIO4
max_power: 100%
- platform: esp8266_pwm
id: green_output
pin: GPIO12
max_power: 15%
- platform: esp8266_pwm
id: blue_output
pin: GPIO14
max_power: 15%
- platform: esp8266_pwm
id: warm_output
pin: GPIO13
- platform: esp8266_pwm
id: cool_output
pin: GPIO5
binary_sensor:
- platform: gpio
name: ${friendly_name} Motion
pin:
number: GPIO16
mode:
input: true
- platform: gpio
name: ${friendly_name} Button
pin:
number: GPIO0
mode:
input: true
inverted: true
Laptop Script:
Once I got that working, the next step was the code running on my laptop to push meeting/mute/camera status to Home Assistant. This was where I ran into most of my troubles. I won’t detail them in order to save you from boredom. Suffice to say that I made an autohotkey script to POST to HA RestAPI, then changed to a python script to push out to MQTT, then changed to a python script to POST to webhook, then changed to a python script to push to an AWS Lambda function which relays the request to a HA webhook. For most people, any of these methods would work. For my locked-down work laptop, I had to send out to the AWS Lambda function because my own HA instance was blocked by my work firewall. (I even bought a domain and that was blocked shortly thereafter.)
Here’s the simplified version of code that I ended up with:
Python GIST
This code uses a different URL for POST’ing the data to AWS after each POST attempt. I found I had better success with HTTP errors, and I don’t know if that’s due to the work firewall or what. This code can be easily modified to POST directly to a homeassistant webhook, and if anyone uses it I suspect that’s what you’ll want to do. So I’m not going to share the AWS Lambda function code or explain how I set that up.
Home Assistant Automation Webhook:
For the webhook, I set up 3 helper toggles: one for the status of each ‘thing’ (meeting, mute, camera). Then I created this automation, which takes the webhook data and updates each helper toggle:
alias: Laptop Webhook
description: ''
trigger:
- platform: webhook
webhook_id: mylaptop-webhook-some-unguessable-random-code-here
condition: []
action:
- if:
- condition: template
value_template: >-
{{ states('input_boolean.mylaptop_mute_status') !=
trigger.json.mute_status }}
then:
- service: input_boolean.toggle
data: {}
target:
entity_id: input_boolean.mylaptop_mute_status
- if:
- condition: template
value_template: >-
{{ states('input_boolean.mylaptop_meeting_status') !=
trigger.json.meeting_status }}
then:
- service: input_boolean.toggle
data: {}
target:
entity_id: input_boolean.mylaptop_meeting_status
- if:
- condition: template
value_template: >-
{{ states('input_boolean.mylaptop_camera_status') !=
trigger.json.camera_status }}
then:
- service: input_boolean.toggle
data: {}
target:
entity_id: input_boolean.mylaptop_camera_status
mode: restart
Home Assistant Automation for Indicator Color:
Then, all that was left was to create an automation to pick the correct color based on those status toggles. I also added a toggle for a night light; so I could turn that toggle on whenever I want the night light to be on (but it won’t override the status light color, in case I’m working late).
alias: Office Status Light
description: Change the color of the office status light based on helper toggle status
trigger:
- platform: state
entity_id:
- input_boolean.mylaptop_meeting_status
- input_boolean.mylaptop_camera_status
- input_boolean.mylaptop_mute_status
- input_boolean.hallway_nightlight
- device_tracker.mylaptop_ethernet
- device_tracker.mylaptop_wifi
- platform: time
at: '07:00:00'
- platform: time
at: '17:30:00'
condition:
- condition: or
conditions:
- condition: not
conditions:
- condition: state
entity_id: device_tracker.mylaptop_wifi
state: not_home
- condition: not
conditions:
- condition: state
entity_id: device_tracker.mylaptop_ethernet
state: not_home
action:
- choose:
- conditions:
- condition: state
entity_id: input_boolean.mylaptop_camera_status
state: 'on'
sequence:
- service: light.turn_on
data:
rgb_color:
- 255
- 0
- 0
brightness_pct: 100
target:
entity_id: light.office_status_light
- conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.mylaptop_meeting_status
state: 'on'
- condition: state
entity_id: input_boolean.mylaptop_mute_status
state: 'off'
sequence:
- service: light.turn_on
data:
rgb_color:
- 255
- 0
- 0
brightness_pct: 100
target:
entity_id: light.office_status_light
- conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.mylaptop_meeting_status
state: 'on'
- condition: state
entity_id: input_boolean.mylaptop_mute_status
state: 'on'
sequence:
- service: light.turn_on
data:
rgb_color:
- 255
- 255
- 0
brightness_pct: 100
target:
entity_id: light.office_status_light
- conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.mylaptop_camera_status
state: 'off'
- condition: state
entity_id: input_boolean.mylaptop_meeting_status
state: 'off'
- condition: time
before: '17:30:00'
after: '06:00:00'
weekday:
- sun
- sat
- fri
- thu
- wed
- tue
- mon
- condition: state
entity_id: binary_sensor.workday
state: 'on'
sequence:
- service: light.turn_on
data:
rgb_color:
- 0
- 255
- 0
brightness_pct: 100
target:
entity_id: light.office_status_light
- conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.mylaptop_camera_status
state: 'off'
- condition: state
entity_id: input_boolean.mylaptop_meeting_status
state: 'off'
- condition: state
entity_id: input_boolean.hallway_nightlight
state: 'on'
sequence:
- service: light.turn_on
data:
white: 255
target:
entity_id: light.office_status_light
- conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.mylaptop_camera_status
state: 'off'
- condition: state
entity_id: input_boolean.mylaptop_meeting_status
state: 'off'
- condition: state
entity_id: input_boolean.hallway_nightlight
state: 'off'
sequence:
- service: light.turn_off
target:
entity_id: light.office_status_light
data: {}
default: []
mode: restart
Final result: Don’t Enter!