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

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
-
Async Incompatibility: Home Assistant pyscripts sometimes have issues with async functions and executors. Direct synchronous execution is simpler and more reliable.
-
Missing Error Context: The old code would fail silently on FTP errors. New code logs exactly what went wrong.
-
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.
-
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.

