2025年3月

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