A "presence-aware" AC controller with hysteresis, overheat overrides, and a Gree/EWPE Pow-bit fix

I’ve spent a few months iterating on a fairly elaborate climate automation for a Gree/EWPE-protocol split AC (branded as Sinclair here, but the protocol is shared by many rebrands — Cooper&Hunter, etc.). Sharing the design + full YAML in case it’s useful to anyone with a similar unit or a similar “which mode am I in” presence-based cooling problem.

What it does

  • Three “presence scenarios” via an input_select (Home / Away / Vacation), each with its own cooling logic
  • Home: different day/evening/night temperature targets, AC off overnight
  • Away: keep the room from overheating, but more aggressively (higher setpoint, always-on gate)
  • Vacation: only intervene on real overheating (higher threshold), don’t fight for comfort
  • A “free cooling” gate: don’t turn the AC on if it’s already cooler outside than inside — but an explicit overheat override always wins over that gate, so comfort/safety beats energy-saving when it actually matters
  • Hysteresis on the free-cooling gate via a 30-minute for: on a template trigger, to avoid flapping on short outdoor temperature blips
  • A shared “set cooling” and “turn off” script with retry (3x, 15s apart) + a push notification if it still didn’t take after 3 tries
  • A known Gree/EWPE integration quirk fix (see below) baked into both scripts

The Gree/EWPE “Pow bit” bug

This is probably the most broadly useful part if you’re on this integration (core gree or common forks). The integration sends only the fields that changed relative to its last cached state (a diff, not a full state push: Pow, Mod, SetTem, etc.).

If you call climate.set_temperature with both hvac_mode and temperature in one call while the unit is off, there’s a real risk the diff drops the Pow field (physical power-on) while SetTem still goes through. Symptom: the automation fires with no error, but the unit never actually turns on.

Fix: always split it into two calls — climate.set_hvac_mode first (mode only), then after a short delay climate.set_temperature (temperature only). Confirmed this fixed two consecutive failed morning activations for me.

A related gotcha I hit later: if the unit is already in cool mode (e.g. left over from a previous branch) but at the wrong temperature, a retry loop that only checks state == 'cool' will think it already succeeded and never send set_temperature. Check both state == 'cool' AND temperature == target before deciding whether either call is needed.

The YAML

Entity IDs, thresholds, and scenario names are placeholders — swap in your own. The overall shape (multi-branch choose:, shared retry/notify scripts, hysteresis triggers) is what’s meant to be reusable.

# ============================================================
# Main automation: presence-aware AC control
# ============================================================
alias: "AC - main logic (Home/Away/Vacation)"
mode: single
triggers:
  - trigger: time
    at: "08:00:00"
    id: morning
  - trigger: time
    at: "18:00:00"
    id: evening
  - trigger: time
    at: "22:00:00"
    id: night
  - trigger: state
    entity_id: input_select.presence_scenario
    id: scenario
  - trigger: numeric_state
    entity_id:
      - sensor.room_temp_a
      - sensor.room_temp_b
    above: 30
    id: any_above_30
  - trigger: numeric_state
    entity_id:
      - sensor.room_temp_a
      - sensor.room_temp_b
    below: 28
    id: any_below_28
  - trigger: numeric_state
    entity_id: sensor.room_temp_main
    below: 24
    id: too_cold
  - trigger: numeric_state
    entity_id: sensor.room_temp_main
    above: 28
    id: overheat_night
  - trigger: template
    # outside colder than inside, sustained 30 min (avoids flapping)
    value_template: >
      {{ (states('sensor.room_temp_main') | float(-99)) -
         (states('sensor.outdoor_temp') | float(99)) > 0 }}
    for: "00:30:00"
    id: outside_colder
  - trigger: template
    # outside got warm again, sustained 30 min (re-arms the day/evening branches)
    value_template: >
      {{ (states('sensor.outdoor_temp') | float(-99)) -
         (states('sensor.room_temp_main') | float(99)) > 0 }}
    for: "00:30:00"
    id: outside_warmer
  - trigger: numeric_state
    entity_id: sensor.outdoor_temp
    below: 30.01
  - trigger: numeric_state
    entity_id: sensor.room_temp_main
    above: 26
    id: threshold_day
  - trigger: numeric_state
    entity_id: sensor.room_temp_main
    above: 24
    id: threshold_evening
conditions: []
actions:
  - choose:
      # --- OFF: too cold, always wins regardless of scenario/time ---
      - conditions:
          - condition: trigger
            id: too_cold
        sequence:
          - action: script.ac_turn_off_verified
            data:
              entity_id: climate.your_ac
              branch_name: "too cold"

      # --- ON: night overheat safety net (22:00-08:00, >28°C) ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: time
            after: "22:00:00"
            before: "08:00:00"
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            above: 28
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 26
              branch_name: "night overheat safety net"

      # --- ON: daytime overheat override, ignores the free-cooling gate ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: time
            after: "08:00:00"
            before: "18:00:00"
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            above: 28
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 28
              branch_name: "daytime overheat override"

      # --- OFF: free cooling (outside colder than inside for >=30min) ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            below: 28
          - condition: template
            value_template: >
              {{ (states('sensor.room_temp_main') | float(-99)) -
                 (states('sensor.outdoor_temp') | float(99)) > 0 }}
        sequence:
          - action: script.ac_turn_off_verified
            data:
              entity_id: climate.your_ac
              branch_name: "free cooling"

      # --- OFF: night window / scenario change, 22:00-08:00 ---
      - conditions:
          - condition: trigger
            id: [night, scenario]
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: time
            after: "22:00:00"
            before: "08:00:00"
        sequence:
          - action: script.ac_turn_off_verified
            data:
              entity_id: climate.your_ac
              branch_name: "night / scenario change"

      # --- ON: Home evening 18:00-22:00, >24°C ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: time
            after: "18:00:00"
            before: "22:00:00"
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            above: 24
          - condition: template
            value_template: >
              {{ ((states('sensor.room_temp_main') | float(-99)) -
                  (states('sensor.outdoor_temp') | float(99)) <= 0)
                 or (states('sensor.room_temp_main') | float(-99) >= 28) }}
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 26
              branch_name: "evening 18-22h"

      # --- ON: Home day 08:00-18:00, >26°C ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Home
          - condition: time
            after: "08:00:00"
            before: "18:00:00"
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            above: 26
          - condition: template
            value_template: >
              {{ ((states('sensor.room_temp_main') | float(-99)) -
                  (states('sensor.outdoor_temp') | float(99)) <= 0)
                 or (states('sensor.room_temp_main') | float(-99) >= 28) }}
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 28
              branch_name: "day 08-18h"

      # --- OFF: Away, outside colder, unless room is genuinely overheating ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Away
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            below: 28
          - condition: template
            value_template: >
              {{ (states('sensor.room_temp_main') | float(-99)) -
                 (states('sensor.outdoor_temp') | float(99)) > 0 }}
        sequence:
          - action: script.ac_turn_off_verified
            data:
              entity_id: climate.your_ac
              branch_name: "Away - outside colder"

      # --- ON: Away, >26°C regardless of outside temp ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Away
          - condition: numeric_state
            entity_id: sensor.room_temp_main
            above: 26
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 28
              branch_name: "Away"

      # --- ON: Vacation, either room >30°C ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Vacation
          - condition: or
            conditions:
              - condition: numeric_state
                entity_id: sensor.room_temp_a
                above: 30
              - condition: numeric_state
                entity_id: sensor.room_temp_b
                above: 30
        sequence:
          - action: script.ac_set_cooling_verified
            data:
              entity_id: climate.your_ac
              target_temp: 28
              branch_name: "Vacation"

      # --- OFF: Vacation, outside AND both rooms back under 30°C ---
      - conditions:
          - condition: state
            entity_id: input_select.presence_scenario
            state: Vacation
          - condition: numeric_state
            entity_id: sensor.outdoor_temp
            below: 30.01
          - condition: numeric_state
            entity_id: sensor.room_temp_a
            below: 30.01
          - condition: numeric_state
            entity_id: sensor.room_temp_b
            below: 30.01
        sequence:
          - action: script.ac_turn_off_verified
            data:
              entity_id: climate.your_ac
              branch_name: "Vacation"

# ============================================================
# Script: set cooling, with retry + notification on failure
# ============================================================
---
alias: "AC - set cooling (retry + verify)"
fields:
  entity_id:
    description: "Climate entity to set"
    example: climate.your_ac
  target_temp:
    description: "Target temperature (cool mode)"
    example: 28
  branch_name:
    description: "Name of the calling branch, for the failure notification"
    example: "day 08-18h"
sequence:
  - repeat:
      count: 3
      sequence:
        - if:
            - condition: not
              conditions:
                - condition: and
                  conditions:
                    - condition: template
                      value_template: "{{ states(entity_id) == 'cool' }}"
                    - condition: template
                      value_template: "{{ state_attr(entity_id, 'temperature') == target_temp }}"
          then:
            # Only send hvac_mode if we're not already in cool - preserves the Pow-bit fix
            # while still correcting the temperature if mode is already right but temp is wrong.
            - if:
                - condition: template
                  value_template: "{{ states(entity_id) != 'cool' }}"
              then:
                - action: climate.set_hvac_mode
                  target:
                    entity_id: "{{ entity_id }}"
                  data:
                    hvac_mode: cool
                - delay: "00:00:02"
            - action: climate.set_temperature
              target:
                entity_id: "{{ entity_id }}"
              data:
                temperature: "{{ target_temp }}"
            - delay: "00:00:15"
  - if:
      - condition: not
        conditions:
          - condition: and
            conditions:
              - condition: template
                value_template: "{{ states(entity_id) == 'cool' }}"
              - condition: template
                value_template: "{{ state_attr(entity_id, 'temperature') == target_temp }}"
    then:
      - action: notify.notify   # swap for your own notify service
        data:
          title: "⚠️ AC - failed to set"
          message: "{{ entity_id }} did not reach the target state after 3 tries (branch: {{ branch_name }}, target {{ target_temp }}°C)"
mode: queued
max: 5

# ============================================================
# Script: turn off, with retry + notification on failure
# ============================================================
---
alias: "AC - turn off (retry + verify)"
fields:
  entity_id:
    description: "Climate entity to turn off"
    example: climate.your_ac
  branch_name:
    description: "Name of the calling branch, for the failure notification"
    example: "night"
sequence:
  - repeat:
      count: 3
      sequence:
        - if:
            - condition: template
              value_template: "{{ states(entity_id) != 'off' }}"
          then:
            - action: climate.set_hvac_mode
              target:
                entity_id: "{{ entity_id }}"
              data:
                hvac_mode: "off"
            - delay: "00:00:15"
  - if:
      - condition: template
        value_template: "{{ states(entity_id) != 'off' }}"
    then:
      - action: notify.notify   # swap for your own notify service
        data:
          title: "⚠️ AC - failed to turn off"
          message: "{{ entity_id }} did not turn off after 3 tries (branch: {{ branch_name }})"
mode: queued
max: 5

Design notes / gotchas that cost me debugging time

  • choose: picks the first matching branch — if two branches’ time windows overlap, order matters. I had an “overheat” branch (08:00-22:00) sitting above an “evening” branch (18:00-22:00); any overheat event in the evening window was silently handled by the wrong branch with the wrong target temperature. Fix: keep time windows non-overlapping, and order overheat branches so they only cover the periods where nothing else already handles overheat.
  • Retry loop must check both mode and temperature, not just mode — otherwise a unit that’s already cool at the wrong temp looks “already correct” and never gets corrected.
  • An automation can’t reliably catch its own transition to on — this is a known Home Assistant core limitation (the entity state flips before the automation’s own trigger listeners are re-attached). If you need “evaluate immediately when this automation gets (re-)enabled,” use a second, always-on watcher automation that listens for the first automation’s state: on and calls automation.trigger with skip_condition: true on it.
  • Add explicit trigger IDs for threshold crossings, even if a branch’s conditions don’t need a trigger filter. I initially only had above: 30 / below: 28 triggers plus a couple of relative (outside-vs-inside) template triggers; there was a window where the room could slowly cross 26°C or 24°C with nothing to re-trigger evaluation, so the automation just never re-checked.

Happy to answer questions if anyone’s fighting similar integration quirks or building something along these lines.