Last active
April 13, 2024 20:20
-
-
Save v6ak/ed35492f0d1851a138e757d9b429a8ba to your computer and use it in GitHub Desktop.
ESPHome config for IKEA Vindrikting with LáskaKit v3 board
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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";' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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