Skip to content

Instantly share code, notes, and snippets.

@v6ak
Last active April 13, 2024 20:20
Show Gist options
  • Save v6ak/ed35492f0d1851a138e757d9b429a8ba to your computer and use it in GitHub Desktop.
Save v6ak/ed35492f0d1851a138e757d9b429a8ba to your computer and use it in GitHub Desktop.
ESPHome config for IKEA Vindrikting with LáskaKit v3 board
api_encryption_key: "don't expect me to publish it"
ota_key: "are you serious?"
wifi:
id: wifi_id
networks:
- ssid: "some wireless fiction network"
password: assword
- ssid: "some other wireless network"
password: foobar
on_connect:
- lambda: |-
const auto is_ha = id(wifi_id).wifi_ssid() == "wireless network with HA";
const auto reboot_timeout = is_ha ? (5*60*1000) : 0;
ESP_LOGD("wifi init", "is_ha: %d, reboot_timeout: %d", is_ha, reboot_timeout);
id(ha_api).set_reboot_timeout(reboot_timeout);
reboot_timeout: 0s # disable; already handled in API component
light_auto_on_condition:
lambda: 'return id(wifi_id).wifi_ssid() == "wireless network without HA";'
# a bit of inspiration from https://github.com/TataGEEK/IKEA-Vindriktning/blob/main/ikea-semafor.yaml
substitutions:
node_name: IKEA semaphore
sensor_fan: GPIO12
pm1006_pin_rx: GPIO16
pm1006_pin_tx: GPIO17
mhz19_pin_rx: GPIO22
mhz19_pin_tx: GPIO21
GPIO_RGB_LED: GPIO25
GPIO_IR_SENSOR: GPIO33 # for v3.0+
GPIO_BUZZER: GPIO2 # for v3.0+
esphome:
name: ikea-semaphore
#compile_process_limit: 1 seems to be useful on low-RAM devices; on high-RAM devices, it just slows down compilation
on_boot:
priority: -10
then:
- number.set:
id: orange_green
value: 215
- number.set:
id: orange_green_exponent
value: 4
- number.set:
id: fan_time_before_pm25
value: 10_000
- if:
condition: !secret light_auto_on_condition
then:
- light.turn_on:
id: led_rgb
brightness: 30%
effect: "Air status"
esp32:
board: esp32dev
framework:
type: esp-idf # IDF is recommended for better BLE proxy support
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
id: ha_api
ota:
password: !secret ota_key
wifi: !secret wifi
web_server:
port: 80
ota: false
local: true
esp32_ble_tracker:
scan_parameters:
interval: 1100ms
window: 1100ms
bluetooth_proxy:
active: true
cache_services: true # requires esp-idf
script:
- id: update_sensors_requiring_fan
then:
- if:
condition:
- lambda: 'return id(fan_control).state;'
then:
- logger.log: "Fan is ON"
- switch.turn_on: fan
- logger.log: "Waiting for fan and fun"
- delay: !lambda "return id(fan_time_before_pm25).state;"
- logger.log: "Requesting update from PM1006"
- uart.write:
id: uart_pm1006
data: [ 0x11, 0x02, 0x0B, 0x01, 0xE1 ]
# Usually, fan is turned off by sensor's on_value handler.
# However, when reading fails, we have a timeout.
- if:
condition:
- lambda: 'return id(fan_control).state;'
then:
# According to the PM1006 datasheet, it might take up to 8 seconds. It is usually immediate, but we'll be more generous:
- logger.log: "Waiting for timeout"
- delay: 10s
- if:
condition:
- lambda: 'return id(fan).state;'
then:
- logger.log:
format: "Fan is stil running after timeout, we'll disable it!"
level: WARN
- switch.turn_off: fan
else:
- logger.log: "Fan was turned of before timeout, which is fine."
switch:
- platform: template
id: fan_control
name: "${node_name} fan periodic"
optimistic: true
turn_off_action:
# prevent indefinite fan running when turned off during measurement
- switch.turn_off: fan
- platform: gpio
pin: $sensor_fan
id: fan
name: "${node_name} fan running"
internal: false
icon: mdi:fan
entity_category: config
- platform: template
id: fake_co2
name: "${node_name} Use fake CO2 for status"
optimistic: true
number:
- platform: template
id: fan_time_before_pm25
name: "${node_name} Fan time before PM2.5 measurement"
min_value: 0
max_value: 60000
step: 1000
optimistic: true
- platform: template
id: fake_co2_value
name: "${node_name} Fake CO2 value (if enabled)"
min_value: 300
max_value: 2000
step: 10
optimistic: true
# Orange color tuning
- platform: template
id: orange_green
name: "${node_name} orange: green component"
min_value: 0
max_value: 255
step: 1
optimistic: true
- platform: template
id: orange_green_exponent # how steep is transition from orange to red
name: "${node_name} orange: green component: exponent"
min_value: 1
max_value: 5
step: 1
optimistic: true
uart:
- id: uart_pm1006 # PM2.5
rx_pin: ${pm1006_pin_rx}
tx_pin: ${pm1006_pin_tx}
baud_rate: 9600
- id: uart_mhz19 # CO2
rx_pin: ${mhz19_pin_rx}
tx_pin: ${mhz19_pin_tx}
baud_rate: 9600
sensor:
- platform: pm1006
uart_id: uart_pm1006
# No automatic update. We'll request it manually to ensure proper sync with fan.
pm_2_5:
name: "${node_name} PM2.5"
id: pm2_5_value
internal: false
on_value:
# This should happen only when requested by update_sensors_requiring_fan script
then:
# record whether fan was running during measurement
- binary_sensor.template.publish:
id: pm1006_with_fan_running
state: !lambda 'return id(fan).state;'
# As this is currently the only sensor that requires fan, we no longer need the fan to run.
# When we add multiple fan-dependent sensors, this will need to be adjusted, so fan is disabled by the last result.
- if:
condition:
- lambda: 'return id(fan_control).state;'
then:
- logger.log: "Turning fan OFF after successful read"
- switch.turn_off: fan
- platform: wifi_signal
name: "${node_name} WiFi Signal"
entity_category: diagnostic
- platform: mhz19 # https://esphome.io/components/sensor/mhz19.html
id: co2_sensor
uart_id: uart_mhz19
co2:
name: "${node_name} CO2"
id: co2_value
temperature:
name: "${node_name} temperature"
update_interval: 60s
automatic_baseline_calibration: false # reportedly better for indoor
# TODO: something more useful than voltage reading
- platform: adc
pin: "${GPIO_IR_SENSOR}"
name: "${node_name} IR sensor raw"
update_interval: 60s
binary_sensor:
- platform: template
id: pm1006_with_fan_running
name: "${node_name} PM2.5 measured when fan was running"
- platform: homeassistant
name: "Want window open"
entity_id: input_boolean.want_window_open
id: want_window_open
- platform: homeassistant
name: "Curtains auto"
entity_id: input_boolean.curtains_auto
id: curtains_auto
- platform: homeassistant
name: "Sleep mode"
entity_id: input_boolean.sleep_mode
id: sleep_mode
- platform: homeassistant
name: "Room might be heated by ventilation"
entity_id: binary_sensor.room_might_be_heated_by_ventilation
id: heating_suspect
button:
- platform: restart
name: "${node_name} restart"
- platform: shutdown
name: "${node_name} shutdown"
- platform: template
name: "${node_name} CO2: Calibrate zero"
on_press:
- logger.log: "CO2: Zero calibration started"
- mhz19.calibrate_zero: co2_sensor
- logger.log: "CO2: Zero calibration finished"
- platform: template
name: "${node_name} buzz"
on_press:
- output.ledc.set_frequency:
id: buzzer_output
frequency: 1000Hz
- output.set_level:
id: buzzer_output
level: 50%
# various sounds from https://esphome.io/components/rtttl.html
- platform: template
name: "${node_name} siren"
on_press:
- rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e'
- platform: template
name: "${node_name} two short"
on_press:
- rtttl.play: 'two_short:d=4,o=5,b=100:16e6,16e6'
- platform: template
name: "${node_name} long"
on_press:
- rtttl.play: 'long:d=1,o=5,b=100:e6'
- platform: template
name: "${node_name} scale up"
on_press:
- rtttl.play: 'scale_up:d=32,o=5,b=100:c,c#,d#,e,f#,g#,a#,b'
- platform: template
name: "${node_name} Star Wars"
on_press:
- rtttl.play: 'star_wars:d=16,o=5,b=100:4e,4e,4e,8c,p,g,4e,8c,p,g,4e,4p,4b,4b,4b,8c6,p,g,4d#,8c,p,g,4e,8p'
- platform: template
name: "${node_name} Mission Impossible"
on_press:
- rtttl.play: 'mission_imp:d=16,o=6,b=95:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,a#5,8c,2p,32p,a#5,g5,2f#,32p,a#5,g5,2f,32p,a#5,g5,2e,d#,8d'
- platform: template
name: "${node_name} Mario"
on_press:
- rtttl.play: 'mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6'
- platform: template
name: "${node_name} stop ringing"
on_press:
- rtttl.stop
rtttl:
output: buzzer_output
id: ringer
gain: 60%
light:
# FIXME: top LED sometimes enables blue subpixel, not sure why; it seems that max_refresh_rate can cure it
- id: led_rgb # used to briefly flash on boot, but stopped, maybe after adding individual lights…
internal: False
platform: esp32_rmt_led_strip
rgb_order: GRB
chipset: WS2812
pin: $GPIO_RGB_LED
num_leds: 3
rmt_channel: 6 # Don't know why 6, and it probably doesn't matter…
#max_refresh_rate: 950ms # might help with color shift issues; 900ms isn't enough, 1000ms seems enough
max_refresh_rate: 100ms # or maybe max_refresh_rate should equal to update_interval?
name: "${node_name} RGB LED indicator"
effects:
- random:
- pulse:
- addressable_rainbow:
- addressable_color_wipe:
- addressable_scan:
- addressable_twinkle:
- addressable_random_twinkle:
- addressable_fireworks:
- addressable_flicker:
- addressable_lambda:
name: "Air status"
#update_interval: 1s
update_interval: 100ms
lambda: |-
if (id(sleep_mode).state) {
it.all() = Color::BLACK;
return;
}
const float co2 = id(fake_co2).state ? id(fake_co2_value).state : id(co2_value).state;
const float pm25 = id(pm2_5_value).state;
const int ha_connected = id(ha_api).is_connected();
//ESP_LOGD("air status", "co2: % 7.2f pm25: % 7.2f", co2, pm25);
const bool heating_warning = (co2 < 420) && id(heating_suspect).state;
static bool heating_warning_previous = false;
static bool heating_warning_led = false;
if (heating_warning) {
// blink on every update
heating_warning_led = heating_warning_previous ? !heating_warning_led : true;
}
heating_warning_previous = heating_warning;
const int ogrc = id(orange_green).state;
const int ogrex = id(orange_green_exponent).state;
// top: CO2 smooth indicator with sudden break at 1000 ppm (=ventilation suggested)
it.get(2) =
isnan(co2)
? Color::BLACK
:
Color(
/* red = */ min(255, max(0, (int)((co2 - 400.0) / 600.0 * 255.0))),
/* green = */
(co2 > 1000)
? (ogrc - min(ogrc, max(0, (int)(pow((co2 - 1000.0) / 500.0, ogrex) * ogrc))))
: 255,
/* blue = */ 0
);
// middle: CO2 very low / very high or warning about temporary curtain settings
it.get(1) =
( heating_warning
? (heating_warning_led ? Color(255, 255, 0) : Color(0, 255, 0))
: ((co2 < 420)
? Color(0, 255, 0)
: ((co2 > 1500)
? Color(255, 0, 0)
: ((ha_connected && (!id(curtains_auto).state || id(want_window_open).state))
? Color(255, ogrc, 0)
: /* default*/ Color::BLACK
)
)));
// bottom: PM25
it.get(0) =
isnan(pm25)
? Color::BLACK
:
Color(
/* red = */ min(255, max(0, (int)((pm25 - 5.0) / 15.0 * 255.0))),
/* green = */ (pm25 > 20) ? 0 : 255,
/* blue = */ 0
);
//ESP_LOGD("air status", "top: RGB(%3d, %3d, %3d)", it.get(2).get_red(), it.get(2).get_green(), it.get(2).get_blue());
//ESP_LOGD("air status", "middle: RGB(%3d, %3d, %3d)", it.get(1).get_red(), it.get(1).get_green(), it.get(1).get_blue());
//ESP_LOGD("air status", "bottom: RGB(%3d, %3d, %3d)", it.get(0).get_red(), it.get(0).get_green(), it.get(0).get_blue());
- platform: partition
name: "${node_name} LED bottom"
segments:
- id: led_rgb
from: 0
to: 0
- platform: partition
name: "${node_name} LED middle"
segments:
- id: led_rgb
from: 1
to: 1
- platform: partition
name: "${node_name} LED top"
segments:
- id: led_rgb
from: 2
to: 2
output:
- platform: ledc
pin: "${GPIO_BUZZER}"
id: buzzer_output
interval:
- interval: 1min
then:
- script.execute: update_sensors_requiring_fan
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment