Dynamic AI Weather Card with Google Gemini & PyScript in Home Assistant

Goal: This guide details how to create a dynamic weather card in Home Assistant. The card’s background image is generated by Google’s Gemini AI based on the current weather conditions and time of day, updating automatically. Clicking the card shows the standard weather entity details.

(This project was developed collaboratively using Home Assistant user ‘kloudy’ idea Weather Dashboard with ChatGPT & DALL-E and Google’s Gemini AI, navigating several technical challenges along the way.)
Example output…


Technologies Used:

Disclaimers & Considerations:

  • PyScript Environment: The standard HA python_script integration proved too restrictive. PyScript was used but presented challenges with secure secret access and handling blocking file I/O (open, yaml.safe_load) reliably via task.executor. The final file saving method uses standard io.open directly, which worked in testing but carries a small theoretical risk of blocking HA if disk I/O is extremely slow.
  • API Key Security: Due to issues with the !secret tag validation in the Automation UI and problems accessing secrets programmatically from PyScript, the final working solution passes the Google API Key directly as plain text within the automation’s YAML configuration. This is less secure. Implement only if you understand and accept the risk within your network environment. AppDaemon is recommended as a more secure alternative.
  • Gemini Model & Safety Filters: This guide uses the gemini-2.0-flash-exp-image-generation model via the :streamGenerateContent endpoint. Testing revealed this specific model has very strict safety filters via the REST API, often refusing seemingly harmless prompts. Your results may vary. You might need to experiment heavily with prompts or find a different, stable image generation model name in Google’s current documentation and adapt the script.

Cost: Using Google AI APIs can incur costs. Check Google Cloud pricing and ensure billing is enabled for your project if required.

Prerequisites Checklist

  1. Home Assistant: A running instance.
  2. HACS: Installed and working.
  3. PyScript Integration: Installed via HACS and added via HA Integrations. Restart HA after install. Check PyScript config if requests or yaml imports fail later (Allow All Imports might be needed).
  4. button-card: Installed via HACS (Frontend section). Refresh browser after install.
  5. Weather Entity: A working weather integration providing an entity (e.g., weather.your_location). Note its entity_id. This guide uses weather.forecast_home.
  6. Sun Integration: Enabled (usually via default_config:). Provides sun.sun.
  7. Google Cloud Project & API Key: Project created, “Generative Language API” Enabled, API Key generated (and restricted if possible).
  8. secrets.yaml Entry (Recommended): Store your API Key in /config/secrets.yaml for reference, even though it will be pasted into the automation.
# /config/secrets.yaml
gemini_api_key: YOUR_GOOGLE_API_KEY_HERE
  1. www Directory: The /config/www directory exists.

  2. Libraries for PyScript: Ensure requests and PyYAML are available (usually are, check PyScript docs if import fails).

Step 1: The PyScript Script (generate_gemini.py)

This script handles the core logic: dynamic prompt generation, API call, response parsing, image decoding, and saving.

  1. Create the directory /config/pyscript/ if needed.
  2. Create a file named generate_gemini.py inside /config/pyscript/.
  3. Paste the entire following code into generate_gemini.py. Customize the prompt components as desired.
# Filename: /config/pyscript/generate_gemini.py
# Version: v24.3 (Final Corrected - Uses sun.sun, api_key arg, io.open save)

# Required Imports
import requests
import base64
import os
import json
import io
# import yaml # Not needed

# --- Helper Function for Saving Binary File ---
def _save_binary_file(path, data_bytes):
    """Standard Python helper function for binary writing using io.open."""
    try:
        with io.open(path, "wb") as f: f.write(data_bytes)
        return True # Success
    except Exception as e: return e # Return exception

# --- Main PyScript Service Function ---
@service
def generate_gemini_image(condition=None, temperature=None, sun_state=None, sun_elevation=None, api_key=None):
    """
    Generates an image using the Gemini API (v24.3) based on weather and sun position, then saves it locally.
    Uses API Key passed as an argument.
    Args:
        condition (str): Standard HA weather condition.
        temperature (float/int): Current temperature.
        sun_state (str): State of sun.sun ('above_horizon'/'below_horizon').
        sun_elevation (float): Sun's elevation in degrees.
        api_key (str): Gemini API Key.
    """
    log.info(f"PyScript: Received request (v24.3) condition='{condition}', temp='{temperature}', sun='{sun_state}'({sun_elevation}°)")

    # --- Validate Input Arguments ---
    if not condition: log.error("PyScript: Condition missing!"); return
    if temperature is None: log.error("PyScript: Temperature missing!"); return
    if not sun_state: log.error("PyScript: Sun state missing!"); return
    if sun_elevation is None: log.error("PyScript: Sun elevation missing!"); return
    if not api_key: log.error("PyScript: API Key missing!"); return
    if not isinstance(api_key, str) or len(api_key) < 10: log.error(f"PyScript: Invalid API Key provided."); return

    # --- Generate Dynamic Prompt using Weather and Sun ---
    subject = "a very cute, happy and playful kitten"
    base_style = "whimsical illustration style, sharp focus, high detail, crisp image"
    weather_action_scene = ""
    time_of_day_desc = ""
    generated_prompt = ""
    try:
        condition_lower = condition.lower()
        elevation = float(sun_elevation)
        # Determine Time of Day
        if sun_state == "below_horizon":
            if condition_lower == "clear-night": time_of_day_desc = "under a clear starry night sky"
            else: time_of_day_desc = "at night"
        elif elevation < 6 and elevation >= -1: time_of_day_desc = "during sunrise or sunset, beautiful golden hour lighting"
        elif elevation >= 60: time_of_day_desc = "under the bright midday sun"
        else: time_of_day_desc = "during the day"
        # Map Weather Condition to Scene/Action
        if condition_lower == "sunny": weather_action_scene = "playing happily in the sunbeams, in a magical landscape"
        elif condition_lower == "clear-night": weather_action_scene = "looking amazed at the stars"
        # ... (Include all other elif conditions for weather here as in previous versions) ...
        elif condition_lower == "cloudy": weather_action_scene = "sitting peacefully under overcast fluffy clouds"
        elif condition_lower == "partlycloudy": weather_action_scene = "peeking playfully from behind a fluffy white cloud"
        elif condition_lower == "rainy": weather_action_scene = "holding a tiny colorful umbrella during a light rain shower"
        elif condition_lower == "pouring": weather_action_scene = "splashing happily in puddles during a heavy downpour (wearing tiny rain boots!)"
        elif condition_lower == "snowy": weather_action_scene = "building a tiny snowman in a snowy winter scene"
        elif condition_lower == "snowy-rainy": weather_action_scene = "looking confused at the mix of rain and snow falling"
        elif condition_lower == "hail": weather_action_scene = "hiding quickly under a large mushroom during a hail storm"
        elif condition_lower == "lightning": weather_action_scene = "watching distant lightning flashes from a safe, cozy spot"
        elif condition_lower == "lightning-rainy": weather_action_scene = "watching safely from a cozy window during a thunderstorm"
        elif condition_lower == "fog": weather_action_scene = "exploring cautiously through a thick magical fog"
        elif condition_lower == "windy" or condition_lower == "windy-variant": weather_action_scene = "holding onto its tiny hat on a very windy day"
        elif condition_lower == "exceptional": weather_action_scene = f"reacting surprised to exceptional weather conditions"
        else: weather_action_scene = f"experiencing {condition} weather" # Fallback
        # Combine prompt elements
        generated_prompt = f"{subject} {weather_action_scene} {time_of_day_desc}. {base_style}. Square aspect ratio."
        generated_prompt = generated_prompt.replace("  ", " ").strip()
        log.info(f"PyScript: Dynamically generated prompt: {generated_prompt}")
    except Exception as e:
        log.error(f"PyScript: Error during dynamic prompt generation: {e}")
        generated_prompt = f"{subject} in {condition} weather. {base_style}. Square aspect ratio." # Fallback

    # --- Prepare API Call Details ---
    model_name = "gemini-2.0-flash-exp-image-generation"; action = "streamGenerateContent"
    api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:{action}?key={api_key}"
    headers = {'Content-Type': 'application/json'}
    payload = { "contents": [ {"role": "user", "parts": [ {"text": generated_prompt} ] } ], "generationConfig": { "responseModalities": ["IMAGE", "TEXT"] } }

    # --- Call Gemini API & Parse Response ---
    response_data = None
    try:
        log.info(f"PyScript: Sending request to Gemini API ({action})...")
        response = task.executor(requests.post, api_endpoint, headers=headers, json=payload, timeout=120)
        response.raise_for_status(); full_response_text = response.text
        log.debug(f"PyScript DEBUG: Raw API response text: {full_response_text}")
        try: # Parse array/object
            response_data_list = json.loads(full_response_text)
            if isinstance(response_data_list, list) and len(response_data_list) > 0: response_data = response_data_list[0]; log.info("PyScript: Parsed JSON array response.")
            elif isinstance(response_data_list, dict): response_data = response_data_list; log.info("PyScript: Parsed JSON single object response.")
            else: raise ValueError("Response is not a valid JSON array or object.")
        except Exception as e: log.error(f"PyScript: Failed to parse JSON: {e}"); return
    except Exception as e: log.error(f"PyScript: Error during API call: {e}", exc_info=True); return

    # --- Extract, Decode Image Data ---
    if response_data is None: log.error(f"PyScript: Invalid JSON data after parsing."); return
    base64_image_data = None; image_bytes = None
    try: # Extract Base64
        if 'candidates' in response_data and len(response_data['candidates']) > 0:
             candidate = response_data['candidates'][0]
             if 'content' in candidate and 'parts' in candidate['content'] and len(candidate['content']['parts']) > 0:
                 for part in candidate['content']['parts']:
                     if 'inlineData' in part and isinstance(part['inlineData'], dict) and 'data' in part['inlineData']: base64_image_data = part['inlineData']['data']; log.info("PyScript: Base64 data extracted."); break
        # Check if image was found or if API refused/errored
        if base64_image_data: image_bytes = base64.b64decode(base64_image_data); log.info("PyScript: Base64 data decoded.")
        else: # If no image data, log refusal/text and exit
            log.warning("PyScript: No 'inlineData' found in response.")
            try:
                if 'promptFeedback' in response_data and 'blockReason' in response_data['promptFeedback']: log.error(f"PyScript: BLOCKED by safety filter: {response_data['promptFeedback']['blockReason']}")
                elif 'error' in response_data: log.error(f"PyScript: API returned error: {response_data['error']}")
                else: text_part = response_data['candidates'][0]['content']['parts'][0]['text']; log.warning(f"PyScript: API returned text: {text_part[:300]}...")
            except Exception: log.warning(f"PyScript: Cannot parse text/error from response. Parsed data (partial): {str(response_data)[:1000]}")
            return
    except base64.binascii.Error as e: log.error(f"PyScript: Error decoding Base64: {e}"); return
    except Exception as e: log.error(f"PyScript: Error during extraction/decoding: {e}", exc_info=True); return

    # --- Save Image File ---
    if image_bytes:
        output_filename = "gemini_pyscript_image.png"; output_path = os.path.join("/config/www", output_filename)
        log.info(f"PyScript: Saving image to {output_path} using io.open directly...")
        try:
            # Use standard Python io.open directly in the main thread
            with io.open(output_path, "wb") as f:
                 f.write(image_bytes) # Write the image bytes
            log.info(f"PyScript: Image saved successfully (using direct io.open).")
            # Optionally notify Home Assistant UI
            service.call('persistent_notification', 'create', title="Gemini Weather Image", message=f"Image for '{condition}' generated.")
        except PermissionError as pe: log.error(f"PyScript: PERMISSION ERROR writing to {output_path}. Details: {pe}", exc_info=True)
        except IOError as e: log.error(f"PyScript: I/O ERROR writing image file (io.open): {e}", exc_info=True)
        except Exception as e: log.error(f"PyScript: Unexpected error writing file (io.open): {e}", exc_info=True)
    else:
        # This should not be reachable if the logic above is correct
        log.error("PyScript: Logical error - image_bytes is None, cannot save file.")
  1. Save the file.
  2. Reload PyScript: Go to Developer Tools → Server Management → YAML configuration reloading → PyScript → Reload.


Step 2: Create the Cache-Busting Helper

This input_text helper ensures the Lovelace card updates reliably.

  1. Go to Settings → Devices & Services → Helpers tab.
  2. Click + CREATE HELPER.
  3. Choose Text.
  4. Name: Gemini Image Timestamp (or choose your own).
  5. Note the Entity ID created (e.g., input_text.gemini_image_timestamp). You’ll need it below.
  6. Click CREATE.


Step 3: Create the Automation

This automation triggers the PyScript service hourly and on weather changes.

  1. Go to Settings → Automations & Scenes → Automations tab.
  2. Click + CREATE AUTOMATION → Start with an empty automation.
  3. Click the 3-dots menu (top right) → Edit in YAML.
  4. Replace the entire content with the following YAML code.
  5. IMPORTANT - EDIT THESE VALUES:
  • Replace weather.forecast_home (used 3 times) with your weather entity ID.
  • Replace YOUR_GOOGLE_API_KEY_HERE with your actual API key string (copied from secrets.yaml or entered directly). Remember the security warning!
  • Replace input_text.gemini_image_timestamp with the entity ID of the helper you created in Step 2.
# Automation YAML
alias: Generate Gemini Weather Image (Hourly + Change)
description: Updates the Gemini AI weather image via PyScript
# Run actions sequentially, dropping older triggers if previous run is still active
mode: queued
max_exceeded: silent

trigger:
  # Trigger 1: Every hour at 1 minute past the hour (adjust trigger time as needed)
  - platform: time_pattern
    hours: "/1"
    minutes: 1
    seconds: 0
  # Trigger 2: When the weather condition state changes
  - platform: state
    entity_id: weather.forecast_home #<-- YOUR WEATHER ENTITY ID HERE

condition: [] # No conditions

action:
  # Action 1: Call the PyScript service to generate/save image
  - service: pyscript.generate_gemini_image
    data:
      # Pass current weather state
      condition: "{{ states('weather.forecast_home') }}" #<-- YOUR WEATHER ENTITY ID HERE
      # Pass current temperature attribute
      temperature: "{{ state_attr('weather.forecast_home', 'temperature') | float(0) }}" #<-- YOUR WEATHER ENTITY ID HERE
      # Pass sun state for time of day context
      sun_state: "{{ states('sun.sun') }}"
      # Pass sun elevation for time of day context
      sun_elevation: "{{ state_attr('sun.sun', 'elevation') | float(0) }}"
      # WARNING: API Key in plain text - Less Secure!
      # !!! REPLACE WITH YOUR ACTUAL API KEY !!!
      api_key: "YOUR_GOOGLE_API_KEY_HERE"

  # Action 2: Update the helper text to trigger cache busting in Lovelace
  # This runs AFTER the pyscript service call attempts
  - service: input_text.set_value
    target:
      # !!! USE YOUR HELPER'S ENTITY ID !!!
      entity_id: input_text.gemini_image_timestamp
    data:
      # Set value to the current timestamp as an integer
      value: "{{ now().timestamp() | int }}"

  # Action 3: Force update the camera entity (Removed - not needed with button-card)
  # We removed the camera_proxy method in favor of button-card + markdown img
  1. Click Save. Name the automation.
  2. Reload Automations: Go to Developer Tools → Server Management → YAML configuration reloading → Automations → Reload.


Step 4: Create the Lovelace Card

This uses custom:button-card from HACS to display the image dynamically (using the helper for cache busting) and handle the tap action correctly.

  1. Ensure button-card is installed via HACS (see Prerequisites).
  2. Go to your Lovelace dashboard, enter Edit Mode.
  3. Click + ADD CARD.
  4. Choose the “Manual” card type.
  5. Replace the content with the following YAML.
  6. IMPORTANT:
  • Replace weather.forecast_home with your weather entity ID.
  • Replace input_text.gemini_image_timestamp with your helper’s entity ID.
# Lovelace Card using custom:button-card
type: custom:button-card
# Entity used for the more-info tap action context
entity: weather.forecast_home #<-- YOUR WEATHER ENTITY ID

# Hide default button elements
show_name: false
show_icon: false
show_state: false
show_label: false

# Define the tap action for the whole card (opens more-info for the entity above)
tap_action:
  action: more-info

# Style the card itself (remove padding, add border radius matching HA cards)
styles:
  card:
    - padding: "0px"
    - border-radius: "var(--ha-card-border-radius, 12px)"
    - overflow: "hidden" # Hide image corners if they don't fit the radius

# Use custom_fields to inject the HTML for the dynamic image
custom_fields:
  # Define a field named 'img' (can be any name)
  img: >
    [[[
      // Use button-card's JavaScript templating capability
      // Get the state of the cache-busting helper entity
      const cacheBuster = states['input_text.gemini_image_timestamp'].state; // <-- YOUR HELPER ENTITY ID
      // Construct the img tag. Use Date.now() as a fallback cache value if helper state is briefly unavailable.
      const cacheValue = cacheBuster || Date.now();
      // The image path must match where PyScript saves the file (/config/www -> /local/)
      // style ensures the image fills the card area
      return `<img src="/local/gemini_pyscript_image.png?v=${cacheValue}" style="width: 100%; height: 100%; display: block; object-fit: cover;" alt="Weather Image">`;
    ]]]
  1. Click Save.
  2. Position the card and exit Edit Mode (Done).


Final Result & Next Steps

You should now have a fully functional, dynamic AI weather card! The automation triggers image generation based on time and weather changes, PyScript calls Gemini, the image is saved, the helper forces a cache update, and the button-card displays the latest image and handles clicks correctly.

Possible next steps:

  • Fine-tune prompts: Adjust the subject, base_style, and weather_action_scene logic in the PyScript file for different artistic outcomes.
  • Improve Error Handling: The script currently logs errors if Gemini refuses or fails. You could enhance the Lovelace card (perhaps using a conditional card or button-card state_filter) to show a default image or hide the card when the gemini_pyscript_image.png hasn’t been updated recently or if generation fails.
  • Explore AppDaemon: If the API key security is a major concern, consider migrating this logic to AppDaemon for standard secret handling.
  • Try Different Models: If the gemini-2.0-flash-exp… model’s safety filters are too restrictive, look for currently recommended stable image generation model names in the Google AI documentation and update the model_name and potentially the api_endpoint/payload in the script.

Note on Temperature Argument:
You might notice that the automation passes the current temperature to the pyscript.generate_gemini_image service, and the script accepts it as an argument (temperature). However, in the final prompt generation logic provided (generated_prompt = f"{subject} {weather_action_scene} {time_of_day_desc}. {base_style}. Square aspect ratio."), the temperature variable is not directly used to modify the text sent to the Gemini API.

It was kept intentionally to allow for future flexibility. You could easily modify the Python if/elif block to incorporate temperature nuances into your prompts (e.g., adding phrases like “on a cold windy day”, “during a hot sunny afternoon”, “bundled up warmly in the snow”) without needing to change the automation’s service call again. For now, it provides context that isn’t visually rendered or used in the default prompt generation.

Important Note on Gemini API Limits & Cost:
This tutorial utilizes the Google Generative Language API (specifically the generativelanguage.googleapis.com endpoint with the gemini-2.0-flash-exp-image-generation model during testing). The limits for generating images via this specific public API endpoint (e.g., requests per minute, images per day) using a standard API key are not clearly documented by Google at the time of writing, especially concerning free tiers or how usage relates to consumer subscriptions like Google One / Gemini Advanced.

While a Google One subscription provides access to Gemini Advanced features (often through the chatbot interface), it does not necessarily guarantee unlimited or extensive free API usage for all models, particularly computationally intensive tasks like image generation. I have such a subscription and I was able to generate images via the API key, but the exact free quota or rate limits remain unclear. Also with no subscriptions image generation is working… Official information found suggested limits might depend on usage frequency and intervals.

So users should be aware that:

  • They might encounter undocumented rate limits (e.g., temporary errors after several requests) or quotas (e.g., images per day).
  • Depending on the specific model used, API terms of service, and usage volume, costs might be incurred. This is more likely if using models explicitly designated as paid services, such as those typically accessed via the Vertex AI platform (like Imagen 3).
  • It’s highly recommended to monitor your API usage and billing within your Google Cloud Console project associated with the API key.
  • Consult the official Google Cloud pricing pages for the “Generative Language API” and “Vertex AI” for the most current information on potential costs and quotas.

Final Thoughts

This project was quite a journey, involving deep dives into PyScript’s execution environment, Gemini API behaviors (including its sometimes overzealous safety filters), and Lovelace caching intricacies. While the path wasn’t always straightforward, the final result – a dynamically updating AI-generated weather image – is quite rewarding.

I hope this detailed walkthrough sparks your interest and encourages you to try implementing this in your own Home Assistant setup! Your experience might differ based on your specific HA version, PyScript configuration, the Gemini models available to you, and especially the prompts you design. Experiment with the code, refine the prompts, and please share your results and any improvements with the Home Assistant community! Good luck!

Did I forget a screenshot? :slight_smile:

2 Likes

Thanks for sharing.
I followed the instructions but the automation says that pyscript.generate_gemini_image is unknown.

2025-04-02 07:39:18.352 ERROR (MainThread) [custom_components.pyscript.file.generate_gemini] Exception in </config/pyscript/generate_gemini.py> line 153:
    except Exception as e:
    ^
SyntaxError: invalid syntax (generate_gemini.py, line 153)

Thank you for pointing it out! You are right, the posted script version had an indentation error. It has been corrected and should function correctly now.

Working now, thanks for the fix.
Love it. :heart_eyes:

So do I. The testing result with the posted script…


:grinning:

Something seems to go wrong with day/night recognition: currently it is 09:06 am (CEST) and I am getting a nightly image (cat starring at night sky with stars).
Timestamp helper value is 1743577263, weather condition is cloudy.

Thanks for reporting this! That’s definitely unexpected behavior for a daytime cloudy condition.

To help diagnose, could you please check the exact prompt that was sent to the Gemini API when this happened?

  1. Make the prompt log more visible: Please edit your /config/pyscript/generate_gemini.py file. Find the line (around line 98 in the tutorial code): log.info(f"PyScript: Dynamically generated prompt: {generated_prompt}") and change log.info to log.warning.
  2. Save & Reload: Save the file and reload PyScript (Developer Tools → Server Management → PyScript → Reload).
  3. Trigger & Check Logs: Trigger the automation again (manually or wait for it). Then, check your Home Assistant logs (Settings → System → Logs) for the warning level message (usually highlighted yellow/orange) starting with PyScript: Dynamically generated prompt: ....
  4. Share the Prompt: Could you please share the exact prompt text that was logged?

Just to clarify, the input_text helper mentioned in your comment (Timestamp helper value is...) is only used to force the Lovelace card image to refresh (cache busting) and doesn’t influence the day/night logic or the prompt generation itself.

Knowing the exact prompt that was sent when you received the nighttime image will help determine if the issue was in the prompt generation logic under those specific conditions, or if it was purely the AI model misinterpreting a correctly generated daytime prompt.

Thanks for helping troubleshoot!

I will show it on the wall tablet. My family will love it. Nice idea, thank you for sharing!

2025-04-02 09:56:42.965 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Dynamically generated prompt: a very cute, happy and playful kitten sitting peacefully under overcast fluffy clouds during the day. whimsical illustration style, sharp focus, high detail, crisp image. Square aspect ratio.

Generated image lookng better now:

2025-04-02 10:45:00.233 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Dynamically generated prompt: a very cute, happy and playful kitten sitting peacefully under overcast fluffy clouds during the day. whimsical illustration style, sharp focus, high detail, crisp image. Widescreen aspect ratio.

Nightly image again ! So, it seems that Gemini AI is doing wrong sometimes.

Update & Refinements for the Gemini AI Weather Card

Hi everyone,

Following up on my initial guide for the AI Weather Card, I wanted to share several improvements implemented based on further testing and great suggestions, aiming to make the setup more robust, flexible, and user-friendly. A big thank you to Google’s Gemini AI for the extensive collaboration during the troubleshooting involved!

Here’s a summary of the key enhancements:

1. Simplified & More Accurate Time of Day Handling

I found the initial idea of approximating daylight phases using only sun.sun elevation thresholds was difficult to get right across different seasons, especially at mid-latitudes (like my location around 45°N). A much cleaner and more reliable approach uses a dedicated Template Sensor to provide distinct, simpler phases based on the sun’s state (above_horizon/below_horizon) and a low elevation check for sunrise/sunset.

  • Create/Update Template Sensor: I recommend adding the following sensor configuration. You can place it directly in configuration.yaml (under the template: key) or in a separate templates.yaml file (as a new list item under - sensor:). This creates the entity sensor.simple_daylight_phase.
# YAML for the Template Sensor (in templates.yaml or configuration.yaml)
template:
  - sensor:
      - name: "Simple Daylight Phase"     # Name of the sensor in the UI
        unique_id: sensor_simple_daylight_phase_gemini # Unique ID for Home Assistant
        icon: mdi:theme-light-dark      # Optional icon
        state: >
          {# This Jinja2 template calculates the state based on sun #}
          {% set sun_state = states('sun.sun') %}
          {% set elevation = state_attr('sun.sun', 'elevation') | float(0) %}
          {% set current_hour = now().hour %} {# Get the current hour (0-23) #}

          {# 1. Check if it's Night #}
          {% if sun_state == 'below_horizon' %}
            Night
          {# 2. Check if it's Sunrise/Sunset (Sun is very low) #}
          {# Adjust the '8' degree threshold if needed for your location/preference #}
          {% elif elevation < 8 and elevation >= -1 %}
            {% if current_hour < 12 %} {# Simple check: If before noon, assume Sunrise #}
              Sunrise
            {% else %} {# Otherwise, assume Sunset #}
              Sunset
            {% endif %}
          {# 3. If it's daytime and sun is higher, consider it simply 'Day' #}
          {% else %}
            Day
          {% endif %}

Remember to Reload Template Entities or Restart HA after adding/modifying.

  • Update PyScript: The generate_gemini_image function was updated to accept a daylight_phase argument instead of sun_state and sun_elevation. The internal logic now uses this simpler state to add time context (like “at night, dark scene”, “during a bright daytime scene”, etc.) to the prompt. (See the full final script below for the implementation).
  • Update Automation: The automation trigger should now include sensor.simple_daylight_phase state changes. The action must be updated to pass the state of this new sensor to the PyScript service using the daylight_phase key.
# --- Snippet from Automation YAML ---
trigger:
  # 1. On weather condition change
  - platform: state
    entity_id: weather.forecast_home #<-- YOUR WEATHER ENTITY ID
  # 2. On simple daylight phase change
  - platform: state
    entity_id: sensor.simple_daylight_phase #<-- YOUR PHASE SENSOR ID

action:
  - service: pyscript.generate_gemini_image
    data:
      condition: "{{ states('weather.forecast_home') }}" #<-- YOUR WEATHER ENTITY ID
      temperature: "{{ state_attr('weather.forecast_home', 'temperature') | float(0) }}" #<-- YOUR WEATHER ENTITY ID
      # Pass the state of the new simplified sensor
      daylight_phase: "{{ states('sensor.simple_daylight_phase') }}" #<-- YOUR PHASE SENSOR ID
      # WARNING: API Key in plain text!
      api_key: "YOUR_GOOGLE_API_KEY_HERE" #<-- YOUR API KEY
  # ... (action to update input_text helper remains) ...

2. API Call Retry Logic

To make the image generation more resilient to temporary network glitches or API timeouts from Google (which sometimes occurred during testing, often as 503 Service Unavailable or ReadTimeout errors):

  • PyScript Change: The requests.post call inside the generate_gemini_image function is now wrapped in a for loop (currently set for max_attempts = 2). It specifically catches requests.exceptions.Timeout and general requests.exceptions.RequestException. If one of these occurs on the first attempt, it logs a warning, waits 5 seconds using PyScript’s built-in task.sleep(5), and then retries the API call once more. If the second attempt also fails, or if a different error occurs (like HTTP 4xx/5xx errors or JSON parsing failures), it logs the failure and exits gracefully without retrying further. (See the full final script below for implementation details).

3. Manual Trigger via Card (Hold Action)

To provide an easy way to trigger an image update on demand directly from the dashboard:

  • Lovelace Card Change: A hold_action was added to the custom:button-card configuration. This calls the automation.trigger service to run the image generation automation immediately when the card is long-pressed. Remember to replace the entity_id with your automation’s actual entity ID (you can find it in Developer Tools → States).
# --- Snippet from Lovelace button-card YAML ---
hold_action: # Action on long press
  action: call-service
  service: automation.trigger # Service to trigger an automation
  target:
    # !!! REPLACE with your automation's ACTUAL entity_id !!!
    entity_id: automation.generate_gemini_weather_image_on_change #<-- YOUR AUTOMATION'S ID HERE
  # Optional: Add haptic feedback for mobile devices
  # haptic: success

4. Optional Image Archiving

To keep a history of the generated images instead of just overwriting the single gemini_pyscript_image.png file used by the card:

  • PyScript Change: The script now includes an option to save a timestamped copy of each successfully generated image to a separate archive directory.
    • Set the archive_images = True (or False) flag near the top of the generate_gemini.py script to enable/disable this feature.
    • You can change the archive_dir_relative = "www/gemini_archive" variable to customize the sub-directory within /config/ where archives are stored (e.g., media/gemini_archive). The script will attempt to create this directory using os.makedirs if it doesn’t exist (ensure Home Assistant has permission).
    • The file saving logic now includes an extra step using io.open to save a copy with a filename like gemini_YYYYMMDD_HHMMSS_condition_phase.png after the main image is saved successfully. (See the full final script below).
    • Note: Be mindful of disk space usage if archiving is enabled, as images can accumulate over time. You might want to implement a separate mechanism to clean up old archive files periodically.

Final PyScript Code (generate_gemini.py - v27.1)

Here is the complete, final code for /config/pyscript/generate_gemini.py incorporating all the refinements discussed above. Make sure you have this version saved and that PyScript has been reloaded.

# Filename: /config/pyscript/generate_gemini.py
# Version: v27.1 (FINAL - Includes Archiving + All Fixes)

# Required Imports
import requests
import base64
import os
import json
import io
from datetime import datetime # Import datetime for timestamp generation

# --- Configuration ---
# Set to True to save a unique copy of each generated image, False to only overwrite the main image
archive_images = True
# Directory where archived images will be saved (relative to /config)
# Example: /config/www/gemini_archive/ makes it accessible via /local/gemini_archive/
archive_dir_relative = "www/gemini_archive"
archive_dir_full_path = os.path.join("/config", archive_dir_relative)

# --- Helper Function for Saving Binary File ---
# Uses io.open, potentially run via task.executor but used directly here for saving.
def _save_binary_file(path, data_bytes):
    """Standard Python helper function for binary writing using io.open."""
    try:
        # Ensure the target directory exists before writing
        dir_name = os.path.dirname(path)
        if dir_name:
             # Create directory if it doesn't exist.
             os.makedirs(dir_name, exist_ok=True)
        # Use io.open for reliable binary file writing
        with io.open(path, "wb") as f:
            f.write(data_bytes)
        return True # Indicate success
    except Exception as e:
        # Return the exception object if saving fails
        # Use print as log might not be available if run by executor in other contexts
        print(f"ERROR in _save_binary_file writing to {path}: {e}")
        return e

# --- Main PyScript Service Function ---
# Registered as service: pyscript.generate_gemini_image
@service
def generate_gemini_image(condition=None, temperature=None, daylight_phase=None, api_key=None):
    """
    Generates an image using the Gemini API (v27.1) based on weather and daylight phase.
    Includes retry logic. Saves main image and optionally archives a copy.
    Args:
        condition (str): Standard HA weather condition.
        temperature (float/int): Current temperature (available for potential future prompt use).
        daylight_phase (str): State from sensor.simple_daylight_phase (Night, Sunrise, Day, Sunset).
        api_key (str): Gemini API Key (passed directly from automation).
    """
    global archive_images, archive_dir_full_path # Make config vars accessible

    log.info(f"PyScript: Received request (v27.1 - archive) condition='{condition}', temp='{temperature}', phase='{daylight_phase}'")

    # --- Validate Input Arguments ---
    if not condition: log.error("PyScript: Condition missing!"); return
    if temperature is None: log.error("PyScript: Temperature missing!"); return
    if not daylight_phase: log.error("PyScript: Daylight Phase missing!"); return
    if not api_key: log.error("PyScript: API Key missing!"); return
    if not isinstance(api_key, str) or len(api_key) < 10: log.error(f"PyScript: Invalid API Key provided."); return

    # --- Generate Dynamic Prompt ---
    # <<< --- CUSTOMIZE PROMPT COMPONENTS HERE --- >>>
    subject = "a very cute, happy and playful kitten"
    base_style = "whimsical illustration style, sharp focus, high detail, crisp image"
    weather_action_scene = ""; time_context_desc = ""; generated_prompt = ""
    try:
        condition_lower = condition.lower(); phase_lower = daylight_phase.lower()
        # Determine Time Context
        if phase_lower == "night": time_context_desc = "at night, dark scene"
        elif phase_lower == "sunrise": time_context_desc = "during a beautiful sunrise with golden hour light"
        elif phase_lower == "sunset": time_context_desc = "during a beautiful sunset with warm golden light"
        elif phase_lower == "day": time_context_desc = "during a bright daytime scene"
        else: time_context_desc = f"({daylight_phase})" # Fallback
        # Map Weather Scene
        if condition_lower == "sunny": weather_action_scene = "playing happily in the sunbeams in a magical landscape"
        elif condition_lower == "clear-night": weather_action_scene = "looking amazed at the starry sky"
        elif condition_lower == "cloudy": weather_action_scene = "sitting peacefully under overcast fluffy clouds"
        elif condition_lower == "partlycloudy": weather_action_scene = "peeking playfully from behind a fluffy white cloud"
        elif condition_lower == "rainy": weather_action_scene = "holding a tiny colorful umbrella during a light rain shower"
        elif condition_lower == "pouring": weather_action_scene = "splashing happily in puddles during a heavy downpour"
        elif condition_lower == "snowy": weather_action_scene = "building a tiny snowman in a snowy winter scene"
        elif condition_lower == "snowy-rainy": weather_action_scene = "looking confused at the mix of rain and snow falling"
        elif condition_lower == "hail": weather_action_scene = "peeking out from under a large mushroom while visible hailstones fall heavily"
        elif condition_lower == "lightning": weather_action_scene = "watching distant lightning flashes from a safe, cozy spot"
        elif condition_lower == "lightning-rainy": weather_action_scene = "watching safely from a cozy window during a thunderstorm"
        elif condition_lower == "fog": weather_action_scene = "exploring cautiously through a thick magical fog"
        elif condition_lower == "windy" or condition_lower == "windy-variant": weather_action_scene = "holding onto its tiny hat on a very windy day"
        elif condition_lower == "exceptional": weather_action_scene = f"reacting surprised to exceptional weather conditions"
        else: weather_action_scene = f"experiencing {condition} weather" # Fallback
        # Combine prompt
        if condition_lower == "clear-night": generated_prompt = f"{subject} {weather_action_scene}. {base_style}. Square aspect ratio."
        else: generated_prompt = f"{subject} {weather_action_scene} {time_context_desc}. {base_style}. Square aspect ratio."
        generated_prompt = generated_prompt.replace("  ", " ").replace("..", ".").strip(); log.info(f"PyScript: Dynamically generated prompt: {generated_prompt}")
    except Exception as e: log.error(f"PyScript: Error prompt generation: {e}"); generated_prompt = f"{subject} in {condition} weather. {base_style}. Square aspect ratio." # Fallback

    # --- Prepare API Call Details ---
    model_name = "gemini-2.0-flash-exp-image-generation"; action = "streamGenerateContent"
    api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:{action}?key={api_key}"; headers = {'Content-Type': 'application/json'}
    payload = { "contents": [ {"role": "user", "parts": [ {"text": generated_prompt} ] } ], "generationConfig": { "responseModalities": ["IMAGE", "TEXT"] } }

    # --- Call Gemini API & Parse Response (with Retry Logic) ---
    response_data = None; max_attempts = 2
    for attempt in range(max_attempts):
        try:
            log.info(f"PyScript: Sending request to API (Attempt {attempt + 1}/{max_attempts})...")
            response = task.executor(requests.post, api_endpoint, headers=headers, json=payload, timeout=240) # Keep long timeout
            response.raise_for_status(); # Check 4xx/5xx - no retry for these
            full_response_text = response.text; log.debug(f"PyScript DEBUG: Raw API response text (Attempt {attempt + 1}): {full_response_text}")
            try: # Parse response JSON (expecting list or dict)
                response_data_list = json.loads(full_response_text)
                if isinstance(response_data_list, list) and len(response_data_list) > 0: response_data = response_data_list[0]
                elif isinstance(response_data_list, dict): response_data = response_data_list
                else: raise ValueError("Response not list/dict.")
                log.info(f"PyScript: Parsed JSON (Attempt {attempt + 1})."); break # SUCCESS -> Exit Loop
            except Exception as e: log.error(f"PyScript: Failed parse JSON (Attempt {attempt + 1}): {e}"); return # Exit if parsing fails
        except requests.exceptions.Timeout as e: # Catch Timeout for retry
            log.warning(f"PyScript: Timeout API (Attempt {attempt + 1}): {e}")
            if attempt + 1 == max_attempts: log.error("PyScript: Timeout on final attempt."); return
            log.info("PyScript: Waiting 5 seconds before retry..."); task.sleep(5) # PyScript async sleep
        except requests.exceptions.RequestException as e: # Catch other Network/Request errors for retry
            log.warning(f"PyScript: Network/Req Error API (Attempt {attempt + 1}): {e}")
            if attempt + 1 == max_attempts: log.error("PyScript: Network/Req Error on final attempt."); return
            log.info("PyScript: Waiting 5 seconds before retry..."); task.sleep(5)
        except Exception as e: log.error(f"PyScript: Unexpected API call error (Attempt {attempt + 1}): {e}", exc_info=True); return # No retry for other errors
    if response_data is None: log.error(f"PyScript: API call failed definitively after {max_attempts} attempts."); return

    # --- Extract, Decode Image Data ---
    if not isinstance(response_data, dict): log.error(f"PyScript: Invalid response_data."); return
    base64_image_data = None; image_bytes = None
    try: # Extract Base64
        if 'candidates' in response_data and len(response_data['candidates']) > 0:
             candidate = response_data['candidates'][0]
             if 'content' in candidate and 'parts' in candidate['content'] and len(candidate['content']['parts']) > 0:
                 for part in candidate['content']['parts']:
                     # Check if 'inlineData' exists and is a dict, then check for 'data' key
                     if 'inlineData' in part and isinstance(part.get('inlineData'), dict) and 'data' in part['inlineData']:
                         base64_image_data = part['inlineData']['data']; log.info("PyScript: Base64 data extracted."); break
        # Decode if found
        if base64_image_data: image_bytes = base64.b64decode(base64_image_data); log.info("PyScript: Base64 data decoded.")
        else: # Log refusal/text and exit if no image data found
            log.warning("PyScript: No 'inlineData' found in response.")
            try: # Try logging specific feedback from API
                if 'promptFeedback' in response_data and 'blockReason' in response_data['promptFeedback']: log.error(f"PyScript: BLOCKED by safety filter: {response_data['promptFeedback']['blockReason']}")
                elif 'error' in response_data: log.error(f"PyScript: API returned error: {response_data['error']}")
                else: text_part = response_data['candidates'][0]['content']['parts'][0]['text']; log.warning(f"PyScript: API returned text: {text_part[:300]}...")
            except Exception: log.warning(f"PyScript: Cannot parse text/error from response. Parsed data (partial): {str(response_data)[:1000]}")
            return # Stop if no image
    except base64.binascii.Error as e: log.error(f"PyScript: Error decoding Base64: {e}"); return
    except Exception as e: log.error(f"PyScript: Error during extraction/decoding: {e}", exc_info=True); return

    # --- Save Image File(s) ---
    if image_bytes:
        main_save_success = False # Flag to track main save status
        # --- 1. Save the main image for Lovelace (using direct io.open) ---
        main_filename = "gemini_pyscript_image.png"
        main_output_path = os.path.join("/config/www", main_filename)
        log.info(f"PyScript: Saving main image to {main_output_path} using io.open...")
        try:
            # Use direct io.open as it worked reliably for saving
            with io.open(main_output_path, "wb") as f:
                 f.write(image_bytes) # Write the image bytes
            log.info(f"PyScript: Main image saved successfully.")
            main_save_success = True # Set flag on success
            # Send notification AFTER main save is confirmed
            service.call('persistent_notification', 'create', title="Gemini Weather Image", message=f"Image for '{condition}' generated.")
        except PermissionError as pe: log.error(f"PyScript: PERMISSION ERROR writing main image {main_output_path}: {pe}", exc_info=True)
        except IOError as e: log.error(f"PyScript: I/O ERROR writing main image: {e}", exc_info=True)
        except Exception as e: log.error(f"PyScript: Unexpected error writing main image: {e}", exc_info=True)

        # --- 2. Optionally save an archive copy (using direct io.open) ---
        # Check if main save succeeded AND archiving is enabled by the flag at the top
        if main_save_success and archive_images:
            log.info("PyScript: Archiving enabled, attempting to save archive copy...")
            try:
                # Ensure the archive directory exists using standard Python os module
                # Note: Ensure HA has permissions to create this directory if it doesn't exist
                os.makedirs(archive_dir_full_path, exist_ok=True)

                # Generate unique filename using datetime.now() and sanitized inputs
                timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S')
                safe_condition = condition.lower().replace(' ', '_')[:15] # Sanitize
                safe_phase = daylight_phase.lower().replace(' ', '_')[:10] # Sanitize
                archive_filename = f"gemini_{timestamp_str}_{safe_condition}_{safe_phase}.png"
                archive_file_path = os.path.join(archive_dir_full_path, archive_filename)

                log.info(f"PyScript: Saving archive image to {archive_file_path} using io.open...")
                # Use direct io.open for archive as well
                with io.open(archive_file_path, "wb") as f_archive:
                    f_archive.write(image_bytes)
                log.info(f"PyScript: Archive image saved successfully.")

            # Catch potential errors during archiving process
            except PermissionError as pe_arch: log.error(f"PyScript: PERMISSION ERROR writing archive {archive_file_path}: {pe_arch}", exc_info=True)
            except IOError as e_arch: log.error(f"PyScript: I/O ERROR writing archive image: {e_arch}", exc_info=True)
            except Exception as e_arch: log.error(f"PyScript: Unexpected error during image archiving: {e_arch}", exc_info=True)
        elif main_save_success and not archive_images:
             log.info("PyScript: Archiving is disabled, skipping archive save.")
        # End of archiving block

    else:
        # This case means decoding failed or base64 data wasn't found initially
        log.error("PyScript: Logical error - image_bytes is None, cannot save file.")

Summary of Enhancements Implemented:

This final version includes several refinements compared to a basic setup:

  1. Smarter Triggers: The automation is no longer triggered only periodically. It now triggers more relevantly based on actual changes to the weather entity’s state and significant changes in the time of day detected by the sensor.simple_daylight_phase (catching shifts between Night, Sunrise, Day, Sunset). This provides more timely and context-appropriate image updates.
  2. Enhanced Prompt Context: Dynamic prompts sent to the Gemini API now incorporate time-of-day context derived from the sensor.simple_daylight_phase (e.g., adding “at night, dark scene”, “during a beautiful sunrise with golden hour light”, “bright daytime scene”). This is added alongside the weather condition details, aiming for more atmospheric and correctly lit images.
  3. API Call Robustness: The PyScript code includes a simple retry mechanism. If the initial API call to Gemini fails due to a temporary network error or timeout, the script will wait a few seconds and attempt the call one more time, increasing the chances of success during transient API or network issues.
  4. Manual Trigger: A hold_action (long press) has been added to the Lovelace custom:button-card, allowing users to manually trigger the image generation process on demand directly from the dashboard.
  5. Simplified Daylight Logic: Instead of complex calculations, a simple Home Assistant Template Sensor (sensor.simple_daylight_phase) uses sun.sun state and basic elevation checks to provide distinct Night, Sunrise, Day, and Sunset states, making the PyScript logic for time context cleaner.
  6. Optional Image Archiving: The PyScript includes a flag (archive_images) and logic to optionally save a timestamped copy of each generated image to a separate directory (e.g., /config/www/gemini_archive/), creating a visual history.

Final Thoughts

I hope this detailed walkthrough sparks your interest and encourages you to try implementing this in your own Home Assistant setup! Although it might seem a bit complex initially, you might find it’s quite manageable once you start. Your experience might differ based on your specific HA version, PyScript configuration, the Gemini models available to you, and especially the prompts you design. Experiment with the code, refine the prompts, and please share your results and any improvements with the Home Assistant community! Good luck!

1 Like

gemini-2.0-flash-exp-image-generation

is not available anymore (at least not in Germany).
So we have to use another model. I have tried to change the model to

gemini-2.0-flash

but its not working too.

2025-04-19 12:52:08.607 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Network/Req Error API (Attempt 2): 404 Client Error: Not Found for url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:streamGenerateContent?key=######

(api-key removed)

Image generation was removed for Europe …

But why is gemini-2.0-flash not working?
I got following response:

PyScript: Network/Req Error API (Attempt 2): 400 Client Error: Bad Request for url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?key=

perhaps there are different required field for the different models which have to be passes in the api calls.

Oh that’s really bad from Google.
I tried OpenAI alternatively, but with the same prompts their images are complete bull****.

A guy was able to workaround:

Adapting the AI Weather Card PyScript for Alternative Image Generation (e.g., using ir.myqa.cc)

Hi everyone,

It seems the experimental Google Gemini image generation model (google/gemini-2.0-flash-exp-image-generation) used in the previous version of the AI Weather Card script might no longer be available or accessible in all regions.

Here’s a potential workaround using a third-party image router service like ir.myqa.cc, which offers an OpenAI-compatible API endpoint and potentially free model tiers (like stabilityai/sdxl-turbo:free at the time of writing).

Disclaimer: ir.myqa.cc is a third-party service. You’ll need to sign up with them to get an API key. Please review their terms, pricing (beyond any free tiers), reliability, and data privacy policies before using. The quality and style of the generated images may differ from the original Gemini model.

Here are the steps to modify the setup:

1. Get an API Key:

  • Sign up for an account at ir.myqa.cc (or their relevant platform) and obtain an API key.

2. Modify the PyScript File (generate_gemini.py):

  • Open your existing pyscript file (e.g., /config/pyscript/generate_gemini.py).
  • Locate the function @service def generate_gemini_image(...).
  • delete the function and replace with the next code:
# --- Main PyScript Service Function ---
# Registered as service: pyscript.generate_gemini_image
@service
def generate_gemini_image(condition=None, temperature=None, daylight_phase=None, api_key=None):
    """
    Generates an image using the ir.myqa.cc API (v28.0 - adapted from Gemini v27.1)
    based on weather and daylight phase. Uses stabilityai/sdxl-turbo:free model.
    Includes retry logic. Saves main image and optionally archives a copy.
    Args:
        condition (str): Standard HA weather condition.
        temperature (float/int): Current temperature (available for potential future prompt use).
        daylight_phase (str): State from sensor.simple_daylight_phase (Night, Sunrise, Day, Sunset).
        api_key (str): ir.myqa.cc API Key (passed directly from automation).
    """
    global archive_images, archive_dir_full_path # Make config vars accessible

    log.info(f"PyScript: Received request (v28.0 - ir.myqa.cc) condition='{condition}', temp='{temperature}', phase='{daylight_phase}'")

    # --- Validate Input Arguments ---
    if not condition: log.error("PyScript: Condition missing!"); return
    if temperature is None: log.error("PyScript: Temperature missing!"); return
    if not daylight_phase: log.error("PyScript: Daylight Phase missing!"); return
    if not api_key: log.error("PyScript: API Key (for ir.myqa.cc) missing!"); return
    # Basic check for the API key format/length might need adjustment if keys differ significantly
    if not isinstance(api_key, str) or len(api_key) < 10:
        log.error(f"PyScript: Invalid API Key (for ir.myqa.cc) provided.")
        return

    # --- Generate Dynamic Prompt (Keep your existing logic) ---
    # <<< --- CUSTOMIZE PROMPT COMPONENTS HERE --- >>>
    subject = "a very cute, happy and playful kitten"
    base_style = "whimsical illustration style, sharp focus, high detail, crisp image"
    # --- (Keep the rest of your prompt generation logic exactly as it was) ---
    weather_action_scene = ""; time_context_desc = ""; generated_prompt = ""
    try:
        condition_lower = condition.lower(); phase_lower = daylight_phase.lower()
        # Determine Time Context
        if phase_lower == "night": time_context_desc = "at night, dark scene"
        elif phase_lower == "sunrise": time_context_desc = "during a beautiful sunrise with golden hour light"
        elif phase_lower == "sunset": time_context_desc = "during a beautiful sunset with warm golden light"
        elif phase_lower == "day": time_context_desc = "during a bright daytime scene"
        else: time_context_desc = f"({daylight_phase})" # Fallback
        # Map Weather Scene (keep your existing mappings)
        if condition_lower == "sunny": weather_action_scene = "playing happily in the sunbeams in a magical landscape"
        elif condition_lower == "clear-night": weather_action_scene = "looking amazed at the starry sky"
        elif condition_lower == "cloudy": weather_action_scene = "sitting peacefully under overcast fluffy clouds"
        elif condition_lower == "partlycloudy": weather_action_scene = "peeking playfully from behind a fluffy white cloud"
        elif condition_lower == "rainy": weather_action_scene = "holding a tiny colorful umbrella during a light rain shower"
        elif condition_lower == "pouring": weather_action_scene = "splashing happily in puddles during a heavy downpour"
        elif condition_lower == "snowy": weather_action_scene = "building a tiny snowman in a snowy winter scene"
        elif condition_lower == "snowy-rainy": weather_action_scene = "looking confused at the mix of rain and snow falling"
        elif condition_lower == "hail": weather_action_scene = "peeking out from under a large mushroom while visible hailstones fall heavily"
        elif condition_lower == "lightning": weather_action_scene = "watching distant lightning flashes from a safe, cozy spot"
        elif condition_lower == "lightning-rainy": weather_action_scene = "watching safely from a cozy window during a thunderstorm"
        elif condition_lower == "fog": weather_action_scene = "exploring cautiously through a thick magical fog"
        elif condition_lower == "windy" or condition_lower == "windy-variant": weather_action_scene = "holding onto its tiny hat on a very windy day"
        elif condition_lower == "exceptional": weather_action_scene = f"reacting surprised to exceptional weather conditions"
        else: weather_action_scene = f"experiencing {condition} weather" # Fallback
        # Combine prompt
        if condition_lower == "clear-night": generated_prompt = f"{subject} {weather_action_scene}. {base_style}. Square aspect ratio."
        else: generated_prompt = f"{subject} {weather_action_scene} {time_context_desc}. {base_style}. Square aspect ratio."
        generated_prompt = generated_prompt.replace("  ", " ").replace("..", ".").strip(); log.info(f"PyScript: Dynamically generated prompt: {generated_prompt}")
    except Exception as e: log.error(f"PyScript: Error prompt generation: {e}"); generated_prompt = f"{subject} in {condition} weather. {base_style}. Square aspect ratio." # Fallback

    # --- Prepare API Call Details (NEW for ir.myqa.cc) ---
    api_endpoint = "https://ir-api.myqa.cc/v1/openai/images/generations" # <-- NEW Endpoint
    target_model = "stabilityai/sdxl-turbo:free" # <-- NEW Use the FREE model identifier
    headers = {
        'Authorization': f'Bearer {api_key}', # <-- NEW Authentication Header
        'Content-Type': 'application/json'
    }
    payload = {
        "prompt": generated_prompt,
        "model": target_model
    }
    log.info(f"PyScript: Using model via ir.myqa.cc router: {target_model}")

    # --- Call ir.myqa.cc API & Parse Response (with Retry Logic) ---
    response_data = None; max_attempts = 2
    for attempt in range(max_attempts):
        try:
            log.info(f"PyScript: Sending request to ir.myqa.cc API (Attempt {attempt + 1}/{max_attempts})...")
            # Use task.executor for non-blocking request
            response = task.executor(requests.post, api_endpoint, headers=headers, json=payload, timeout=240) # Keep long timeout
            response.raise_for_status(); # Check for HTTP errors (4xx/5xx) - will raise an exception
            full_response_text = response.text; log.debug(f"PyScript DEBUG: Raw API response text (Attempt {attempt + 1}): {full_response_text}")
            try: # Parse response JSON
                response_data = json.loads(full_response_text) # Expecting a dictionary based on test
                log.info(f"PyScript: Parsed JSON response (Attempt {attempt + 1}).")
                break # SUCCESS -> Exit Loop
            except json.JSONDecodeError as e:
                log.error(f"PyScript: Failed parse JSON response (Attempt {attempt + 1}): {e}. Response text: {full_response_text}")
                return # Exit if parsing fails
            except Exception as e:
                log.error(f"PyScript: Unexpected error during JSON parsing (Attempt {attempt + 1}): {e}")
                return # Exit on other parsing errors
        except requests.exceptions.Timeout as e: # Catch Timeout for retry
            log.warning(f"PyScript: API call timeout (Attempt {attempt + 1}): {e}")
            if attempt + 1 == max_attempts: log.error("PyScript: Timeout on final API attempt."); return
            log.info("PyScript: Waiting 5 seconds before retry..."); task.sleep(5) # PyScript async sleep
        except requests.exceptions.RequestException as e: # Catch other Network/Request errors
            log.warning(f"PyScript: API Network/Request Error (Attempt {attempt + 1}): {e}")
            # Log response body if available, might contain error details from API (like 401/403)
            if e.response is not None:
                 log.warning(f"PyScript: API Response status: {e.response.status_code}, body: {e.response.text[:500]}") # Log first 500 chars
            if attempt + 1 == max_attempts: log.error("PyScript: Network/Request Error on final API attempt."); return
            log.info("PyScript: Waiting 5 seconds before retry..."); task.sleep(5)
        except Exception as e:
            log.error(f"PyScript: Unexpected error during API call (Attempt {attempt + 1}): {e}", exc_info=True)
            return # No retry for other unexpected errors

    if response_data is None:
        log.error(f"PyScript: API call failed definitively after {max_attempts} attempts.")
        return

    # --- Extract, Decode Image Data (NEW LOGIC based on ir.myqa.cc response) ---
    if not isinstance(response_data, dict):
        log.error(f"PyScript: Invalid response_data type received: {type(response_data)}")
        return
    base64_image_data = None; image_bytes = None
    try:
        # Extract Base64 data based on the confirmed response structure
        if 'data' in response_data and isinstance(response_data['data'], list) and len(response_data['data']) > 0:
            first_item = response_data['data'][0]
            if isinstance(first_item, dict) and 'b64_json' in first_item and first_item['b64_json']:
                base64_image_data = first_item['b64_json']
                log.info("PyScript: Base64 data extracted successfully from 'b64_json' field.")
            else:
                 log.warning("PyScript: 'b64_json' field not found or empty in the first item of the 'data' array.")
        else:
            log.warning("PyScript: 'data' array not found or empty in the API response.")

        # Decode if base64 data was found
        if base64_image_data:
            try:
                image_bytes = base64.b64decode(base64_image_data)
                log.info("PyScript: Base64 data decoded successfully.")
            except base64.binascii.Error as e:
                log.error(f"PyScript: Error decoding Base64 data: {e}")
                return
            except Exception as e:
                 log.error(f"PyScript: Unexpected error during Base64 decoding: {e}")
                 return
        else:
            # Log potential error messages from the API if image data wasn't found
            if 'error' in response_data and isinstance(response_data['error'], dict) and 'message' in response_data['error']:
                 log.error(f"PyScript: API returned an error: {response_data['error']['message']}")
            else:
                 log.warning(f"PyScript: No image data found and no specific error message in response. Response (partial): {str(response_data)[:1000]}")
            return # Stop if no image data could be extracted

    except Exception as e:
        log.error(f"PyScript: Error during response data extraction: {e}", exc_info=True)
        return

    # --- Save Image File(s) (Keep your existing saving logic) ---
    if image_bytes:
        main_save_success = False # Flag to track main save status
        # --- 1. Save the main image for Lovelace (using direct io.open) ---
        main_filename = "gemini_pyscript_image.png" # Or your preferred filename
        main_output_path = os.path.join("/config/www", main_filename)
        log.info(f"PyScript: Saving main image to {main_output_path} using io.open...")
        try:
            with io.open(main_output_path, "wb") as f:
                 f.write(image_bytes) # Write the image bytes
            log.info(f"PyScript: Main image saved successfully.")
            main_save_success = True # Set flag on success
            # Send notification AFTER main save is confirmed
            service.call('persistent_notification', 'create', title="Weather Image Updated", message=f"Image for '{condition}' generated via ir.myqa.cc.")
        except PermissionError as pe: log.error(f"PyScript: PERMISSION ERROR writing main image {main_output_path}: {pe}", exc_info=True)
        except IOError as e: log.error(f"PyScript: I/O ERROR writing main image: {e}", exc_info=True)
        except Exception as e: log.error(f"PyScript: Unexpected error writing main image: {e}", exc_info=True)

        # --- 2. Optionally save an archive copy (using direct io.open) ---
        if main_save_success and archive_images:
            log.info("PyScript: Archiving enabled, attempting to save archive copy...")
            try:
                # Ensure the archive directory exists
                os.makedirs(archive_dir_full_path, exist_ok=True)
                # Generate unique filename
                timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S')
                safe_condition = condition.lower().replace(' ', '_').replace('/', '_')[:15] # Sanitize further
                safe_phase = daylight_phase.lower().replace(' ', '_')[:10] # Sanitize
                archive_filename = f"weather_{timestamp_str}_{safe_condition}_{safe_phase}.png" # Changed prefix
                archive_file_path = os.path.join(archive_dir_full_path, archive_filename)
                log.info(f"PyScript: Saving archive image to {archive_file_path} using io.open...")
                with io.open(archive_file_path, "wb") as f_archive:
                    f_archive.write(image_bytes)
                log.info(f"PyScript: Archive image saved successfully.")
            except PermissionError as pe_arch: log.error(f"PyScript: PERMISSION ERROR writing archive {archive_file_path}: {pe_arch}", exc_info=True)
            except IOError as e_arch: log.error(f"PyScript: I/O ERROR writing archive image: {e_arch}", exc_info=True)
            except Exception as e_arch: log.error(f"PyScript: Unexpected error during image archiving: {e_arch}", exc_info=True)
        elif main_save_success and not archive_images:
             log.info("PyScript: Archiving is disabled, skipping archive save.")
        # End of archiving block

    else:
        # This case means decoding failed or base64 data wasn't successfully extracted
        log.error("PyScript: Logical error - image_bytes is None, cannot save file.")

3. Update Home Assistant Automation:

  • Go to your Home Assistant configuration where the automation that calls the pyscript.generate_gemini_image service is defined (e.g., in automations.yaml).
  • Find the action: section that calls the service.
  • Update the api_key: data field to use your new ir.myqa.cc API key instead of the old Google API key.

4. Reload PyScript:

  • Save the changes to your generate_gemini.py file and your automation configuration.
  • In Home Assistant, go to Configuration → Server Management (or Settings → Server Management) → YAML Configuration → Pyscript (or find the Pyscript integration) and click Reload.

Now, when the automation triggers, it should call the modified PyScript function, which will then contact the ir.myqa.cc API using your new key and the specified model to generate the image. Remember to check the PyScript logs (/config/pyscript.log or via the HA interface) if you encounter issues.

Hope this helps keep your AI weather cards running! :slight_smile:

PS Gemini isn’t working here either, it says I’ve exceeded the daily request limit… Luckily, there are free models like FLUX-1-schnell:free and sdxl-turbo:free. I tried the last one…

PPS Apparently, my daily request quota reset today (looks like it’s 50), so I can use the Gemini model again - but just for 50 requests today :). I’m just changing the model identifier in the script from 'stabilityai/sdxl-turbo:free' to 'google/gemini-2.0-flash-exp:free'.

Thank you very much for the alternative.

2025-04-22 09:47:59.102 ERROR (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] Exception in <file.generate_gemini.generate_gemini_image> line 84:
                response = task.executor(requests.post, api_endpoint, headers=headers, json=payload, timeout=240) # Keep long timeout
                                         ^
NameError: name 'requests.post' is not defined

maybe you have a missing “import requests” at the beginning of the script?

Sorry my bad, I should have read the description better. I just replaced the whole script by your modified function.