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

Perfect, thanks again. :+1::+1::+1:

Hi everyone,

This is an update for those using the AI Weather Card setup with PyScript, specifically if you’ve adapted it to use the ir.myqa.cc API service (using their /v1/openai/images/generations endpoint) as an alternative for image generation.

Problem: Starting around May 1st, 2025, the image generation via this API endpoint may have stopped working, resulting in errors in your Home Assistant logs. You might see a 400 Bad Request error logged when the pyscript.generate_gemini_image service is called.

Cause: It appears the ir.myqa.cc API had a recent backend change. It now seems to require a quality parameter in the request payload, even though this might not have been strictly necessary before and could be considered optional. When the script sends the request without this parameter, the API returns the 400 Bad Request error (specifically mentioning a toLowerCase issue in the detailed error message, indicating a server-side bug triggered by the missing parameter).

The Fix: You need to make a small modification to your PyScript file (e.g., /config/pyscript/generate_gemini.py or whatever you named it).

  1. Edit the script file.
  2. Find the function @service def generate_gemini_image(...).
  3. Locate the section where the payload dictionary is defined for the API call.
  4. Add the line "quality": "auto" to this dictionary.

It should look similar to this (make sure to add the comma after the model line):

Inside the generate_gemini_image function…

payload = {
    "prompt": generated_prompt,
    "model": target_model,  # e.g., "sdxl-turbo:free" or "FLUX-1-schnell:free"
    "quality": "auto"   # <-- ADD THIS LINE (ensure comma on previous line!)
}

Models:

  • This fix should allow models like sdxl-turbo:free and FLUX-1-schnell:free to work again via the API.
  • Note on Gemini: Based on recent testing (as of May 1st), the google/gemini-2.0-flash-exp:free model accessed through this ir.myqa.cc router still seems to have a separate issue where it returns text instead of an image (resulting in a 406 Not Acceptable error). You may need to stick with the other models for now.

Final Step:

  • Save the modified .py file.
  • Reload your PyScript integration in Home Assistant (Configuration → Server Management → YAML Configuration → Pyscript, or restart HA if preferred).

This should resolve the 400 Bad Request error and get your image generation working again with the compatible models on ir.myqa.cc.

Hope this helps!

1 Like

Hi, I’m the developer of ImageRouter and just wanted to say thankfully to Andrei I was able to fix both bugs in hours.

  • quality is not required anymore, it is optional, and defaults to “auto” when not set.
  • Gemini will return the image when it is generated. I think Google updated the model, which broke my implementation, but it works now. You can now use the google/gemini-2.0-flash-exp:free model for free.

Thanks again for Andrei for the detailed bug report :pray:

1 Like

I still haven’t found any transparent information about prices, can I use a regular free API key to generate the weather image described in this topic? Or do I have to pay? In what amount?

Hi andrewjswan,

Regarding your question about pricing and API keys for generating the weather images described in this topic using the ImageRouter service (ir.myqa.cc):

  1. API Key for ImageRouter: To use the ImageRouter service, you need to sign up on their platform (likely myqa.cc or as directed by DaWe35) and get an API key specifically for their router service. This key is distinct from, say, a direct Google Gemini API key.
  2. Using ImageRouter’s Free Models:
  • As DaWe35 confirmed, ImageRouter has access to various backend models. The good news is that they offer some of these under “free” identifiers, such as stabilityai/sdxl-turbo:free and FLUX-1-schnell:free, which my updated script (and the test script we troubleshooted) now uses.
  • If you use your ImageRouter API key with one of these :free model identifiers as specified in the script’s target_model variable (and include the "quality": "auto" parameter in the payload as recently discussed), then the image generation for the weather card should work without direct charges from the ImageRouter service for those specific calls.
  • This assumes you stay within any potential fair usage limits or quotas that ImageRouter might have for their free tier.
  1. Information on Pricing/Limits:
  • For precise details on what the “free” tier includes, any rate limits, or potential costs if you were to use other (non-free) models they might offer, the best source of information would be the ImageRouter website itself or by asking DaWe35 directly. Since he’s active in this thread, he might be able to clarify his service’s pricing structure or point to documentation.
  • I’m a user of his service in this context, so I can only speak to my experience with the free model identifiers.

So, in summary, for the weather card images described in this topic, by using a “free” model identifier like stabilityai/sdxl-turbo:free via the ImageRouter API key, it’s intended to be free of charge from ImageRouter’s side.

Hope this helps clarify!

Best, Andrei

1 Like

I can confirm that gemini is working now. Many thanks!

google/gemini-2.0-flash-exp:free

Have you thought about adding the ability to fallback to one of the images in the archive when there is an error generating an image and archiving is enabled?

Hello, this seems like a very cool concept. I followed the documentation exactly except for the ImageRouter service. I am in the US, do I need ImageRouter?
My card just has “broken image” icon in it and my logs say:

2025-05-21 11:38:35.372 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling len([{'text': 'I'}], {})
2025-05-21 11:38:35.374 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling warning("PyScript: No 'inlineData' found in response.", {})
2025-05-21 11:38:35.375 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: No 'inlineData' found in response.
2025-05-21 11:38:35.376 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling warning("PyScript: API returned text: I...", {})
2025-05-21 11:38:35.376 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: API returned text: I...

Any suggestions on what else to look for or do would be greatly appreciated.

2025-05-22 17:46:25.060 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: API Network/Request Error (Attempt 1): 429 Client Error: Too Many Requests for url: https://ir-api.myqa.cc/v1/openai/images/generations
2025-05-22 17:46:25.060 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: API Response status: 429, body: {"error":{"message":"Daily limit of 50 free requests reached. There is no limit on paid models.","type":"rate_limit_error"}}

This does not correspond to ImageRouters webpage:

General API Rate Limits

100 requests per second

Image Generation Rate Limits

30 requests per minute

@DaWe35 Any update about this?

Make sure to use generate_gemini.py - v27.1 with the instructions posted by Andrei on April,11.

I restored a back up of my HA which means that I got to follow these steps all over again.
I followed the original steps and then made all of the changes/updates suggested in the 04/11 post.
I still just get a broken image and when I manually run the automation, I get this error 2025-05-22 14:48:00.242 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: No 'inlineData' found in response. 2025-05-22 14:48:00.243 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: API returned text: I...

Here is my .py

# 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.")

Here is my automation

alias: Generate Gemini Weather Image (Hourly + Change)
description: Updates the Gemini AI weather image via PyScript
triggers:
  - entity_id: weather.forecast_home
    trigger: state
  - entity_id: sensor.simple_daylight_phase
    trigger: state
actions:
  - data:
      condition: "{{ states('weather.home') }}"
      temperature: "{{ state_attr('weather.home', 'temperature') | float(0) }}"
      daylight_phase: "{{ states('sensor.simple_daylight_phase') }}"
      api_key: **using my API key here**
    action: pyscript.generate_gemini_image
  - target:
      entity_id: input_text.gemini_image_timestamp
    data:
      value: "{{ now().timestamp() | int }}"
    action: input_text.set_value
mode: queued
max_exceeded: silent

I added this YAML to my 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 %}

I set up the input_text.gemini_image_timestamp helper

Here is my cards code:

type: custom:button-card
entity: weather.home
show_name: false
show_icon: false
show_state: false
show_label: false
hold_action:
  action: call-service
  service: automation.trigger
  target:
    entity_id: automation.generate_gemini_weather_image_hourly_change
tap_action:
  action: more-info
styles:
  card:
    - padding: 0px
    - border-radius: var(--ha-card-border-radius, 12px)
    - overflow: hidden
custom_fields:
  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">`;
    ]]]

I can’t imagine what I’m missing, but really hope someone can see whats going wrong for me. If you need any other configs to looks over please let me know.

Hi,
The error PyScript: No 'inlineData' found in response. followed by PyScript: API returned text: I... indicates that the Gemini API responded with a text message instead of the expected image data. Here are the most likely reasons:

  • Safety Filters: The generated prompt might be triggering Google’s safety filters, causing the API to refuse image generation and return a textual explanation or a generic message. The full text message logged by your script (after “API returned text:”) should provide clues if this is the case (e.g., a blockReason).
  • API Key or Google Cloud Project Issues:
    • The API key might lack necessary permissions for the Generative Language API or the specific image model.
    • Billing might not be enabled for the Google Cloud Project associated with the API key.
    • API usage quotas (daily, per minute, etc.) might have been exceeded.
  • API Model Behavior/Availability: The model gemini-2.0-flash-exp-image-generation seems experimental. Its availability, behavior, or expected request/response structure might have changed, or it might require specific access. The API might be defaulting to a text response if it can’t process the image request with this model.
  • Prompt Content: Even if not triggering safety filters explicitly, the prompt might be interpreted by the model in a way that leads to a textual answer rather than an image.
  • responseModalities Setting: Your script requests both "IMAGE" and "TEXT" in responseModalities. If the API encounters any issue generating the image, it might fall back to providing a response in the TEXT modality, which is what seems to be happening.

The key to diagnosing this is to find the full text message that your script logs after PyScript: API returned text: I.... This message from the API will likely explain why it didn’t return an image. Also, checking the exact generated_prompt that was sent for the failing request can be helpful.

Thank you so much for your help and suggestions.
I use the logview add on to view my logs, I can’t find anything deeper about pyscript than the condensed error message I supplied. Where else might I check for a more complete message? Is this something I can find in home assistant or do I need to find it in the Google Dev API somewhere?

actually I found a ton more stuff when I put into debug mode.
It seems like it takes in the info and creates the image. I decoded the long base64 string into an image decoder and it looks like what the text describes. Still see the mysterious error at the end and no image is produced in the card

2025-05-28 19:29:21.219 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling info("PyScript: Received request (v27.1 - archive) condition='sunny', temp='80.0', phase='Sunset'", {})
2025-05-28 19:29:21.220 INFO (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Received request (v27.1 - archive) condition='sunny', temp='80.0', phase='Sunset'
2025-05-28 19:29:21.220 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling isinstance("MY API KEY", <class 'str'>, {})
2025-05-28 19:29:21.220 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling len("MY API KEY", {})
2025-05-28 19:29:21.220 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling lower(, {})
2025-05-28 19:29:21.221 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling lower(, {})
2025-05-28 19:29:21.221 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling replace("  ", " ", {})
2025-05-28 19:29:21.221 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling replace("..", ".", {})
2025-05-28 19:29:21.221 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling strip(, {})
2025-05-28 19:29:21.222 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling info("PyScript: Dynamically generated prompt: a very cute, happy and playful kitten playing happily in the sunbeams in a magical landscape during a beautiful sunset with warm golden light. whimsical illustration style, sharp focus, high detail, crisp image. Square aspect ratio.", {})
2025-05-28 19:29:21.222 INFO (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Dynamically generated prompt: a very cute, happy and playful kitten playing happily in the sunbeams in a magical landscape during a beautiful sunset with warm golden light. whimsical illustration style, sharp focus, high detail, crisp image. Square aspect ratio.
2025-05-28 19:29:21.222 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling range(2, {})
2025-05-28 19:29:21.223 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling info("PyScript: Sending request to API (Attempt 1/2)...", {})
2025-05-28 19:29:21.223 INFO (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Sending request to API (Attempt 1/2)...
2025-05-28 19:29:21.223 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling executor(<function post at 0x7f442df3f600>, "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:streamGenerateContent?key=MY API KEY", {'headers': {'Content-Type': 'application/json'}, 'json': {'contents': [{'role': 'user', 'parts': [{'text': 'a very cute, happy and playful kitten playing happily in the sunbeams in a magical landscape during a beautiful sunset with warm golden light. whimsical illustration style, sharp focus, high detail, crisp image. Square aspect ratio.'}]}], 'generationConfig': {'responseModalities': ['IMAGE', 'TEXT']}}, 'timeout': 240})
2025-05-28 19:29:24.593 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling raise_for_status(, {})
2025-05-28 19:29:24.607 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling debug("PyScript DEBUG: Raw API response text (Attempt 1): [{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "I"
          }
        ],
        "role": "model"
      },
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 45,
    "candidatesTokenCount": 1,
    "totalTokenCount": 46,
    "promptTokensDetails": [
      {
        "modality": "TEXT",
        "tokenCount": 45
      }
    ]
  },
  "modelVersion": "gemini-2.0-flash-exp-image-generation",
  "responseId": "gcY3aNKPIa7Vz7IPqIiAqAI"
}
,
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": " will generate a whimsical illustration of an exceptionally adorable kitten, radiating happiness and playfulness"
          }
        ],
        "role": "model"
      },
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 45,
    "candidatesTokenCount": 17,
    "totalTokenCount": 62,
    "promptTokensDetails": [
      {
        "modality": "TEXT",
        "tokenCount": 45
      }
    ]
  },
  "modelVersion": "gemini-2.0-flash-exp-image-generation",
  "responseId": "gcY3aNKPIa7Vz7IPqIiAqAI"
}
,
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": " as it frolics within sunbeams in a fantastical landscape. The scene will be bathed in the warm, golden hues of a breathtaking sunset, captured in sharp focus and high detail, resulting in a crisp image with a square aspect ratio."
          }
        ],
        "role": "model"
      },
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 45,
    "candidatesTokenCount": 65,
    "totalTokenCount": 110,
    "promptTokensDetails": [
      {
        "modality": "TEXT",
        "tokenCount": 45
      }
    ]
  },
  "modelVersion": "gemini-2.0-flash-exp-image-generation",
  "responseId": "gcY3aNKPIa7Vz7IPqIiAqAI"
}
,
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "\n"
          }
        ],
        "role": "model"
      },
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 45,
    "candidatesTokenCount": 65,
    "totalTokenCount": 110,
    "promptTokensDetails": [
      {
        "modality": "TEXT",
        "tokenCount": 45
      }
    ]
  },
  "modelVersion": "gemini-2.0-flash-exp-image-generation",
  "responseId": "gcY3aNKPIa7Vz7IPqIiAqAI"
}
,
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "inlineData": {
              "mimeType": "image/png",
              "data": 
BIG LONG CRAZY BASE 64 ENCODING
'role': 'model'}, 'index': 0}], 'usageMetadata': {'promptTokenCount': 45, 'candidatesTokenCount': 1355, 'totalTokenCount': 1400, 'promptTokensDetails': [{'modality': 'TEXT', 'tokenCount': 45}], 'candidatesTokensDetails': [{'modality': 'IMAGE', 'tokenCount': 1290}]}, 'modelVersion': 'gemini-2.0-flash-exp-image-generation', 'responseId': 'gcY3aNKPIa7Vz7IPqIiAqAI'}, {'candidates': [{'content': {'role': 'model'}, 'finishReason': 'STOP', 'index': 0}], 'usageMetadata': {'promptTokenCount': 45, 'candidatesTokenCount': 1355, 'totalTokenCount': 1400, 'promptTokensDetails': [{'modality': 'TEXT', 'tokenCount': 45}], 'candidatesTokensDetails': [{'modality': 'IMAGE', 'tokenCount': 1290}]}, 'modelVersion': 'gemini-2.0-flash-exp-image-generation', 'responseId': 'gcY3aNKPIa7Vz7IPqIiAqAI'}], {})
2025-05-28 19:29:24.733 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling info("PyScript: Parsed JSON (Attempt 1).", {})
2025-05-28 19:29:24.733 INFO (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Parsed JSON (Attempt 1).
2025-05-28 19:29:24.734 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling isinstance({'candidates': [{'content': {'parts': [{'text': 'I'}], 'role': 'model'}, 'index': 0}], 'usageMetadata': {'promptTokenCount': 45, 'candidatesTokenCount': 1, 'totalTokenCount': 46, 'promptTokensDetails': [{'modality': 'TEXT', 'tokenCount': 45}]}, 'modelVersion': 'gemini-2.0-flash-exp-image-generation', 'responseId': 'gcY3aNKPIa7Vz7IPqIiAqAI'}, <class 'dict'>, {})
2025-05-28 19:29:24.737 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling len([{'content': {'parts': [{'text': 'I'}], 'role': 'model'}, 'index': 0}], {})
2025-05-28 19:29:24.737 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling len([{'text': 'I'}], {})
2025-05-28 19:29:24.737 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling warning("PyScript: No 'inlineData' found in response.", {})
2025-05-28 19:29:24.738 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: No 'inlineData' found in response.
2025-05-28 19:29:24.738 DEBUG (MainThread) [custom_components.pyscript.eval] file.generate_gemini.generate_gemini_image: calling warning("PyScript: API returned text: I...", {})
2025-05-28 19:29:24.738 WARNING (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: API returned text: I...
2025-05-28 20:09:22.666 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved (None)
Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/aiohttp/resolver.py", line 106, in resolve
    resp = await self._resolver.getaddrinfo(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<5 lines>...
    )
    ^
aiodns.error.DNSError: (1, 'DNS server returned answer with no data')
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1357, in _create_direct_connection
    hosts = await self._resolve_host(host, port, traces=traces)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 995, in _resolve_host
    return await asyncio.shield(resolved_host_task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1026, in _resolve_host_with_throttle
    addrs = await self._resolver.resolve(host, port, family=self._family)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp_asyncmdnsresolver/_impl.py", line 140, in resolve
    return await super().resolve(host, port, family)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/resolver.py", line 115, in resolve
    raise OSError(None, msg) from exc
OSError: [Errno None] DNS server returned answer with no data
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/alexa/state_report.py", line 471, in async_send_add_or_update_message
    return await session.post(
           ^^^^^^^^^^^^^^^^^^^
        config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/client.py", line 703, in _request
    conn = await self._connector.connect(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        req, traces=traces, timeout=real_timeout
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 548, in connect
    proto = await self._create_connection(req, traces, timeout)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1056, in _create_connection
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
    _, proto = await self._create_direct_connection(req, traces, timeout)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
2025-05-28 20:19:23.039 WARNING (MainThread) [alexapy.helpers] alexaapi.get_customer_history_records((<alexapy.alexalogin.AlexaLogin object at 0x7f4400555fd0>,), {'max_record_size': 10}): Timeout error occurred accessing AlexaAPI: An exception of type CancelledError occurred. Arguments:
()
  File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 1363, in _create_direct_connection
    raise ClientConnectorDNSError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorDNSError: Cannot connect to host api.amazonalexa.com:443 ssl:default [DNS server returned answer with no data]
()

Hi Zach,

Thanks for providing the log file regarding the issue with the Gemini image generation script. I’ve had a chance to look at it, and based on the information, it seems like the behavior of Google’s Gemini API (specifically the streamGenerateContent endpoint) may have changed.

The logs indicate that the API is now streaming data in multiple chunks, and often, text parts are sent before the actual image data (inlineData). The older version of the script (like v27.1 which you were likely using when that log was generated) was designed to look for the image data primarily in the first part of the response. If the image data is no longer in that first chunk due to these API changes, the script will fail to find it, which explains the “No ‘inlineData’ found” error you encountered.

Unfortunately, I have a significant constraint when it comes to addressing this directly for the script version that calls the Gemini API directly: Due to Google’s regional restrictions, I no longer have direct access to the Gemini API from my location. This means I’m unable to test any modifications or new versions of the script that rely on direct Gemini API calls. It’s frustrating, but it’s the situation I’m faced with.

Therefore, I strongly recommend using the latest version of the script (e.g., v28.0 or newer) which utilizes an image router service (like the one at ir.myqa.cc). This version has a few advantages:

  1. I can actively test and support it.
  2. It provides access to multiple free image generation models, not just Gemini. You’ll find options like Stability AI models available through it.

Interestingly, even with the router, I’ve personally found that Gemini’s image generation has become quite unpredictable and sometimes produces strange results for my use cases. As a result, I’ve actually switched to using "stabilityai/sdxl-turbo:free" myself via the router, as I’ve found it more consistent recently.

So, my best advice would be to try the latest script version that uses the image router. It should offer a more reliable experience and more flexibility with model choices.

Hope this clarifies the situation and helps you find a working solution!

Best regards,

Thank you very much for all of this information. I will attempt this hopefully when I have some time this weekend.

AI Weather Card PyScript - Major Update & Fix (June 2025)

Hi everyone,

This is a major update to the AI Weather Card guide for those using the PyScript method. The previous methods, which relied on the direct Google Gemini experimental API or the old ir.myqa.cc endpoint, have recently stopped working due to significant changes in these backend services.

After extensive troubleshooting, here is a new, fully working version that uses the updated and renamed imagerouter.io service.

Overview of the New Solution

The new method uses the imagerouter.io API, which acts as a router to various image generation models. The key changes are:

  1. The API has moved to a new address: https://api.imagerouter.io.
  2. The API no longer returns image data directly. Instead, it provides a URL, and our script must then download the image from that URL.

This guide provides the updated script and steps to get your weather card working again.


Step 1: Get an API Key from Image Router

If you haven’t already, you will need an API key from the new service.


Step 2: Update the PyScript File

Replace the entire content of your old script (e.g., /config/pyscript/generate_gemini.py) with the code below. This new version includes all the necessary changes to work with the new API.

# Filename: /config/pyscript/generate_gemini.py
# Version: v30.2 (Final - Corrected Filename)

# Required Imports
import requests
import base64
import os
import json
import io
from datetime import datetime

# --- Configuration ---
archive_images = True
archive_dir_relative = "www/image_router_archive"
archive_dir_full_path = os.path.join("/config", archive_dir_relative)


# --- Main PyScript Service Function ---
@service
def generate_gemini_image(condition=None, temperature=None, daylight_phase=None, api_key=None):
    """
    Generates an image using the imagerouter.io API based on weather and daylight phase.
    
    IMPORTANT: This version is updated for the imagerouter.io changes as of June 2025,
    which require using a new endpoint and downloading the image from a returned URL.
    
    Args:
        condition (str): Standard HA weather condition.
        temperature (float/int): Current temperature.
        daylight_phase (str): State from sensor.simple_daylight_phase.
        api_key (str): imagerouter.io API Key (passed from the automation).
    """
    global archive_images, archive_dir_full_path

    log.info(f"PyScript: Received request (v30.2 - imagerouter.io) 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 imagerouter.io) missing!"); return
    if not isinstance(api_key, str) or len(api_key) < 10:
        log.error(f"PyScript: Invalid API Key (for imagerouter.io) provided.")
        return

    # --- Generate Dynamic Prompt ---
    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()
        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})"
        
        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"
        
        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 during prompt generation: {e}")
        generated_prompt = f"{subject} in {condition} weather. {base_style}. Square aspect ratio."

    # --- Prepare API Call Details ---
    # UPDATED: The API endpoint has moved to 'api.imagerouter.io'.
    api_endpoint = "https://api.imagerouter.io/v1/openai/images/generations"
    target_model = "stabilityai/sdxl-turbo:free" # Using the working free model identifier.
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }
    payload = {
        "prompt": generated_prompt,
        "model": target_model
    }

    # --- STEP 1: Call API to get the image URL ---
    response_data = None; max_attempts = 2
    for attempt in range(max_attempts):
        try:
            log.info(f"PyScript: Sending request to get image URL (Attempt {attempt + 1}/{max_attempts})...")
            response = task.executor(requests.post, api_endpoint, headers=headers, json=payload, timeout=240)
            response.raise_for_status()
            response_data = response.json()
            log.info(f"PyScript: Successfully received API response (Attempt {attempt + 1}).")
            break
        except Exception as e:
            log.error(f"PyScript: Error during API call to get URL (Attempt {attempt + 1}): {e}", exc_info=True)
            if attempt + 1 == max_attempts:
                log.error("PyScript: API call failed on final attempt.")
                return
            task.sleep(5)
    if response_data is None:
        log.error(f"PyScript: API call failed definitively after {max_attempts} attempts.")
        return

    # --- STEP 2: Extract URL and Download Image Data ---
    # NEW LOGIC: The API now returns a URL, so we must make a second request to download the image.
    image_url = None
    image_bytes = None
    try:
        image_url = response_data['data'][0]['url']
        log.info(f"PyScript: Image URL extracted: {image_url}")
        if image_url:
            log.info(f"PyScript: Downloading image from URL...")
            download_response = task.executor(requests.get, image_url, timeout=60)
            download_response.raise_for_status()
            image_bytes = download_response.content
            log.info("PyScript: Image downloaded successfully.")
    except Exception as e:
        log.error(f"PyScript: Error during URL extraction or image download: {e}", exc_info=True)
        return

    # --- STEP 3: Save Image File(s) ---
    if image_bytes:
        main_save_success = False
        # IMPORTANT: Keeping original filename to avoid breaking existing Lovelace card configurations.
        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}...")
        
        try:
            with io.open(main_output_path, "wb") as f:
                f.write(image_bytes)
            log.info(f"PyScript: Main image saved successfully.")
            main_save_success = True
            service.call('persistent_notification', 'create', title="Weather Image Updated", message=f"Image for '{condition}' generated via imagerouter.io.")
        except Exception as e:
            log.error(f"PyScript: Unexpected error writing main image: {e}", exc_info=True)

        if main_save_success and archive_images:
            log.info("PyScript: Archiving enabled, attempting to save archive copy...")
            try:
                os.makedirs(archive_dir_full_path, exist_ok=True)
                timestamp_str = datetime.now().strftime('%Y%m%d_%H%M%S')
                safe_condition = condition.lower().replace(' ', '_').replace('/', '_')[:15]
                safe_phase = daylight_phase.lower().replace(' ', '_')[:10]
                archive_filename = f"weather_{timestamp_str}_{safe_condition}_{safe_phase}.png"
                archive_file_path = os.path.join(archive_dir_full_path, archive_filename)
                
                with io.open(archive_file_path, "wb") as f_archive:
                    f_archive.write(image_bytes)
                log.info(f"PyScript: Archive image saved successfully to {archive_file_path}")
                
            except Exception as e_arch:
                log.error(f"PyScript: Unexpected error during image archiving: {e_arch}", exc_info=True)
    else:
        log.error("PyScript: Logical error - image_bytes is None, cannot save file.")

Step 3: Update Your Automation

You must update your automation to pass your new imagerouter.io API key. The service name in the action should match your pyscript filename (e.g., pyscript.generate_gemini_image).

Example automation.yaml:

- alias: "Generate Gemini Weather Image (On Change)"
  trigger:
    # Your triggers
  action:
    - service: pyscript.generate_gemini_image # This must match your .py filename
      data:
        condition: "{{ states('weather.forecast_home') }}"
        temperature: "{{ state_attr('weather.forecast_home', 'temperature') | float(0) }}"
        daylight_phase: "{{ states('sensor.simple_daylight_phase') }}"
        # --- UPDATE THIS LINE with your new key ---
        api_key: "YOUR_NEW_IMAGEROUTER_IO_API_KEY_HERE"

Step 4: Reload PyScript

After saving your changes:

  1. Go to Developer Tools → YAML.
  2. Scroll down to PYSCRIPT: RELOAD and click it.

Your weather card should now be generating images again without needing any changes to your Lovelace configuration. Hope this helps everyone!

P.S. A Note on Naming (Moving from “Gemini” to “Weather”)

You’ll notice many components in this guide are still named “Gemini” (e.g., the script file generate_gemini.py, the output file gemini_pyscript_image.png). This is because I originally started the project using the Google Gemini API.

As you can see from the recent updates, access to a reliable image generation model from Gemini has become problematic for me. The current stable solution now uses other models like stabilityai/sdxl-turbo. Because of this, I am gradually moving away from the “Gemini” branding as it no longer accurately reflects the backend model being used.

  • For existing users: You can continue to use the existing names without any issue. The script I provided above intentionally keeps the original output filename (gemini_pyscript_image.png) to ensure your existing Lovelace cards do not break.
  • For new users setting this up for the first time: I recommend using more generic names for clarity. For example:
    • PyScript filename: generate_weather_image.py
    • Service call: pyscript.generate_weather_image
    • Output filename in the script: weather_pyscript_image.png
    • (Just remember that whatever filename you choose in the script, you must use the same corresponding path in your Lovelace card configuration.)
1 Like

I am getting

2025-06-10 18:55:55.015 ERROR (MainThread) [custom_components.pyscript.file.generate_gemini.generate_gemini_image] PyScript: Error during API call to get URL (Attempt 2): 429 Client Error: Too Many Requests for url: https://api.imagerouter.io/v1/openai/images/generations

Obviously imagerouter started to limit access for free users.

Hi,

Thanks for reporting this. The 429 Too Many Requests error is a classic rate-limiting error from the imagerouter.io service. This means their server could have a policy to limit the number of requests a single user (identified by their API key) can make in a certain period (e.g., per minute or per day).

This usually happens during the initial setup and testing phase when the automation might trigger many times in a row. Since rate limits are typically applied per API key, this would explain why it’s affecting your new setup while mine might still be working.

Here are a few troubleshooting steps you could try:

  1. Wait for the Limit to Reset: The most common reason for this is making too many requests in a short time. The first thing to try is simply waiting for a while (it could be anything from a minute to an hour, or even until the next day, depending on their policy) for the rate limit to reset.
  2. Check the Image Router Website: Another great step is to log into your imagerouter.io account. Please check two things:
  • Can you generate an image successfully using their web interface? This confirms your account and key are still active.
  • Do they have a dashboard or account page that shows your current usage or any error messages? This might tell you what the limit is and when it resets.
  1. Verify the Model in Your Script: Please double-check that the target_model variable in your PyScript is set to a working free model, like stabilityai/sdxl-turbo:free. If you are trying to use the Gemini model, please be aware that it had separate issues and was not reliably generating images through this service.
  2. Check Your Automation Triggers: Take a look at the triggers for your Home Assistant automation. If it’s set to run too frequently (for example, on every minor update of a sensor that changes every minute), you could be hitting the daily limit very quickly. You might want to add a condition or a delay to your automation to ensure it doesn’t run more than, say, once every 10-15 minutes.

The most likely scenario is that you’ve hit a temporary limit while testing. Waiting for it to reset is usually the first and best step.

Let us know if waiting helps, or if you find any more info on your imagerouter.io account page. Hope this helps!

Yeah, its 50 calls per day.
I deactivated the weather state change trigger which seem to call too frequent and changed it to a time based trigger every hour together with the daylight phase.