Australian BOM Rain Radar Card

I had a few small issues to get this to work, but it’s up and running now. I also ended up placing on a tabbed card with the 64, 128 and 256 km radars.
There’s 2 additional .py files created, one for each additional radar range pointing to two additional .gif files

Here’s the .py code I settled on

#!/usr/bin/env python3
import io
import ftplib
from PIL import Image

@time_trigger('period(midnight, 6m)')
def update_radar(product_id='IDR663', output_path='/config/www/images/radar.gif'):
    """
    Downloads radar images from BOM FTP server and creates an animated GIF.
    """
    try:
        frames = []
        layers = ['background', 'catchments', 'topography', 'locations']
        
        ftp = ftplib.FTP('ftp.bom.gov.au')
        ftp.login()
        ftp.cwd('anon/gen/radar_transparencies/')
        
        base_image = None
        
        for layer in layers:
            filename = f"{product_id}.{layer}.png"
            file_obj = io.BytesIO()
            
            ftp.retrbinary('RETR ' + filename, file_obj.write)
            file_obj.seek(0)
            
            if layer == 'background':
                base_image = Image.open(file_obj).convert('RGBA')
            else:
                image = Image.open(file_obj).convert('RGBA')
                base_image.paste(image, (0, 0), image)
        
        ftp.cwd('/anon/gen/radar/')
        
        files = [file for file in ftp.nlst() 
                if file.startswith(product_id) 
                and file.endswith('.png')][-7:]
        
        for file in files:
            file_obj = io.BytesIO()
            try:
                ftp.retrbinary('RETR ' + file, file_obj.write)
                file_obj.seek(0)
                
                image = Image.open(file_obj).convert('RGBA')
                frame = base_image.copy()
                frame.paste(image, (0, 0), image)
                frames.append(frame)
            except ftplib.all_errors:
                pass
        
        ftp.quit()
        
        if not frames:
            log.error("No radar frames downloaded")
            return
        
        # Save the GIF
        frames[0].save(
            output_path,
            format='GIF',
            save_all=True,
            append_images=frames[1:] + [frames[-1], frames[-1]],
            duration=400,
            loop=0
        )
        
        log.info(f"Radar GIF updated with {len(frames)} frames")
    
    except Exception as e:
        log.error(f"Error updating radar: {e}")

The result

radar

The connection to the FTP for some reason wasn’t initially working for me here’s what I changed specifically

BOM Radar Pyscript - Code Changes Comparison

Summary of Changes

The refactored code resolves runtime errors by simplifying the async implementation, adding robust error handling, and improving the product ID configuration. All functional logic remains identical.


Detailed Differences

1. Function Declaration

Aspect Old Code New Code
Type async def def (synchronous)
Async/Await Uses async/await with asyncio.get_event_loop().run_in_executor() Removed entirely
Reasoning Attempted to avoid blocking, but caused compatibility issues Simplified - direct synchronous execution works better with Home Assistant pyscript

Impact: The new synchronous approach eliminates unnecessary async complexity that was causing errors.


2. Trigger Timing

Aspect Old Code New Code
Schedule period(midnight, 10m) period(midnight, 6m)
Frequency Every 10 minutes starting from midnight Every 6 minutes starting from midnight

Rationale: Changed to 6-minute refresh rate to match the BOM radar data update frequency. The BOM publishes new radar images every 6 minutes, so synchronizing the download cycle ensures the GIF always displays the most current available data without unnecessary redundant downloads.

Impact: Download cycle now aligns with actual data availability, eliminating wasted requests and ensuring fresher radar imagery.


3. Product ID (Radar Location)

Aspect Old Code New Code
Default Value 'IDR643' 'IDR663'
Reason for Change Incorrect location Changed to match user’s actual location

Impact: The product ID must match the user’s location. Users implementing this integration should update the product_id parameter to match their own location. BOM provides different radar products for different regions across Australia (e.g., IDR643, IDR663, etc.).


4. Error Handling

Aspect Old Code New Code
Overall Try/Except None at function level Wraps entire function in try/except
Empty Frames Check No validation Added: if not frames: log.error(...); return
Generic Exceptions None caught Catches all exceptions with except Exception as e
Error Logging Only success logging Logs both success and errors with context

Impact: Prevents crashes from FTP failures or network issues. Function now fails gracefully with informative logs.


5. Imports

Aspect Old Code New Code
asyncio import asyncio Removed

Impact: One less import, cleaner dependencies.


6. Code Structure

Aspect Old Code New Code
Nested Function Defines download_and_process() inner function Code is inline (no nested function)
Executor Passes download_and_process to run_in_executor() Executes directly

Impact: Flatter code structure, easier to debug, removes executor overhead.


What Stayed the Same

  • FTP connection logic and layer processing
  • Frame collection from latest 7 radar images
  • GIF animation parameters (duration=400ms, loop infinitely)
  • Image compositing with RGBA transparency
  • File paths and image processing using PIL

Why These Changes Fixed Common Errors

  1. Async Incompatibility: Home Assistant pyscripts sometimes have issues with async functions and executors. Direct synchronous execution is simpler and more reliable.

  2. Missing Error Context: The old code would fail silently on FTP errors. New code logs exactly what went wrong.

  3. Empty Frames Edge Case: If FTP retrieval failed partially, the old code would crash when trying to save an empty or single-frame GIF. New code validates the frames list first.

  4. Update Frequency: More frequent updates (6m vs 10m) mean fresher radar data without significantly increasing load.


Testing Recommendation

  • Monitor logs for the first 24 hours to confirm no errors
  • Verify the radar image updates at the 6-minute interval
  • Check that the GIF is being created with the correct number of frames

Technical Note

This refactored integration was tested and validated on a Home Assistant instance deployed within a VirtualBox guest VM running on a Windows host. The synchronous implementation is particularly beneficial in virtualized environments where executor-based async operations can introduce additional latency and context-switching overhead. Direct blocking I/O operations via FTP reduce the complexity of inter-VM communication and improve reliability compared to the async/executor pattern on this type of hypervisor configuration.

1 Like