https://github.com/foggyhlw/fish_tank_controller_eink
esphome:
name: fishtank-controller-epaper-demo
friendly_name: fishtank-controller-epaper-demo
on_boot:
then:
- light.turn_on:
id: ws2812_light
brightness: 20%
red: 0%
green: 0%
blue: 100%
globals:
- id: pump_state
type: int
restore_value: no
initial_value: '1'
- id: heater_control_value
type: int
restore_value: yes
initial_value: '32'
- id: timer_enable
type: bool
restore_value: yes
initial_value: 'false'
- id: timer_start_time
type: int
restore_value: yes
initial_value: '0'
- id: timer_stop_time
type: int
restore_value: yes
initial_value: '0'
- id: temperature_offset
type: float
restore_value: yes
initial_value: '0'
esp32:
board: nodemcu-32s
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "G6juwO2HDX8VubqtigqtFgcspp+7ehWTtTXF+nkKpB4="
reboot_timeout: 0s
# services:
# - service: play_rtttl
# variables:
# song_str: string
# then:
# - rtttl.play:
# rtttl: !lambda 'return song_str;'
ota:
- platform: esphome
password: "your_ota_password"
wifi:
ssid: "your_ssid"
password: "your_password"
id: fish_tank_controller_wifi
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Fish-Tank-Epaper-Demo"
password: "123456798"
captive_portal:
font:
- file: 'fonts/arial.ttf'
id: font_en_16
size: 16
- file: 'fonts/arial.ttf'
id: font_en_10
size: 10
- file: 'fonts/arial.ttf'
id: font_en_12
size: 12
- file: 'fonts/arial.ttf'
id: font_en_44
size: 44
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font_icon_24
size: 24
glyphs: [
"\U000f03c8", # water-temperature
"\U000f1402", # pump
"\U000f06e9", # light
"\U000f0210", # fan
# "\U000f051b", # clock-on
# "\U000f051e", # clock-off
]
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font_icon_20
size: 20
glyphs: [
"\U000f03c8", # water-temperature
"\U000f1402", # pump
"\U000f06e9", # light
"\U000f0210", # fan-on
"\U000f081d", # fan-off
"\U000f051b", # clock-on
"\U000f051e", # clock-off
"\U000f05a9", # wifi-on
"\U000f05aa", # wifi-off
]
- file: 'fonts/SourceHanSansSC-Light.otf'
id: font_cn_16
size: 16
glyphs: [
"温","度","水","泵","开","关","灯","光","定","时","返","回","加","热","棒","间","冷","气","小","调","速",
"1","2","3","4","5","6","7","8","9","0","年","月","日","控","制","喂","食","中","器","曲","线","补","尝",
"<",">","+","=","°","C","(",")"," ",".",":","-","限","鱼","缸","f","o","g","y","h","l","w","%","/","[","]",
]
- file: 'fonts/SourceHanSansSC-Light.otf'
id: font_cn_15
size: 15
glyphs: [
"温","度","水","泵","开","关","灯","光","定","时","返","回","加","热","棒","间","冷","气","小","调","速",
"1","2","3","4","5","6","7","8","9","0","年","月","日","控","制","喂","食","中","器","曲","线","补","偿",
"<",">","+","=","°","C","(",")"," ",".",":","-","限","鱼","缸","f","o","g","y","h","l","w","%","/","[","]",
]
- file: 'fonts/SourceHanSansSC-Normal.otf'
id: font_cn_44
size: 44
glyphs: [
"扫","码","关","注",
]
time:
- platform: sntp
id: sntp_time
timezone: Asia/Shanghai
servers:
- 0.ntp.ntsc.ac.cn
- 1.cn.ntp.org.cn
- 2.pool.ntp.org
on_time: #check every hour if time is the start/stop time
- seconds: 0
minutes: 0
hours: /1
then:
if:
condition:
and:
- lambda: 'return id(sntp_time).now().hour == id(timer_start_time);'
- lambda: 'return id(timer_start_time) < id(timer_stop_time);'
- lambda: 'return id(timer_enable);'
then:
switch.turn_on: tank_light
- seconds: 0
minutes: 0
hours: /1
then:
if:
condition:
and:
- lambda: 'return id(sntp_time).now().hour == id(timer_stop_time);'
- lambda: 'return id(timer_start_time) < id(timer_stop_time);'
- lambda: 'return id(timer_enable);'
then:
switch.turn_off: tank_light
one_wire:
- platform: gpio
pin: 4
sensor:
- platform: dallas_temp
accuracy_decimals: 1
# index: 0
name: 'fish tank temperature'
id: fish_tank_temp
update_interval: 10s
on_value:
then:
- if:
condition:
lambda: 'return x > id(heater_control_value);'
then:
- switch.turn_off: tank_heater
# - switch.turn_on: tank_fan
- light.turn_on:
id: ws2812_light
brightness: 100%
red: 100%
green: 0%
blue: 0%
- rtttl.play: 'two_short:d=4,o=5,b=100:16e6,16e6'
- if:
condition:
lambda: 'return x < id(heater_control_value)-1;'
then:
- switch.turn_on: tank_heater
# - switch.turn_off: tank_fan
- light.turn_on:
id: ws2812_light
brightness: 20%
red: 0%
green: 0%
blue: 100%
- platform: rotary_encoder
name: "Rotary Encoder"
pin_a: 35
pin_b: 34
resolution: 1
on_clockwise:
then:
# - rtttl.play: 'rotate:d=16,o=5,b=240:16e6;'
- if:
condition:
display_menu.is_active: my_graphical_display_menu
then:
- display_menu.up:
else:
- display.page.show_previous: my_epaper
- component.update: my_epaper
- script.execute: menu_timeout
on_anticlockwise:
then:
# - rtttl.play: 'rotate:d=16,o=5,b=240:16e6;'
- if:
condition:
display_menu.is_active: my_graphical_display_menu
then:
- display_menu.down:
else:
- display.page.show_next: my_epaper
- component.update: my_epaper
- script.execute: menu_timeout
- platform: template
id: time_hour
name: Time hour
lambda: |-
auto time = id(sntp_time).now();
return int(time.hour);
switch:
- platform: gpio
pin: 27
name: "tank pump"
id: tank_pump
restore_mode: ALWAYS_ON
on_turn_off:
- script.execute: pump_off_timeout
- platform: gpio
pin: 32
name: "tank heater"
id: tank_heater
restore_mode: ALWAYS_ON
- platform: gpio
pin: 33
name: "tank light"
id: tank_light
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 25
name: "tank fan"
id: tank_fan
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 26
name: "tank air pump"
id: tank_air_pump
restore_mode: ALWAYS_OFF
binary_sensor:
- platform: gpio
filters:
- invert:
name: "feed button"
id: feed_button
pin:
number: 22
mode:
input: true
on_click:
then:
- if:
condition:
lambda: 'return id(pump_state) == 1;'
then:
- switch.turn_off: tank_pump
- lambda: 'id(pump_state) = 2;'
- component.update: my_epaper
- delay: 10min
- switch.turn_on: tank_pump
- lambda: 'id(pump_state) = 1;'
- if:
condition:
lambda: 'return id(pump_state) == 2;'
then:
- switch.turn_on: tank_pump
- lambda: 'id(pump_state) = 1;'
- component.update: my_epaper
# - lambda: 'ESP_LOGI("pump_state","pump_state: %d", id(pump_state));'
- platform: gpio
filters:
- invert:
name: "light button"
id: light_button
pin:
number: 15
mode:
input: true
on_click:
then:
- switch.toggle: tank_light
- component.update: my_epaper
- platform: gpio
filters:
- invert:
name: "fan button"
id: fan_button
pin:
number: 36
mode:
input: true
on_click:
then:
- switch.toggle: tank_fan
- component.update: my_epaper
- platform: gpio
filters:
- invert:
name: "air pump button"
id: air_pump_button
pin:
number: 13
mode:
input: true
on_click:
then:
- switch.toggle: tank_air_pump
- component.update: my_epaper
- platform: gpio
id: rotary_button
pin: 39
filters:
- delayed_on: 50ms
- delayed_off: 50ms
on_press:
then:
# - rtttl.play: 'rotary_press:d=16,o=5,b=240:16e,16e6;'
- if:
condition:
display_menu.is_active: my_graphical_display_menu
then:
- display_menu.enter: my_graphical_display_menu
- script.execute: menu_timeout
else:
- display_menu.show: my_graphical_display_menu
- script.execute: menu_timeout
spi:
clk_pin: 16
mosi_pin: 17
display:
- platform: waveshare_epaper
id: my_epaper
cs_pin: 19
dc_pin: 18
busy_pin: 21
reset_pin: 5
model: 2.90in #296x128
full_update_every: 600
rotation: 270
update_interval: 5s
pages:
- id: main_page
lambda: |-
it.line(0, 21, 296, 21);
it.line(88, 22, 88, 127);
it.strftime(35, 0, id(font_cn_16), "foggyhlw鱼缸控制器 %H:%M", id(sntp_time).now());
if(id(timer_enable)){
if( id(timer_start_time) < id(timer_stop_time) ){
it.printf(250,1, id(font_icon_20),"\U000f051b");
}
if( id(timer_start_time) >= id(timer_stop_time) ){
it.printf(250,1, id(font_icon_20),"\U000f051e");
}
}
else{
it.printf(250,1, id(font_icon_20),"\U000f051e");
}
if(id(fish_tank_controller_wifi).is_connected()){
it.printf(270,1, id(font_icon_20),"\U000f05a9");
}
else{
it.printf(270,1, id(font_icon_20),"\U000f05aa");
}
it.printf(0,27, id(font_cn_16)," 水泵 --");
if(id(tank_pump).state == 1){
it.printf(60,27, id(font_cn_16),"开");
}
if(id(tank_pump).state == 0){
if(id(pump_state) == 1){
it.printf(60,27, id(font_cn_16),"关");
}
if(id(pump_state) == 2){
it.printf(60,27, id(font_cn_16),"喂");
}
}
it.printf(0,52,id(font_cn_16)," 加热 --");
if(id(tank_heater).state){
it.printf(60,52, id(font_cn_16),"开");
}
else{
it.printf(60,52, id(font_cn_16),"关");
}
it.printf(0,77,id(font_cn_16)," 灯光 --");
if(id(tank_light).state){
it.printf(60,77, id(font_cn_16),"开");
}
else{
it.printf(60,77, id(font_cn_16),"关");
}
it.printf(0,102,id(font_cn_16)," 气泵 -- ");
if(id(tank_air_pump).state){
it.printf(60,102, id(font_cn_16),"开");
}
else{
it.printf(60,102, id(font_cn_16),"关");
}
if(id(tank_fan).state){
it.printf(270,42, id(font_icon_20),"\U000f0210");
}
else{
it.printf(270,42, id(font_icon_20), "\U000f081d");
}
it.printf(110,35,id(font_icon_24),"\U000f03c8");
it.printf(143,22,id(font_en_44),"%.1f",id(fish_tank_temp).state+id(temperature_offset));
it.printf(240,23,id(font_en_16),"°C");
it.printf(240,43,id(font_en_16),"%d",id(heater_control_value));
it.rectangle(235, 42, 30, 20);
it.graph(112, 68, id(temperature_graph_1h));
it.printf(275,110, id(font_en_10),"16°");
it.printf(275,90, id(font_en_10),"24°");
it.printf(275,70, id(font_en_10),"32°");
it.printf(95,110, id(font_en_10),"1h");
- id: graph_page
lambda: |-
it.line(0, 21, 296, 21);
it.printf(95,2,id(font_cn_16),"10小时温度曲线");
it.graph(25, 30, id(temperature_graph_10h));
it.printf(272,88, id(font_en_10),"20");
it.printf(272,72, id(font_en_10),"24");
it.printf(272,56, id(font_en_10),"28");
it.printf(272,40, id(font_en_10),"32");
it.printf(270,24, id(font_en_12),"°C");
it.printf(253,110, id(font_en_10),"%d:00",id(sntp_time).now().hour);
it.printf(205,110, id(font_en_10),"%d:00",id(sntp_time).now().hour-2);
it.printf(157,110, id(font_en_10),"%d:00",id(sntp_time).now().hour-4);
it.printf(109,110, id(font_en_10),"%d:00",id(sntp_time).now().hour-6);
it.printf(61,110, id(font_en_10),"%d:00",id(sntp_time).now().hour-8);
it.printf(13,110, id(font_en_10),"%d:00",id(sntp_time).now().hour-10);
- id: info_page
lambda: |-
it.qr_code(170, 5, id(foggyhlw_link), Color(255,255,255), 4);
it.printf(20, 20, id(font_cn_44), "扫码");
it.printf(20, 65, id(font_cn_44), "关注");
graphical_display_menu:
id: my_graphical_display_menu
display: my_epaper
font: font_cn_15
on_redraw:
then:
component.update: my_epaper
active: false
mode: rotary
menu_item_value: !lambda |-
// Will render your menu item value as "My menu label ~my value here~"" normally and "My menu label *my value here*" when in edit mode
std::string label = " ";
if (it->is_item_selected && it->is_menu_editing) {
label.append("< ");
label.append(it->item->get_value_text());
label.append(" >");
} else {
label.append("[ ");
label.append(it->item->get_value_text());
label.append(" ]");
}
return label;
items:
- type: command
text: ' 返回'
on_value:
then:
- display_menu.hide:
- type: number
immediate_edit: false
text: ' 1.温控限制 °C'
format: '%.0f'
number: maxmuim_temperature
# on_value:
# then:
# lambda: 'id(heater_control_value) = it->get_text().c_str();'
- type: number
immediate_edit: false
text: ' 2.温度补偿 °C'
format: '%.1f'
number: temperature_offset_range
- type: number
immediate_edit: true
text: ' 3.调光/调速 %'
format: '%.0f'
number: pwm_duty
on_value:
then:
- light.turn_on:
id: pwm_out
brightness: !lambda |-
return atoi(it->get_value_text().c_str()) / 100.0;
- type: custom
text: ' 4.灯光定时'
immediate_edit: true
value_lambda: |-
if(id(timer_enable)){
return "开";
}
else{
return "关";
}
on_value:
then:
- lambda: "id(timer_enable) = !id(timer_enable);"
- type: number
immediate_edit: false
text: ' - 开灯时间'
format: '%.0f'
number: start_time
- type: number
immediate_edit: false
text: ' - 关灯时间'
format: '%.0f'
number: stop_time
number:
- platform: template
id: maxmuim_temperature
optimistic: true
min_value: 26
max_value: 34
initial_value: 32
restore_value: true
step: 1
on_value:
then:
lambda: 'id(heater_control_value) = x;'
- platform: template
id: temperature_offset_range
optimistic: true
min_value: -2
max_value: 2
step: 0.1
initial_value: 0
restore_value: true
on_value:
then:
lambda: 'id(temperature_offset) = x;'
- platform: template
id: start_time
optimistic: true
min_value: 0
max_value: 23
restore_value: true
step: 1
on_value:
then:
lambda: 'id(timer_start_time) = x;'
- platform: template
id: stop_time
optimistic: true
restore_value: true
min_value: 0
max_value: 23
step: 1
on_value:
then:
lambda: 'id(timer_stop_time) = x;'
- platform: template
id: pwm_duty
optimistic: true
min_value: 0
max_value: 100
restore_value: true
step: 20
# on_value:
# then:
# - lambda: 'ESP_LOGI("pwm_duty: ","%d", x);'
graph:
# Show bare-minimum auto-ranged graph
- id: temperature_graph_1h
sensor: fish_tank_temp
duration: 1h
x_grid: 10min
y_grid: 4.0 # degC/div
width: 161
height: 51
max_value: 34
min_value: 16
- id: temperature_graph_10h
sensor: fish_tank_temp
duration: 10h
x_grid: 1h
y_grid: 4.0 # degC/div
width: 241
height: 81
max_value: 34
min_value: 16
qr_code:
- id: foggyhlw_link
value: https://space.bilibili.com/305218804
script:
- id: menu_timeout
mode: restart
then:
- delay: 40s
- display.page.show: main_page
- display_menu.hide:
- id: pump_off_timeout
mode: restart
then:
- delay: 1h
- switch.turn_on: tank_pump
output:
- platform: ledc
pin: 14
id: pwm_output
frequency: 100
channel: 0
- platform: ledc
pin: 02
id: rtttl_output
channel: 2
# # Example usage in a light
light:
- platform: monochromatic
id: pwm_out
output: pwm_output
name: "PWM Light"
- platform: neopixelbus
id: ws2812_light
type: GRB
variant: WS2812
pin: 23
num_leds: 1
name: "NeoPixel Light"
rtttl:
output: rtttl_output
on_finished_playback:
- logger.log: 'Song ended!'