⚽ My World Cup Match Day automation with live goal announcements!⚽

Hey everyone! :wave:

With the World Cup in full swing, I wanted to share my Game Day FootballFever Automation setup that goes a bit beyond standard static scenes. I built a dynamic setup that automatically figures out who is playing, turns on my Aqara gear to match the competing countries, and makes custom match announcements!

:hammer_and_wrench: The Setup

  • Aqara Smart Light Bulb (representing Team 1)
  • Aqara LED Strip (representing Team 2)
  • Google Nest Hub Max (for stadium-style announcements)
  • Home Assistant & Cloudflare Workers (the brains)

:brain: How It Works Under the Hood

  1. Lightweight Tracking: To avoid complex parsing directly in my smart home, I created a custom Cloudflare Worker script hosted on my subdomain. It checks the live tournament schedule, automatically finds the current live match (or the closest upcoming one), and formats it cleanly e.g. as Paraguay vs. France (2026-07-04T21:00:00Z).
  2. Smart Timing & Dimming: Every 30 minutes, my automation evaluates this data:
    • 1 Hour Before Kick-off: The Aqara Bulb and LED Strip turn on at 50% brightness to build up the pre-game hype.
    • Match In Progress: The brightness automatically kicks up to 100%.
  3. AI Team Colors: Instead of hardcoding hex codes for dozens of countries, the automation uses a local AI task model (Gemini). It reads the two competing countries dynamically on the fly, chooses two perfectly contrasting colors that represent their flags, and sends those exact RGB values straight to my Aqara lights!
  4. The Kick-off Hype: Right when the timestamp matches the current time, my Google Nest Hub Max plays a custom voice announcement to let the room know the match has officially begun.


:computer: The Code (For My Fellow Home Assistant Geeks)

For anyone running Home Assistant, here is the exact YAML automation that handles the AI generation, itemizes the hex values into RGB arrays for the Aqara devices, and executes the timeline:

alias: "FIFA World Cup Lights"
description: "Controls living room lights and announcements based on World Cup match states."
mode: single
trigger:
  - platform: time_pattern
    minutes: "0"
  - platform: time_pattern
    minutes: "30"
condition:
  - condition: template
    value_template: >-
      {{ states('sensor.fifa_world_cup_2026') not in ['unknown', 'unavailable', ''] and 'vs.' in states('sensor.fifa_world_cup_2026') }}
action:
  - variables:
      match_text: "{{ states('sensor.fifa_world_cup_2026') }}"
      teams_part: "{{ match_text.split(' (')[0] }}"
      team1_name: "{{ teams_part.split(' vs. ')[0] | trim }}"
      team2_name: "{{ teams_part.split(' vs. ')[1] | trim }}"
      timestamp_str: "{{ match_text.split('(')[1].split(')')[0] }}"
      time_diff: "{{ (as_datetime(match_text.split('(')[1].split(')')[0]) - now()).total_seconds() / 60 }}"
  - variables:
      is_live: "{{ time_diff <= 0 and time_diff >= -125 }}"
      starting_soon: "{{ time_diff > 0 and time_diff <= 60 }}"
      is_exactly_starting: "{{ time_diff >= -15 and time_diff <= 15 }}"
  - condition: template
    value_template: "{{ is_live or starting_soon }}"
  - action: ai_task.generate_data
    data:
      entity_id: ai_task.gemini_flash_latest_version
      task_name: "Get light colors"
      instructions: >-
        There's a football match between {{ team1_name }} and {{ team2_name }}. 
        Choose two colors that best represent these countries but make sure they are different enough to differentiate.
        Return raw JSON mapping team1 and team2 with a hex key.
    response_variable: gemini_response
  - variables:
      colors: "{{ gemini_response.data | from_json }}"
      hex_team1: "{{ (gemini_response.data | from_json).team1.color }}"
      hex_team2: "{{ (gemini_response.data | from_json).team2.color }}"
      brightness_pct: "{{ 100 if is_live else 50 }}"
  - action: light.turn_on
    target:
      entity_id: light.wohnzimmerlampe
    data:
      brightness_pct: "{{ brightness_pct | int }}"
      rgb_color:
        - "{{ hex_team1[1:3] | int(base=16) }}"
        - "{{ hex_team1[3:5] | int(base=16) }}"
        - "{{ hex_team1[5:7] | int(base=16) }}"
  - action: light.turn_on
    target:
      entity_id: light.aqara_led_strip_t2
    data:
      brightness_pct: "{{ brightness_pct | int }}"
      rgb_color:
        - "{{ hex_team2[1:3] | int(base=16) }}"
        - "{{ hex_team2[3:5] | int(base=16) }}"
        - "{{ hex_team2[5:7] | int(base=16) }}"
  - if:
      - condition: template
        value_template: "{{ is_exactly_starting }}"
    then:
      - action: script.announcement_on_maxi
        metadata: {}
        data:
          message: "Match {{ team1_name }} vs. {{ team2_name }} begins"
          volume: 0.6

:satellite: Fetching the Data with the Scrape Integration

To get the worker’s text payload into Home Assistant so that the automation above can read it, you can use the native Scrape integration. Because the Cloudflare worker outputs a simple string, you just point the integration to the worker URL and use the HTML h1 tag as the selector.

:earth_africa: Want to use my API endpoints?

If you want to use my tracking endpoint to feed your own custom automation, feel free to pull from my Cloudflare worker: https://wc.markus.workers.dev/

(Just please don’t overdo the scan rate interval so the origin API doesn’t end up blocking my server! :sweat_smile: A poll every 5 to 10 minutes is plenty!)

Let me know what you think, or if you’ve got any cool suggestions to add to cinema modes or match day tracking! :soccer::boom:

5 Likes

With the World Cup in full swing, I felt motivated to implement a major upgrade to my match-day automation setup. I wanted something more dynamic than just lighting changes, so I’ve now added live audio announcements for goals to the mix!

To pull this off, I’m leveraging the Aqara Hub M3 alongside my existing Home Assistant configuration. Here is how I got it working:

  1. Custom Sound Effects: I downloaded a great cheering crowd sound effect from Pixabay (specifically this one) and uploaded it directly to the Aqara M3 Hub.
  2. Matter Integration: I created a scene within the Aqara app to play that specific cheering sound, and then exposed that scene via Matter directly into Home Assistant.
  3. API Upgrade: I updated my API to expose live goal events as they happen.

If you want to replicate this setup or hook into the live data, you can scrape my API using the popular Multiscrape Home Assistant integration.

1. Multiscrape Configuration

Add this to your configuration (adjusting for your specific entity names):

multiscrape:
  - resource: "https://wc.markus.workers.dev/"
    scan_interval: 60
    sensor:
      - name: "FIFA World Cup 2026"
        unique_id: "fifa_world_cup_2026"
        select: "h1"
        attributes:
          - name: Team 1
            select: "h1"
            value_template: >
              {% set raw_teams = value.split(' (')[0] if ' (' in value else value %}
              {{ raw_teams.split(' vs. ')[0].strip() if ' vs. ' in raw_teams else 'Unknown' }}
          - name: Team 2
            select: "h1"
            value_template: >
              {% set raw_teams = value.split(' (')[0] if ' (' in value else value %}
              {{ raw_teams.split(' vs. ')[1].strip() if ' vs. ' in raw_teams else 'Unknown' }}
          - name: Ongoing
            select: "body"
            extract: content
            value_template: "{{ '<p>' in value }}"
            on_error:
              value: none
          - name: Score
            select: "p"
            value_template: "{{ value if ':' in value else None }}"
            on_error:
              value: none
          - name: Goals Team 1
            select: "p"
            value_template: "{{ value.split(':')[0] if ':' in value else None }}"
            on_error:
              value: none
          - name: Goals Team 2
            select: "p"
            value_template: "{{ value.split(':')[1] if ':' in value else None }}"
            on_error:
              value: none

2. Home Assistant Automation

Then, use this automation code to trigger the Aqara M3 scene whenever a goal is detected by the scraper:

alias: FIFA World Cup Lights
description: >-
  Controls living room lights and announcements based on World Cup match states
  and scores.
triggers:
  - minutes: "0"
    trigger: time_pattern
    id: time_check
  - minutes: "30"
    trigger: time_pattern
    id: time_check
  - trigger: state
    entity_id: sensor.fifa_world_cup_2026
    attribute: goals_team_1
    id: goal_team1
  - trigger: state
    entity_id: sensor.fifa_world_cup_2026
    attribute: goals_team_2
    id: goal_team2
conditions:
  - condition: template
    value_template: >-
      {{ states('sensor.fifa_world_cup_2026') not in ['unknown', 'unavailable',
      ''] 
         and 'vs.' in states('sensor.fifa_world_cup_2026') }}
actions:
  - variables:
      match_text: "{{ states('sensor.fifa_world_cup_2026') }}"
      teams_part: "{{ match_text.split(' (')[0] }}"
      team1_name: "{{ state_attr('sensor.fifa_world_cup_2026', 'team_1') }}"
      team2_name: "{{ state_attr('sensor.fifa_world_cup_2026', 'team_2') }}"
      score: "{{ state_attr('sensor.fifa_world_cup_2026', 'score') }}"
      goals_t1: "{{ state_attr('sensor.fifa_world_cup_2026', 'goals_team_1') | int(0) }}"
      goals_t2: "{{ state_attr('sensor.fifa_world_cup_2026', 'goals_team_2') | int(0) }}"
      leading_team: >-
        {% if goals_t1 > goals_t2 %}{{ team1_name }} {% elif goals_t2 > goals_t1
        %}{{ team2_name }} {% else %}Tie{% endif %}
  - alias: If team scored goal
    if:
      - condition: template
        value_template: >-
          {{ trigger.id in ['goal_team1', 'goal_team2'] and trigger.from_state
          is not none }}
    then:
      - variables:
          scoring_team: "{{ team1_name if trigger.id == 'goal_team1' else team2_name }}"
          other_team: "{{ team2_name if trigger.id == 'goal_team1' else team1_name }}"
          target_light: >-
            {{ 'light.wohnzimmerlampe' if trigger.id == 'goal_team1' else
            'light.aqara_led_strip_t2' }}
          initial_state: "{{ states(target_light) }}"
          initial_bright: "{{ state_attr(target_light, 'brightness') }}"
          initial_rgb: "{{ state_attr(target_light, 'rgb_color') }}"
      - parallel:
          - if:
              - condition: template
                value_template: "{{ goals_t1 >= 1 or goals_t2 >= 1 }}"
            then:
              - action: switch.turn_on
                metadata: {}
                target:
                  entity_id: switch.play_crowd_cheers_sound
                data: {}
            alias: Play crowd cheers
          - if:
              - condition: template
                value_template: "{{ goals_t1 >= 1 or goals_t2 >= 1 }}"
            then:
              - action: script.announcement_on_maxi
                data:
                  message: >-
                    Gooooaaaaal! {{ scoring_team }} scored.  {% if leading_team
                    == 'Tie' %}
                      It now stands {{ score }} between both teams.
                    {% else %}
                      It now stands {{ score }} for {{ leading_team }} against {{ other_team }}.
                    {% endif %}
                  volume: 0.5
            alias: Goal announcement
          - alias: Animate light
            sequence:
              - action: ai_task.generate_data
                data:
                  entity_id: ai_task.gemini_flash_latest_version
                  task_name: Get flag colors
                  instructions: >-
                    Provide a JSON list of the dominant distinct colors found in
                    the national flag of {{ scoring_team }}.  Provide at least 2
                    or 3 colors. Return ONLY valid JSON in this exact format: {
                    "colors": ["#HEX1", "#HEX2", "#HEX3"] }
                response_variable: flag_colors_response
              - variables:
                  flag_colors: "{{ (flag_colors_response.data | from_json).colors }}"
              - repeat:
                  for_each: "{{ flag_colors * 5 }}"
                  sequence:
                    - action: light.turn_on
                      target:
                        entity_id: "{{ target_light }}"
                      data:
                        brightness_pct: 100
                        rgb_color:
                          - "{{ repeat.item[1:3] | int(base=16) }}"
                          - "{{ repeat.item[3:5] | int(base=16) }}"
                          - "{{ repeat.item[5:7] | int(base=16) }}"
                    - delay:
                        hours: 0
                        minutes: 0
                        seconds: 0
                        milliseconds: 600
              - choose:
                  - conditions:
                      - condition: template
                        value_template: "{{ initial_state == 'on' }}"
                    sequence:
                      - action: light.turn_on
                        target:
                          entity_id: "{{ target_light }}"
                        data:
                          brightness: "{{ initial_bright | int(255) }}"
                          rgb_color: >-
                            {{ initial_rgb if initial_rgb is not none else [255,
                            255, 255] }}
                default:
                  - action: light.turn_off
                    target:
                      entity_id: "{{ target_light }}"
                    data: {}
  - if:
      - condition: template
        value_template: "{{ trigger.id == 'time_check' }}"
    then:
      - variables:
          timestamp_str: "{{ match_text.split('(')[1].split(')')[0] }}"
          time_diff: "{{ (as_datetime(timestamp_str) - now()).total_seconds() / 60 }}"
          is_live: "{{ time_diff <= 0 and time_diff >= -125 }}"
          starting_soon: "{{ time_diff > 0 and time_diff <= 60 }}"
          is_exactly_starting: "{{ time_diff >= -15 and time_diff <= 15 }}"
      - condition: template
        value_template: "{{ is_exactly_starting or starting_soon }}"
      - action: ai_task.generate_data
        data:
          entity_id: ai_task.gemini_flash_latest_version
          task_name: Get light colors
          instructions: >-
            There's a FIFA world cup 2026 football match between {{ team1_name
            }} and {{ team2_name }}.  I have two RGB lamps which I want to
            represent those two teams. Choose the two colors  that best
            represent these countries but make sure they are different enough to
            be able  to differentiate them. Return the result as valid JSON in
            the following form and nothing else: {
              "team1": { "name": "{{ team1_name }}", "color": "#<hex color code>" },
              "team2": { "name": "{{ team2_name }}", "color": "#<hex color code>" }
            }
        response_variable: gemini_response
      - variables:
          colors: "{{ gemini_response.data | from_json }}"
          hex_team1: "{{ colors.team1.color }}"
          hex_team2: "{{ colors.team2.color }}"
          brightness_pct: "{{ 100 if is_live else 50 }}"
      - action: light.turn_on
        target:
          entity_id: light.wohnzimmerlampe
        data:
          brightness_pct: "{{ brightness_pct | int }}"
          rgb_color:
            - "{{ hex_team1[1:3] | int(base=16) }}"
            - "{{ hex_team1[3:5] | int(base=16) }}"
            - "{{ hex_team1[5:7] | int(base=16) }}"
      - action: light.turn_on
        target:
          entity_id: light.aqara_led_strip_t2
        data:
          brightness_pct: "{{ brightness_pct | int }}"
          rgb_color:
            - "{{ hex_team2[1:3] | int(base=16) }}"
            - "{{ hex_team2[3:5] | int(base=16) }}"
            - "{{ hex_team2[5:7] | int(base=16) }}"
      - if:
          - condition: template
            value_template: "{{ is_exactly_starting }}"
        then:
          - action: script.announcement_on_maxi
            data:
              message: Match {{ team1_name }} vs. {{ team2_name }} begins
              volume: 0.5
    alias: If mood lights trigger
mode: single

It’s quite amazing hearing the room erupt into a stadium cheer shortly after a goal is scored. It completely changes the match-day experience. Let me know if you have any questions or need help tweaking the configs for your own setup!

2 Likes

It’s really cool, our users are always more creative than us, hahaha. I think this scene seems to take you to the competition site, it’s amazing. Unfortunately, my favorite Brazilian team was eliminated by the Norwegian team early this morning Beijing time. Haaland is really amazing.

Finally, let’s guess the champion of this World Cup.

1 Like

That’s amazing! Open the door, I’m outside with drinks. :sunglasses:

1 Like

Of yeah, I also thought Brazil would make it but Norway was super strong. They won well deserved.

What’s your guess for the champion @Aqara_PM_Donie ?

2 Likes

Everyone says France will be the champion, but I think Argentina has a chance to defend their title :grinning:

3 Likes