Enhance a single-file YAML configuration with new functions

I need to enhance my YAML ESP32 EspHome configuration file bmssolar.yaml (see the code below) with two functions (ideally, declared within the single existing YAML configuration file).

Context

  • My electricity provider only allows to feed in 4000W, and the inverter is also set to a feed-in power of 4000W. Therefore, I only want to charge the battery when the feed-in power reaches a value close to 4000W.

  • My battery charging current is a value read via Modbus „Netz-Leistung“ (“grid power”), and ranges from a minimum of 1A to a maximum of 25A. Currently, it is hardcoded in the YAML config file at 25.

Function 1

Variables:

  • Be a global constant feed_in_power_threshold equal to 4000W * 0.975.

I want the Modbus „Netz-Leistung“ value to be periodically (every 30 seconds) compared with a decimal value equal to feed_in_power_threshold.

While the Modbus „Netz-Leistung“ value is greater than the threshold, the variable should be increased by a delta 0.2A as long as it hasn’t reached the max constant value of 25A.

While the Modbus „Netz-Leistung“ value is less than the threshold, the variable should be decreased by a delta 0.2A as long as it hasn’t reached the min constant value of 1A.

I would also like to see the Modbus „Netz-Leistung“ value to be logged or printed out under ESPHome.

Function 2

Variables:

  • Be a global constant battery_soc_threshold equal to 80%.

  • Be a global constant high_power_threshold equal to 4000W * 0.9875.

  • Be a global constant low_power_threshold equal to 4000W * 0.875.

The value read via Modbus „SOC“ should be periodically (every 30 seconds) compared with battery_soc_threshold.

While the Modbus „SOC“ is greater than battery_soc_threshold, then:

  • While the Modbus „Netz-Leistung“ is greater than a numeric high_power_threshold, a GPIO should be set to high (a consumer should switch on) and remain high for at least 10 minutes. No new control should be performed within that sleep time.

  • While the Modbus „Netz-Leistung“ is less than a numeric low_power_threshold, the GPIO should go low (the consumer should switch off) and remain low for at least 5 minutes. No new control should be performed within that sleep time.

Particular wishes

  • The code should contain no so-called magic numbers. Any value should be declared as a global constant or a global variable with a meaningful name.

  • Ideally, I want it all to hold within the single existing YAML configuration file.

Error messages I am getting

  • Component not found: automation

  • Component template cannot be loaded via YAML (no CONFIG_SCHEMA)

YAML config file

substitutions:
  name: bmssolar
  max_cycles: "6000.0"
  tx_pin: GPIO1
  rx_pin: GPIO3
  feed_in_power_threshold: !lambda '4000 * 0.975'
  battery_soc_threshold: !lambda '4000 * 0.8'
  high_power_threshold: !lambda '4000 * 0.9875'
  low_power_threshold: !lambda '4000 * 0.875'
  delta_charging_current: 0.2
  max_charging_current: 25
  min_charging_current: 1
esphome:
  name: bmssolar
  friendly_name: bmssolar
esp32:
  board: esp32dev
  framework:
    type: arduino
logger:
  baud_rate: 0
globals:
  - id: can_420_rx
    type: int
    restore_value: "no"
    initial_value: "0"
  - id: charge_status
    type: "std::string"
    restore_value: "no"
    initial_value: '"Startup"'
  - id: charging_current
    type: float
    restore_value: "no"
    initial_value: "25"
button:
  - platform: restart
    name: Restart button
    id: restart_button
    internal: true
wifi:
  ssid: null
  password: null
  ap:
    ssid: BmsSolar Fallback Hotspot
    password: null
api:
  encryption:
    key: null
ota:
  password: null
uart:
  id: uart_0
  baud_rate: 9600
  rx_buffer_size: 384
  tx_pin: "${tx_pin}"
  rx_pin: "${rx_pin}"
modbus:
  id: modbus0
  uart_id: uart_0
  send_wait_time: 900ms
modbus_controller:
  id: bms0
  address: 247
  modbus_id: modbus0
  command_throttle: 900ms
  update_interval: 30s
output:
  - platform: gpio
    pin: 2
    id: led
    inverted: false
switch:
  - platform: gpio
    pin: GPIO4
    name: Test
    id: Test
sensor:
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: power_sensor
    name: Leistung
    address: 35140
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: W
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: soc_sensor
    name: SOC
    address: 37007
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: "%"
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
  - platform: modbus_controller
    modbus_controller_id: bms0
    name: Temperatur
    address: 35174
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
template:
  - sensor:
      - name: "Charging Current"
        id: charging_current_sensor
        unit_of_measurement: "A"
        accuracy_decimals: 1
        lambda: |-
          float power = id(power_sensor).state;
          float current = id(charging_current);
          if (power > feed_in_power_threshold) {
            if (current < max_charging_current) {
              current += delta_charging_current;
            }
          } else if (power < feed_in_power_threshold) {
            if (current > min_charging_current) {
              current -= delta_charging_current;
            }
          }
          id(charging_current) = current;
          ESP_LOGD("charging_current", "Calculated charging current: %.1f", current);
          return current;
automation:
  - id: charge_control
    alias: "Charge Control"
    trigger:
      - platform: time
        interval: 30s
    action:
      - lambda: |-
          float soc = id(soc_sensor).state;
          float power = id(power_sensor).state;
          if (soc > battery_soc_threshold && power > high_power_threshold) {
            digitalWrite(GPIO_PIN, HIGH);
            id(led).turn_on();
            delay(600000);  // 10 minutes in milliseconds
            float new_power = id(power_sensor).state;
            if (new_power < low_power_threshold) {
              digitalWrite(GPIO_PIN, LOW);
              id(led).turn_off();
              delay(300000);  // 5 minutes in milliseconds
            }
          } else {
            digitalWrite(GPIO_PIN, LOW);
            id(led).turn_off();
          }

Probably because that isn’t a valid component. However automations are fully described here

Nor is that.

Hello Nick, I’m replying to you as you were the only person who has replied as of now. I followed your advice and read that documentation chapter. Now I’m back with the refactored YAML config. Can you please take a look at the refactored code below and tell me what you think?

substitutions:
  name: bmssolar
  max_cycles: "6000.0"
  tx_pin: GPIO1
  rx_pin: GPIO3
esphome:
  name: bmssolar
  friendly_name: bmssolar
esp32:
  board: esp32dev
  framework:
    type: arduino
logger:
  baud_rate: 0
globals:
  - id: can_420_rx
    type: int
    restore_value: 'no'
    initial_value: '0'
  - id: charge_status
    type: 'std::string'
    restore_value: 'no'
    initial_value: '"Startup"'
  - id: battery_charge_current
    type: float
    restore_value: 'no'
    initial_value: '1.0'
  - id: feed_in_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3900.0' # 4000W * 0.975
  - id: battery_soc_threshold
    type: float
    restore_value: 'no'
    initial_value: '80.0'
  - id: high_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3950.0' # 4000W * 0.9875
  - id: low_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3500.0' # 4000W * 0.875
  - id: delta_charging_current
    type: float
    restore_value: 'no'
    initial_value: '0.2'
  - id: max_charging_current
    type: int
    restore_value: 'no'
    initial_value: '25'
  - id: min_charging_current
    type: int
    restore_value: 'no'
    initial_value: '1'
button:
  - platform: restart
    name: Restart button
    id: restart_button
    internal: true
wifi:
  ssid: null
  password: null
  ap:
    ssid: BmsSolar Fallback Hotspot
    password: null
api:
  encryption:
    key: null
ota:
  password: null
uart:
  id: uart_0
  baud_rate: 9600
  rx_buffer_size: 384
  tx_pin: "${tx_pin}"
  rx_pin: "${rx_pin}"
modbus:
  id: modbus0
  uart_id: uart_0
  send_wait_time: 900ms
modbus_controller:
  id: bms0
  address: 247
  modbus_id: modbus0
  command_throttle: 900ms
  update_interval: 3s
output:
  - platform: gpio
    pin: 2
    id: led
    inverted: false
switch:
  - platform: gpio
    pin: GPIO4
    name: Test
    id: Test
sensor:
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: power_sensor
    name: Leistung
    address: 35140
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: W
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
    on_value:
      then:
        - lambda: |-
            if (id(power_sensor).state > id(feed_in_power_threshold)) {
              if (id(battery_charge_current) < id(max_charging_current)) {
                id(battery_charge_current) += id(delta_charging_current);
              }
            } else if (id(power_sensor).state < id(feed_in_power_threshold)) {
              if (id(battery_charge_current) > id(min_charging_current)) {
                id(battery_charge_current) -= id(delta_charging_current);
              }
            }
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: soc_sensor
    name: SOC
    address: 37007
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: "%"
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
    on_value:
      then:
        - lambda: |-
            if (id(soc_sensor).state > id(battery_soc_threshold)) {
              if (id(power_sensor).state > id(high_power_threshold)) {
                digitalWrite(GPIO_PIN, HIGH);
                id(led).turn_on();
                delay(600000);  // 10 minutes in milliseconds
                if (id(power_sensor).state < id(low_power_threshold)) {
                  digitalWrite(GPIO_PIN, LOW);
                  id(led).turn_off();
                  delay(300000);  // 5 minutes in milliseconds
                }
              } else {
                digitalWrite(GPIO_PIN, LOW);
                id(led).turn_off();
              }
            }

Well structurally it looks better, but I am certainly not an expert. Question is, does it work?

Right now I’m getting

In lambda function: (…) error: 'GPIO4' was not declared in this scope

for each digitalWrite(GPIO_PIN, ...) statement

GPIO_PIN is a place holder. You would use the pin number. The only place I can find digitalWrite in esphome is under the depreciated custom component. If you want to turn a switch on/off the lambda from the standard switch component should work.

Duly noted, thank you :writing_hand:

I too have run a search through the documentation, and have come to the very same conclusion.

Duly noted, thank you :writing_hand:

Thank you to everyone who contributed.

Here’s the solution:

substitutions:
  name: bmssolar
  max_cycles: "6000.0"
  tx_pin: GPIO1
  rx_pin: GPIO3
esphome:
  name: bmssolar
  friendly_name: bmssolar
esp32:
  board: esp32dev
  framework:
    type: arduino
logger:
  baud_rate: 0
globals:
  - id: can_420_rx
    type: int
    restore_value: 'no'
    initial_value: '0'
  - id: charge_status
    type: 'std::string'
    restore_value: 'no'
    initial_value: '"Startup"'
  - id: battery_charge_current
    type: float
    restore_value: 'no'
    initial_value: '1.0'
  - id: feed_in_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3900.0' # 4000W * 0.975
  - id: battery_soc_threshold
    type: float
    restore_value: 'no'
    initial_value: '60.0'
  - id: high_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3800.0'
  - id: low_power_threshold
    type: float
    restore_value: 'no'
    initial_value: '3000.0'
  - id: delta_charging_current
    type: float
    restore_value: 'no'
    initial_value: '0.2'
  - id: max_charging_current
    type: int
    restore_value: 'no'
    initial_value: '25'
  - id: min_charging_current
    type: int
    restore_value: 'no'
    initial_value: '1'
button:
  - platform: restart
    name: Restart button
    id: restart_button
    internal: true
wifi:
  ssid: null
  password: null
  ap:
    ssid: BmsSolar Fallback Hotspot
    password: null
api:
  encryption:
    key: null
ota:
  password: null
uart:
  id: uart_0
  baud_rate: 9600
  rx_buffer_size: 384
  tx_pin: "${tx_pin}"
  rx_pin: "${rx_pin}"
modbus:
  id: modbus0
  uart_id: uart_0
  send_wait_time: 900ms
modbus_controller:
  id: bms0
  address: 247
  modbus_id: modbus0
  command_throttle: 900ms
  update_interval: 3s
switch:
  - platform: gpio
    id: relais
    pin: 13
    name: 'Ausgang'
sensor:
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: power_sensor
    name: Leistung
    address: 35140
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: W
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
    on_value:
      then:
        - lambda: |-
            if (id(power_sensor).state > id(feed_in_power_threshold)) {
              if (id(battery_charge_current) < id(max_charging_current)) {
                id(battery_charge_current) += id(delta_charging_current);
                ESP_LOGI('power_sensor', 'battery_charge_current increased to %f A',
                        id(battery_charge_current));
              }
            } else if (id(power_sensor).state < id(feed_in_power_threshold)) {
              if (id(battery_charge_current) > id(min_charging_current)) {
                id(battery_charge_current) -= id(delta_charging_current);
                ESP_LOGI('power_sensor', 'battery_charge_current decreased to %f A',
                        id(battery_charge_current));
              }
            }
  - platform: modbus_controller
    modbus_controller_id: bms0
    id: soc_sensor
    name: SOC
    address: 37007
    register_type: holding
    value_type: S_WORD
    unit_of_measurement: "%"
    device_class: energy
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - multiply: 1
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                return id(soc_sensor).state > id(battery_soc_threshold);
            then:
              - if:
                  condition:
                    and:
                      - lambda: |-
                          return id(power_sensor).state > id(high_power_threshold);
                      - switch.is_off: relais
                      - not:
                          script.is_running: minimum_delay_with_relais_off
                  then:
                    - switch.turn_on: relais
                    - script.execute: minimum_delay_with_relais_on
                    - script.wait: minimum_delay_with_relais_on
                    - while:
                        condition:
                          not:
                            lambda: |-
                              return id(power_sensor).state < id(low_power_threshold);
                        then:
                          - delay: 1s
                    - if:
                        condition:
                          and:
                            - switch.is_on: relais
                            - not:
                                script.is_running: minimum_delay_with_relais_on
                        then:
                          - switch.turn_off: relais
                          - script.execute: minimum_delay_with_relais_off
                          - script.wait: minimum_delay_with_relais_off
script:
  - id: minimum_delay_with_relais_on
    then:
      - delay: 40s
  - id: minimum_delay_with_relais_off
    then:
      - delay: 200s

In order for the delay: idle times to be observed, they must be implemented as 2 separate script: directives, so that we can double check whether a script is done running before the main logic can proceed.

See also:

1 Like