!/bin/bash

set -e

PROJECT_DIR="/home/ykdbc/smarttest"
MYSQL_USER="ykdbc"
MYSQL_PASS="Xy@528848"
MYSQL_DB="smarttestejyy"
DB_SQL="${PROJECT_DIR}/db-sql/db.sql"

echo "=== 更新系统并安装依赖 ==="
sudo apt update -y
sudo apt upgrade -y
sudo apt install -y curl git build-essential nginx mysql-server redis-server

echo "=== 安装 Node.js v16.15.0 ==="
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g n
sudo n 16.15.0
hash -r
node -v
npm -v

echo "=== 配置 MySQL 用户和数据库 ==="
sudo systemctl enable mysql
sudo systemctl start mysql
sudo mysql -e "CREATE DATABASE IF NOT EXISTS ${MYSQL_DB} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'localhost' IDENTIFIED BY '${MYSQL_PASS}';"
sudo mysql -e "GRANT ALL PRIVILEGES ON ${MYSQL_DB}.* TO '${MYSQL_USER}'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;"

if [ -f "$DB_SQL" ]; then
echo "=== 导入初始化 SQL 数据 ==="
sudo mysql -u${MYSQL_USER} -p${MYSQL_PASS} ${MYSQL_DB} < "$DB_SQL"
else
echo "⚠️ 未找到 $DB_SQL,跳过 SQL 导入"
fi

echo "=== 安装并编译 api-server ==="
cd ${PROJECT_DIR}/api-server
npm install
npm run dist
echo "=== 启动 api-server 进程守护 ==="
sudo npm install -g pm2
cd ${PROJECT_DIR}/api-server/dist
pm2 start ejyy_server.js --name ejyy-api-server
pm2 save
pm2 startup systemd -u $USER --hp $HOME

echo "=== 配置 Nginx 目录结构 ==="
sudo mkdir -p /wwwdata/api_dir
sudo mkdir -p /wwwdata/admin_web
sudo mkdir -p /wwwdata/console_web

echo "=== 编译并部署 admin-web ==="
cd ${PROJECT_DIR}/admin-web
npm install
npm run dist
sudo cp -r dist/* /wwwdata/admin_web/

echo "=== 编译并部署 console-web ==="
cd ${PROJECT_DIR}/console-web
npm install
npm run build
sudo cp -r dist/* /wwwdata/console_web/

echo "=== 配置 Nginx ==="
NGINX_CONF="/etc/nginx/sites-available/smarttest"
sudo tee $NGINX_CONF > /dev/null <<EOF
server {

listen 80;
server_name _;

location /api/ {
    proxy_pass http://127.0.0.1:8888/;   # 修改为 api-server 的实际端口
    proxy_http_version 1.1;
    proxy_set_header Upgrade \$http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host \$host;
    proxy_cache_bypass \$http_upgrade;
}

location /admin/ {
    root /wwwdata/admin_web;
    index index.html;
    try_files \$uri /index.html;
}

location /console/ {
    root /wwwdata/console_web;
    index index.html;
    try_files \$uri /index.html;
}

}
EOF

sudo ln -sf $NGINX_CONF /etc/nginx/sites-enabled/smarttest
sudo nginx -t
sudo systemctl restart nginx

echo "=== 部署完成! ==="
echo "管理后台: http://<服务器IP>/admin"
echo "物业中台: http://<服务器IP>/console"
echo "API 地址: http://<服务器IP>/api"

ubantu 24.4.2

1️⃣ 更新系统

sudo apt update && sudo apt upgrade -y

2️⃣Node 使用 16.15.0 版本(用此代码一键安装)

# 一键安装 Node.js 16.15.0
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.6/install.sh | bash && \
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" && \
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \
nvm install 16.15.0 && \
nvm alias default 16.15.0 && \
echo "Node $(node -v) 和 npm $(npm -v) 安装完成并设置为默认版本"

✅ 说明:

1.这个命令会先安装 nvm,再用 nvm 安装 Node.js 16.15.0。

2.安装完成后,node -v 会显示 v16.15.0,npm -v 会显示对应 npm 版本。

3.以后可以用 nvm use 切换 Node 版本。

3️⃣

安装依赖


    cd /home/ykdbc/smarttest/api-server

    npm install

编译代码


    npm run dist

4️⃣
> pm2 推荐作为进程守护,相关文档请查看官方文档

##### 安装 pm2
npm install -g pm2

##### 启动守护
cd /home/ykdbc/smarttest/api-server/dist

pm2 start ejyy_server.js --name ejyy-api-server

5️⃣

cd /home/ykdbc/smarttest/admin-web

npm install
npm config set strict-ssl false  #防止网站证书过期拉取失败
npm config set strict-ssl true   #继续检查证书
npm run dist


6️⃣配置docker代理
sudo mkdir -p /etc/systemd/system/docker.service.d #建文件夹
sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf# 建文件

文件内容

[Service]
Environment="HTTP_PROXY=socks5://ykdbc:Xy@[email protected]:11185"
Environment="HTTPS_PROXY=socks5://ykdbc:Xy@[email protected]:11185"
Environment="ALL_PROXY=socks5://ykdbc:Xy@[email protected]:11185"

7️⃣提升权限

chmod +x deploy.sh

8️⃣运行脚本

sudo ./deploy.sh

查看Mp2 进程
pm2 list

方法 1:使用 sudo 提权

sudo mkdir -p /wwwdata/api_dir
sudo mkdir -p /wwwdata/admin_web
sudo mkdir -p /wwwdata/console_web

如果需要,也可以把当前用户 ykdbc 设为该目录的拥有者:

sudo chown -R ykdbc:ykdbc /wwwdata/api_dir
sudo chown -R ykdbc:ykdbc /wwwdata/admin_web
sudo chown -R ykdbc:ykdbc /wwwdata/console_web

这样以后就不用每次都用 sudo。
cd /home/ykdbc/smarttest/api-server
cp -r dist/* /wwwdata/api_dir
cd /home/ykdbc/smarttest/admin_web
cp -r dist/* /wwwdata/admin_web
cd /home/ykdbc/smarttest/console-web
cp -r dist/* /wwwdata/console_web

编辑nginx
sudo chown -R ykdbc:ykdbc /etc/nginx/sites-available/api-server
sudo chown -R ykdbc:ykdbc /etc/nginx/sites-available/admin_web
sudo chown -R ykdbc:ykdbc /etc/nginx/sites-available/console-web

sudo ln -s /etc/nginx/sites-available/api-server /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/admin_web /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/console-web /etc/nginx/sites-enabled/

创建软连接
sudo ln -s /etc/nginx/sites-available/api-server /etc/nginx/sites-enabled/api-server

删除软连接
sudo rm /etc/nginx/sites-enabled/api-server

    Sub 账单信封 两台打印机同时输出()
    Application.ScreenUpdating = False
    Application.DisplayAlerts = False
    t = Timer
    Dim wb As Workbook
    Set wb = ThisWorkbook

    With ActiveSheet
        x = .[z11]: y = .[z14]
        If Not IsNumeric(x) Or Not IsNumeric(y) Then GoTo 100
        If x > y Then GoTo 100

        ' ✅ 总体打印确认
        If MsgBox("是否确认开始打印编号从 " & x & " 到 " & y & " 的账单和信封?", vbYesNo + vbQuestion, "确认打印") = vbNo Then
            GoTo 100
        End If

        For i = x To y
            .[y1] = i

            ' ------- 打印账单 -------
            .PageSetup.PrintArea = "R3:W17"
            With .PageSetup
                .LeftMargin = Application.CentimetersToPoints(8.2)
                .RightMargin = Application.CentimetersToPoints(0)
                .TopMargin = Application.CentimetersToPoints(1.5)
                .BottomMargin = Application.CentimetersToPoints(1.5)
                .HeaderMargin = Application.CentimetersToPoints(0.8)
                .FooterMargin = Application.CentimetersToPoints(0.8)
            End With
            Application.ActivePrinter = "OKI KS4132(PCL6) 在 Ne00:"
            .PrintOut

            ' ------- 打印信封 -------
            .PageSetup.PrintArea = "R23:Z30"
            With .PageSetup
                .LeftMargin = Application.CentimetersToPoints(0)
                .RightMargin = Application.CentimetersToPoints(0)
                .TopMargin = Application.CentimetersToPoints(0)
                .BottomMargin = Application.CentimetersToPoints(0)
                .HeaderMargin = Application.CentimetersToPoints(0)
                .FooterMargin = Application.CentimetersToPoints(0)
            End With
            Application.ActivePrinter = "OKI B432(PCL6) 在 Ne01:"
            .PrintOut
        Next
    End With

100:
    Application.ScreenUpdating = True
    Application.DisplayAlerts = True
    MsgBox "共耗时:" & Format(Timer - t, "0.0000") & " 秒!!!", 64
End Sub

Sub 账单()

Application.ScreenUpdating = False
Application.DisplayAlerts = False
t = Timer
Dim wb As Workbook
Set wb = ThisWorkbook

With ActiveSheet
    x = .[z11]: y = .[z14]
    If Not IsNumeric(x) Or Not IsNumeric(y) Then GoTo 100
    If x > y Then GoTo 100
    
    ' ? 设置打印区域
    .PageSetup.PrintArea = "R3:W17"
    
    ' ? 设置打印机
    Application.ActivePrinter = "OKI KS4132(PCL6) 在 Ne00:"
    
    ' ? 设置页边距(单位:厘米)
    With .PageSetup
        .LeftMargin = Application.CentimetersToPoints(8.2)
        .RightMargin = Application.CentimetersToPoints(0)
        .TopMargin = Application.CentimetersToPoints(1.5)
        .BottomMargin = Application.CentimetersToPoints(1.5)
        .HeaderMargin = Application.CentimetersToPoints(0.8)
        .FooterMargin = Application.CentimetersToPoints(0.8)
    End With
    
    ' ? 打印确认对话框
    If MsgBox("是否确认开始打印编号从 " & x & " 到 " & y & "?", vbYesNo + vbQuestion, "确认打印") = vbNo Then
        GoTo 100
    End If
    
    ' ? 循环打印编号
    For i = x To y
        .[y1] = i
        .PrintOut
    Next
End With

100:

Application.ScreenUpdating = True
Application.DisplayAlerts = True
MsgBox "共耗时:" & Format(Timer - t, "0.0000") & " 秒!!!", 64

End Sub

Sub 信封()

Application.ScreenUpdating = False
Application.DisplayAlerts = False
t = Timer
Dim wb As Workbook
Set wb = ThisWorkbook

With ActiveSheet
    x = .[z11]: y = .[z14]
    If Not IsNumeric(x) Or Not IsNumeric(y) Then GoTo 100
    If x > y Then GoTo 100
    
    ' ? 设置打印区域
    .PageSetup.PrintArea = "r23:z30"
    
    ' ? 设置打印机
    Application.ActivePrinter = "OKI KS4132(PCL6) 在 Ne00:"
    
    ' ? 设置页边距(单位:厘米)
    With .PageSetup
        .LeftMargin = Application.CentimetersToPoints(0)
        .RightMargin = Application.CentimetersToPoints(0)
        .TopMargin = Application.CentimetersToPoints(0)
        .BottomMargin = Application.CentimetersToPoints(0)
        .HeaderMargin = Application.CentimetersToPoints(0)
        .FooterMargin = Application.CentimetersToPoints(0)
    End With
    
    ' ? 打印确认对话框
    If MsgBox("是否确认开始打印编号从 " & x & " 到 " & y & "?", vbYesNo + vbQuestion, "确认打印") = vbNo Then
        GoTo 100
    End If
    
    ' ? 循环打印编号
    For i = x To y
        .[y1] = i
        .PrintOut
    Next
End With

100:

Application.ScreenUpdating = True
Application.DisplayAlerts = True
MsgBox "共耗时:" & Format(Timer - t, "0.0000") & " 秒!!!", 64

End Sub

Sub 查看当前打印机()

MsgBox Application.ActivePrinter

End Sub

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!'