Created
January 28, 2025 23:47
-
-
Save gapple/3f5952d87d1cab3b4c69e66456f77e58 to your computer and use it in GitHub Desktop.
ESPHome ERV
This file contains hidden or 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
| substitutions: | |
| exhaust_pwm_pin: GPIO26 | |
| exhaust_rpm_pin: GPIO27 | |
| exhaust_min_power: "0.2" | |
| exhaust_max_power: "1.0" | |
| exhaust_power_scale: "1.0" | |
| exhaust_min_rpm: "300" | |
| exhaust_max_rpm: "2000" | |
| exhaust_pulse_per_rotation: "2.0" | |
| intake_pwm_pin: GPIO14 | |
| intake_rpm_pin: GPIO13 | |
| intake_min_power: "0.2" | |
| intake_max_power: "1.0" | |
| intake_power_scale: "1.0" | |
| intake_min_rpm: "300" | |
| intake_max_rpm: "2000" | |
| intake_pulse_per_rotation: "2.0" | |
| status_pin: GPIO2 | |
| onewire_pin: GPIO33 | |
| esphome: | |
| name: erv | |
| friendly_name: Energy Recovery Ventilator | |
| min_version: 2024.12.0 | |
| on_boot: | |
| - priority: 500 # after sensor setup, before wifi | |
| then: | |
| - component.update: co2_pid_float | |
| - script.execute: update_fan_speed | |
| esp32: | |
| board: esp32dev | |
| framework: | |
| type: arduino | |
| logger: | |
| level: WARN | |
| api: | |
| id: ha_api | |
| encryption: | |
| key: !secret hrv_api_key | |
| ota: | |
| - platform: esphome | |
| password: !secret ota_password | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| web_server: | |
| version: 3 | |
| ota: False | |
| include_internal: True | |
| sorting_groups: | |
| - id: sorting_group_control | |
| name: "Control" | |
| sorting_weight: 1 | |
| - id: sorting_group_fans | |
| name: "Fans" | |
| sorting_weight: 2 | |
| - id: sorting_group_efficiency | |
| name: "Efficiency" | |
| sorting_weight: 3 | |
| - id: sorting_group_other | |
| name: "Other" | |
| sorting_weight: 50 | |
| time: | |
| - platform: homeassistant | |
| id: homeassistant_time | |
| one_wire: | |
| platform: gpio | |
| id: onewire_bus | |
| pin: | |
| number: ${onewire_pin} | |
| output: | |
| - platform: ledc | |
| id: exhaust_fan_pwm | |
| pin: | |
| number: ${exhaust_pwm_pin} | |
| mode: OUTPUT_OPEN_DRAIN | |
| frequency: 25000 | |
| channel: 0 | |
| min_power: ${exhaust_min_power} | |
| max_power: ${exhaust_max_power} | |
| zero_means_zero: true | |
| - platform: ledc | |
| id: intake_fan_pwm | |
| pin: | |
| number: ${intake_pwm_pin} | |
| mode: OUTPUT_OPEN_DRAIN | |
| frequency: 25000 | |
| channel: 2 | |
| min_power: ${intake_min_power} | |
| max_power: ${intake_max_power} | |
| zero_means_zero: true | |
| light: | |
| - platform: status_led | |
| id: status_led_light | |
| name: Status LED | |
| internal: True | |
| pin: | |
| number: ${status_pin} | |
| ignore_strapping_warning: true | |
| web_server: | |
| sorting_group_id: sorting_group_other | |
| fan: | |
| ############################## | |
| # Fan Control | |
| ############################## | |
| - platform: template | |
| id: fan_control | |
| name: Fans | |
| restore_mode: RESTORE_DEFAULT_OFF | |
| preset_modes: | |
| - Exchange | |
| - Exhaust | |
| - Intake | |
| on_turn_on: | |
| then: | |
| - script.execute: update_fan_speed | |
| on_turn_off: | |
| then: | |
| - script.execute: update_fan_speed | |
| on_preset_set: | |
| then: | |
| - script.execute: update_fan_speed | |
| web_server: | |
| sorting_group_id: sorting_group_control | |
| sensor: | |
| ############################## | |
| # Fan Speed | |
| ############################## | |
| - platform: pulse_counter | |
| pin: | |
| number: ${exhaust_rpm_pin} | |
| mode: INPUT_PULLUP | |
| id: exhaust_fan_rpm | |
| name: Exhaust Speed | |
| icon: mdi:gauge | |
| unit_of_measurement: 'rpm' | |
| accuracy_decimals: 0 | |
| use_pcnt: False | |
| internal_filter: 1ms # 2000 rpm: 1/(2*2000)*60 = 0.015ms | |
| count_mode: | |
| rising_edge: DISABLE | |
| falling_edge: INCREMENT | |
| update_interval: 5s | |
| filters: | |
| - lambda: "return x / ${exhaust_pulse_per_rotation};" | |
| - clamp: | |
| min_value: 0 | |
| - clamp: | |
| max_value: ${exhaust_max_rpm} | |
| ignore_out_of_range: True | |
| - sliding_window_moving_average: | |
| window_size: 4 | |
| send_every: 12 | |
| send_first_at: 2 | |
| - round_to_multiple_of: 5 | |
| web_server: | |
| sorting_group_id: sorting_group_fans | |
| - platform: pulse_counter | |
| pin: | |
| number: ${intake_rpm_pin} | |
| mode: INPUT_PULLUP | |
| id: intake_fan_rpm | |
| name: Intake Speed | |
| icon: mdi:gauge | |
| unit_of_measurement: 'rpm' | |
| accuracy_decimals: 0 | |
| use_pcnt: False | |
| internal_filter: 1ms | |
| count_mode: | |
| rising_edge: DISABLE | |
| falling_edge: INCREMENT | |
| update_interval: 5s | |
| filters: | |
| - lambda: "return x / ${intake_pulse_per_rotation};" | |
| - clamp: | |
| min_value: 0 | |
| - clamp: | |
| max_value: ${intake_max_rpm} | |
| ignore_out_of_range: True | |
| - sliding_window_moving_average: | |
| window_size: 4 | |
| send_every: 12 | |
| send_first_at: 2 | |
| - round_to_multiple_of: 5 | |
| web_server: | |
| sorting_group_id: sorting_group_fans | |
| ############################## | |
| # C02 | |
| ############################## | |
| - platform: homeassistant | |
| id: co2_pid | |
| name: CO2 PID | |
| icon: mdi:molecule-co2 | |
| internal: True | |
| entity_id: sensor.co2_pid | |
| accuracy_decimals: 1 | |
| filters: | |
| - debounce: 3s | |
| - clamp: | |
| min_value: 1 | |
| max_value: 100 | |
| web_server: | |
| sorting_group_id: sorting_group_fans | |
| - platform: template | |
| id: co2_pid_float | |
| name: C02 PID float | |
| icon: mdi:molecule-co2 | |
| internal: True | |
| accuracy_decimals: 3 | |
| lambda: |- | |
| if (isnan(id(co2_pid).state)) { | |
| // Ignore 'unknown' state if connected to Home Assistant. | |
| if ( id(ha_api).is_connected() ) { | |
| return {}; | |
| } | |
| return 0.5; | |
| } | |
| return id(co2_pid).state / 100.0; | |
| update_interval: 5s | |
| filters: | |
| # - filter_out: NAN | |
| - sliding_window_moving_average: | |
| window_size: 30 | |
| send_every: 1 | |
| send_first_at: 1 | |
| - round: 5 | |
| - clamp: | |
| min_value: 0 | |
| max_value: 1 | |
| on_value: | |
| then: | |
| script.execute: update_fan_speed | |
| web_server: | |
| sorting_group_id: sorting_group_fans | |
| ############################## | |
| # Temperature & Efficiency # | |
| ############################## | |
| - platform: dallas_temp | |
| one_wire_id: onewire_bus | |
| id: exhaust_input_temp | |
| name: "Exhaust Input Temperature" | |
| icon: mdi:thermometer | |
| address: 0xF0F0F0FF0F0F0FF0 | |
| accuracy_decimals: 2 | |
| update_interval: 7s | |
| filters: | |
| - filter_out: nan | |
| - offset: 0.3 | |
| - clamp: | |
| min_value: 10 | |
| max_value: 30 | |
| ignore_out_of_range: True | |
| - throttle_average: 60s | |
| on_value: | |
| then: | |
| component.update: efficiency | |
| web_server: | |
| sorting_group_id: sorting_group_efficiency | |
| - platform: dallas_temp | |
| one_wire_id: onewire_bus | |
| id: intake_input_temp | |
| name: "Intake Input Temperature" | |
| icon: mdi:thermometer | |
| address: 0xFF00FF00FF00FF00 | |
| accuracy_decimals: 2 | |
| update_interval: 10s | |
| filters: | |
| - filter_out: nan | |
| - clamp: | |
| min_value: -20 | |
| max_value: 40 | |
| ignore_out_of_range: True | |
| - throttle_average: 60s | |
| on_value: | |
| then: | |
| component.update: efficiency | |
| web_server: | |
| sorting_group_id: sorting_group_efficiency | |
| - platform: dallas_temp | |
| one_wire_id: onewire_bus | |
| id: intake_output_temp | |
| name: "Intake Output Temperature" | |
| icon: mdi:thermometer | |
| address: 0xFFFF0000FFFF0000 | |
| accuracy_decimals: 2 | |
| update_interval: 8s | |
| filters: | |
| - filter_out: nan | |
| - offset: 0.5 | |
| - clamp: | |
| min_value: 0 | |
| max_value: 30 | |
| ignore_out_of_range: True | |
| - throttle_average: 60s | |
| on_value: | |
| then: | |
| component.update: efficiency | |
| web_server: | |
| sorting_group_id: sorting_group_efficiency | |
| - platform: template | |
| id: efficiency | |
| name: "Efficiency" | |
| icon: mdi:home-percent-outline | |
| state_class: measurement | |
| unit_of_measurement: "%" | |
| accuracy_decimals: 1 | |
| update_interval: never | |
| lambda: |- | |
| if ( | |
| isnan(id(intake_output_temp).state) | |
| || isnan(id(intake_input_temp).state) | |
| || isnan(id(exhaust_input_temp).state) | |
| ) { | |
| return NAN; | |
| } | |
| // When cold outside, | |
| if ( | |
| id(intake_input_temp).state <= id(exhaust_input_temp).state | |
| && | |
| ( | |
| // fresh air must be colder | |
| id(exhaust_input_temp).state <= id(intake_output_temp).state | |
| || | |
| // outside must be at least a degree colder | |
| id(exhaust_input_temp).state - id(intake_input_temp).state < 1 | |
| ) | |
| ) { | |
| return NAN; | |
| } | |
| // When warm outside, | |
| if ( | |
| id(intake_input_temp).state > id(exhaust_input_temp).state | |
| && | |
| ( | |
| // fresh air must be warmer | |
| id(exhaust_input_temp).state >= id(intake_output_temp).state | |
| || | |
| // outside must be at least a degree warmer | |
| id(intake_input_temp).state - id(exhaust_input_temp).state < 1 | |
| ) | |
| ) { | |
| return NAN; | |
| } | |
| return | |
| ( id(intake_output_temp).state - id(intake_input_temp).state ) | |
| / | |
| ( id(exhaust_input_temp).state - id(intake_input_temp).state ) | |
| * 100.0; | |
| filters: | |
| - debounce: 5s | |
| - lambda: |- | |
| if ( | |
| id(exhaust_fan_rpm).state > ${exhaust_min_rpm} | |
| || id(intake_fan_rpm).state > ${intake_min_rpm} | |
| ) { | |
| return x; | |
| } | |
| return NAN; | |
| - round: 1 | |
| - clamp: | |
| min_value: 0 | |
| max_value: 100 | |
| ignore_out_of_range: True | |
| - throttle_average: 60s | |
| web_server: | |
| sorting_group_id: sorting_group_efficiency | |
| script: | |
| - id: update_fan_speed | |
| mode: restart | |
| then: | |
| - script.execute: | |
| id: set_fan_speed | |
| mode: !lambda |- | |
| return id(fan_control).preset_mode; | |
| value: !lambda |- | |
| return id(co2_pid_float).state; | |
| - id: set_fan_speed | |
| mode: restart | |
| parameters: | |
| mode: string | |
| value: float | |
| then: | |
| - if: | |
| condition: | |
| - fan.is_on: fan_control | |
| then: | |
| - if: | |
| condition: | |
| - lambda: return mode == "Intake"; | |
| then: | |
| - output.turn_off: exhaust_fan_pwm | |
| else: | |
| - if: | |
| condition: | |
| - lambda: return mode == "Exhaust"; | |
| then: | |
| - output.set_level: | |
| id: exhaust_fan_pwm | |
| level: 1.0 | |
| else: | |
| - output.set_level: | |
| id: exhaust_fan_pwm | |
| level: !lambda |- | |
| return fminf(1.0, value * ${exhaust_power_scale} ); | |
| - if: | |
| condition: | |
| - lambda: return mode == "Exhaust"; | |
| then: | |
| - output.turn_off: intake_fan_pwm | |
| else: | |
| - if: | |
| condition: | |
| - lambda: return mode == "Intake"; | |
| then: | |
| - output.set_level: | |
| id: intake_fan_pwm | |
| level: 1.0 | |
| else: | |
| - output.set_level: | |
| id: intake_fan_pwm | |
| level: !lambda | |
| return fminf(1.0, value * ${intake_power_scale} ); | |
| else: | |
| - output.turn_off: exhaust_fan_pwm | |
| - output.turn_off: intake_fan_pwm |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment