v0.1.1: Fix icon alignment, improve error display, and enhance UI editor

This commit is contained in:
2025-12-17 01:12:55 +10:00
parent b66b74428d
commit 4ad308fa04
9 changed files with 2611 additions and 268 deletions
+305 -24
View File
@@ -16,12 +16,19 @@ The Australian Bureau of Meteorology's radar API endpoint stopped working in Dec
## Features ## Features
- 🌧️ **Live Radar Display**: View the latest BOM rain radar images for any Australian location - 🌧️ **Live Radar Display**: View the latest BOM rain radar images for any Australian location
- 🎬 **Animated Slideshow**: Play through radar frames to see precipitation movement - 🎬 **Animated Slideshow**: Play through radar frames to see precipitation movement with smooth animations
- 📊 **Historical Data**: View radar history from 1 hour to 24 hours ago - 📊 **Historical Data**: View radar history from 1 hour to 24 hours ago, or custom time ranges
- 🎯 **Location-Based**: Support for any Australian suburb/state combination - 🎯 **Location-Based**: Support for any Australian suburb/state combination with dropdown selection
- 🔄 **Auto-Refresh**: Automatically updates radar data at configurable intervals - 🔄 **Auto-Refresh**: Automatically updates radar data at configurable intervals
- 🎨 **Beautiful UI**: Modern, responsive design that integrates seamlessly with Home Assistant themes - 🎨 **Modern UI**: Sleek, responsive design that integrates seamlessly with Home Assistant themes
- ⚙️ **Visual Editor**: Full GUI configuration editor (no YAML editing required) - ⚙️ **Visual Editor**: Full GUI configuration editor with expandable sections (no YAML editing required)
- 🎛️ **Flexible Display**: Granular control over metadata and controls visibility
- 🖼️ **Image Customization**: Zoom (0.5x to 3.0x) and fit options (contain/cover/fill)
- 📍 **Overlay Options**: Overlay controls and metadata on images to save space
- ⌨️ **Keyboard Navigation**: Full keyboard support (arrow keys, spacebar, home/end)
- ♿ **Accessible**: ARIA labels and screen reader support
- 🔍 **Enhanced Error Handling**: Detailed error messages with retry suggestions and auto-retry
- 📱 **Responsive**: Optimized for mobile, tablet, and desktop
## Prerequisites ## Prerequisites
@@ -138,14 +145,37 @@ For more detailed service setup and configuration options, see the [BOM Local Se
**Service Configuration**: **Service Configuration**:
- **Service URL**: Base URL of your BOM Local Service (default: `http://localhost:8082`) - **Service URL**: Base URL of your BOM Local Service (default: `http://localhost:8082`)
- **Suburb**: The suburb name (e.g., `Pomona`, `Brisbane`) - **Required** - **Suburb**: The suburb name (e.g., `Pomona`, `Brisbane`) - **Required**
- **State**: State abbreviation (e.g., `QLD`, `NSW`, `VIC`) - **Required** - **State**: State dropdown - Select from all Australian states - **Required**
**Display**: **Display**:
- **Card Title**: Optional custom title for the card - **Show Card Title**: Toggle to show/hide card title (uses HA card header)
- **Show Metadata**: Toggle to show/hide cache status, observation time, and weather station info (default: `true`) - **Card Title**: Custom title for the card (only shown if "Show Card Title" is enabled)
- **Show Metadata**: Toggle to show/hide metadata section (expandable for granular control)
- When expanded, you can control individual metadata items:
- Cache Status
- Observation Time
- Forecast Time
- Weather Station
- Distance
- Next Update
- Frame Times
- **Metadata Position**: Choose where to display metadata (Above Image, Below Image, Overlay on Image)
- **Metadata Style**: Choose display style (Cards, Compact, Minimal)
- **Show Controls**: Toggle to show/hide controls section (expandable for granular control)
- When expanded, you can control individual controls:
- Play/Pause Button
- Previous/Next Buttons
- Frame Slider
- Navigation Buttons (-10, +10, First, Last)
- Frame Info
- **Overlay Controls on Image**: Toggle to overlay controls on the radar image
- **Overlay Position**: Choose overlay position (Top, Bottom, Left, Right, Center)
- **Overlay Opacity**: Control overlay transparency (0.0 to 1.0)
- **Image Zoom**: Zoom level for radar images (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, range: 0.5-3.0)
- **Image Fit**: How images fit in container (Contain, Cover, Fill)
**Slideshow**: **Slideshow**:
- **Timespan**: Select historical data range - `latest` (Latest 7 frames), `1h`, `3h`, `6h`, `12h`, or `24h` (default: `latest`) - **Timespan**: Select historical data range - `latest` (Latest 7 frames), `1h`, `3h`, `6h`, `12h`, `24h`, or `custom` (default: `latest`)
- **Frame Interval**: Seconds between frames during animation (default: `2.0`, range: 0.5-10) - **Frame Interval**: Seconds between frames during animation (default: `2.0`, range: 0.5-10)
- **Auto Play**: Automatically start animation when data loads (default: `true`) - **Auto Play**: Automatically start animation when data loads (default: `true`)
@@ -183,20 +213,75 @@ custom_end_time: "2024-01-15T14:00:00Z"
### Configuration Options ### Configuration Options
#### Service Configuration
| Option | Type | Default | Required | Description | | Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------| |--------|------|---------|----------|-------------|
| `service_url` | string | `http://localhost:8082` | No | Base URL of the BOM Local Service | | `service_url` | string | `http://localhost:8082` | No | Base URL of the BOM Local Service |
| `suburb` | string | - | **Yes** | Suburb name (e.g., "Pomona", "Brisbane") | | `suburb` | string | - | **Yes** | Suburb name (e.g., "Pomona", "Brisbane") |
| `state` | string | - | **Yes** | State abbreviation (e.g., "QLD", "NSW", "VIC") | | `state` | string | - | **Yes** | State abbreviation (e.g., "QLD", "NSW", "VIC") - Use dropdown in editor |
| `card_title` | string | - | No | Custom title displayed at the top of the card |
| `show_metadata` | boolean | `true` | No | Show/hide cache status, observation time, and weather station info | #### Display Options
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| `show_card_title` | boolean | `true` | No | Show/hide card title (uses HA card header) |
| `card_title` | string | - | No | Custom title displayed in card header |
| `show_metadata` | boolean \| object | `true` | No | Show/hide metadata. Can be `true`/`false` or object for granular control (see below) |
| `show_controls` | boolean \| object | `true` | No | Show/hide controls. Can be `true`/`false` or object for granular control (see below) |
| `image_zoom` | number | `1.0` | No | Image zoom level: 0.5 = 50%, 1.0 = 100%, 2.0 = 200% (range: 0.5-3.0) |
| `image_fit` | string | `contain` | No | How image fits in container: `contain`, `cover`, or `fill` |
| `overlay_controls` | boolean | `false` | No | Overlay controls on the radar image |
| `overlay_position` | string | `bottom` | No | Overlay position: `top`, `bottom`, `left`, `right`, or `center` |
| `overlay_opacity` | number | `0.9` | No | Overlay opacity (range: 0.0-1.0) |
#### Metadata Display Configuration (when `show_metadata` is an object)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `show_cache_status` | boolean | `true` | Show cache validity status |
| `show_observation_time` | boolean | `true` | Show observation time (absolute and relative) |
| `show_forecast_time` | boolean | `true` | Show forecast time |
| `show_weather_station` | boolean | `true` | Show weather station name |
| `show_distance` | boolean | `true` | Show distance to weather station |
| `show_next_update` | boolean | `true` | Show next update time |
| `show_frame_times` | boolean | `true` | Show frame observation times |
| `position` | string | `above` | Where to display: `above`, `below`, or `overlay` |
| `style` | string | `cards` | Display style: `cards`, `compact`, or `minimal` |
#### Controls Display Configuration (when `show_controls` is an object)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `show_play_pause` | boolean | `true` | Show play/pause button |
| `show_prev_next` | boolean | `true` | Show previous/next buttons |
| `show_slider` | boolean | `true` | Show frame slider |
| `show_nav_buttons` | boolean | `true` | Show navigation buttons (First, -10, +10, Last) |
| `show_frame_info` | boolean | `true` | Show frame information (frame number, timestamp) |
| `position` | string | `below` | Where to display: `above`, `below`, or `overlay` |
#### Slideshow Configuration
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| `timespan` | string | `latest` | No | Historical data timespan: `latest`, `1h`, `3h`, `6h`, `12h`, `24h`, or `custom` | | `timespan` | string | `latest` | No | Historical data timespan: `latest`, `1h`, `3h`, `6h`, `12h`, `24h`, or `custom` |
| `frame_interval` | number | `2.0` | No | Seconds between frames during animation (range: 0.5-10) | | `frame_interval` | number | `2.0` | No | Seconds between frames during animation (range: 0.5-10) |
| `auto_play` | boolean | `true` | No | Automatically start animation when data loads | | `auto_play` | boolean | `true` | No | Automatically start animation when data loads |
| `refresh_interval` | number | `30` | No | Seconds between automatic data refreshes (range: 10-300) |
| `custom_start_time` | string | - | No | ISO 8601 datetime for custom timespan start (requires `timespan: custom`) | | `custom_start_time` | string | - | No | ISO 8601 datetime for custom timespan start (requires `timespan: custom`) |
| `custom_end_time` | string | - | No | ISO 8601 datetime for custom timespan end (requires `timespan: custom`) | | `custom_end_time` | string | - | No | ISO 8601 datetime for custom timespan end (requires `timespan: custom`) |
#### Auto-Refresh Configuration
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| `refresh_interval` | number | `30` | No | Seconds between automatic data refreshes (range: 10-300) |
#### Localization
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| `locale` | string | HA locale | No | Override locale for date/time formatting (e.g., "en-AU", "en-US") |
## Usage Examples ## Usage Examples
### Basic Configuration ### Basic Configuration
@@ -210,6 +295,81 @@ state: QLD
service_url: http://192.168.1.100:8082 service_url: http://192.168.1.100:8082
``` ```
### Minimal Display (Image Only)
Show just the radar image with no controls or metadata:
```yaml
type: custom:bom-local-radar-card
suburb: Brisbane
state: QLD
show_metadata: false
show_controls: false
```
### Overlay Controls on Image
Overlay controls on the radar image to save space:
```yaml
type: custom:bom-local-radar-card
suburb: Brisbane
state: QLD
overlay_controls: true
overlay_position: bottom
overlay_opacity: 0.85
show_metadata:
position: overlay
style: minimal
```
### Image Zoom
Zoom in on the radar image (may pixelate at higher zoom levels):
```yaml
type: custom:bom-local-radar-card
suburb: Brisbane
state: QLD
image_zoom: 1.5
image_fit: contain
```
### Granular Metadata Control
Show only specific metadata items:
```yaml
type: custom:bom-local-radar-card
suburb: Brisbane
state: QLD
show_metadata:
show_cache_status: true
show_observation_time: true
show_weather_station: false
show_distance: false
show_next_update: false
show_frame_times: true
position: above
style: compact
```
### Granular Controls Control
Show only specific controls:
```yaml
type: custom:bom-local-radar-card
suburb: Brisbane
state: QLD
show_controls:
show_play_pause: true
show_slider: true
show_frame_info: true
show_nav_buttons: false
show_prev_next: false
```
### Historical Data (Last 3 Hours) ### Historical Data (Last 3 Hours)
View radar history from the past 3 hours: View radar history from the past 3 hours:
@@ -265,7 +425,7 @@ refresh_interval: 60
## Controls ## Controls
The card provides several controls for navigating radar frames: The card provides several controls for navigating radar frames (all can be individually shown/hidden):
- **Play/Pause Button**: Start or stop the animation - **Play/Pause Button**: Start or stop the animation
- **Previous/Next Buttons**: Navigate to the previous or next frame - **Previous/Next Buttons**: Navigate to the previous or next frame
@@ -274,8 +434,31 @@ The card provides several controls for navigating radar frames:
- ⏮ First frame - ⏮ First frame
- -10 / +10: Jump backward/forward by 10 frames - -10 / +10: Jump backward/forward by 10 frames
- ⏭ Last frame - ⏭ Last frame
- **Frame Info**: Displays frame number, total frames, observation time, and progress percentage
The card displays frame information including frame number, total frames, and timestamp. ### Keyboard Navigation
The card supports full keyboard navigation when focused:
- `←` / `→`: Navigate to previous/next frame
- `Space`: Play/pause animation
- `Home`: Jump to first frame
- `End`: Jump to last frame
### Error Handling
The card provides enhanced error handling with intelligent retry logic:
- **Structured Error Messages**: Detailed error information from the service with error codes and types
- **Smart Auto-Retry**: Automatically retries failed requests based on service recommendations:
- **Normal retries**: Auto-retries after service-suggested delay (typically 30-120 seconds)
- **Manual refresh recommended**: When previous update failed, shows message and skips auto-retry
- **Network issues**: Suggests checking network before retrying
- **Time range errors**: No auto-retry, suggests adjusting time range
- **Error Details**: Shows error codes, previous update failures, available data ranges, and refresh endpoints
- **Retry Button**: Manual retry option - text changes to "Retry Anyway" when manual refresh is recommended
- **Better Cache Messages**: Clear messages for fresh start scenarios, cache generation status, and update progress
- **Service Integration**: Respects service-calculated retry times and action recommendations
## Development ## Development
@@ -411,6 +594,50 @@ To test the card against a local version of the BOM Local Service (instead of th
This builds the service from your local source and uses it in the test environment. This builds the service from your local source and uses it in the test environment.
## Changelog
### Version 0.1.0 (Current)
**Major Improvements:**
- ✨ **Enhanced Error Handling**: Full support for structured API error responses with detailed error codes, retry suggestions, and intelligent auto-retry functionality
- 🔄 **Smart Retry Logic**: Action-based retry behavior - respects service recommendations for when to auto-retry vs. manual refresh
- 🎨 **Redesigned Editor**: Complete overhaul of the visual editor with native dropdowns, better layout, and expandable sections
- 🖼️ **Image Display Fixes**: Fixed image centering issues and added zoom/fit options
- 🎛️ **Granular Configuration**: Fine-grained control over metadata and controls visibility
- 📍 **Overlay Support**: Overlay controls and metadata on images to save dashboard space
- ⌨️ **Keyboard Navigation**: Full keyboard support for accessibility
- ♿ **Accessibility**: ARIA labels and screen reader support
- 🔍 **Better Error Messages**: Detailed error information including previous update failures and available data ranges
**New Configuration Options:**
- `show_card_title`: Control card title visibility
- `show_metadata`: Object-based configuration for granular metadata control
- `show_controls`: Object-based configuration for granular controls control
- `image_zoom`: Zoom images from 0.5x to 3.0x
- `image_fit`: Control how images fit (contain/cover/fill)
- `overlay_controls`: Overlay controls on image
- `overlay_position`: Control overlay position
- `overlay_opacity`: Control overlay transparency
**Bug Fixes:**
- Fixed editor dropdown crashes
- Fixed image centering issues
- Fixed editor layout problems
- Improved error handling for fresh cache scenarios
**Service Integration:**
- Enhanced compatibility with service's action-based error recommendations
- Proper handling of `manual_refresh_recommended` action type
- Respects service-calculated retry times based on update progress
- Displays refresh endpoint URLs in error details
### Version 0.0.1 (Initial Release)
- Initial release with basic radar display functionality
- Support for latest frames and historical data
- Basic configuration options
- Visual editor support
## License ## License
MIT License - see [LICENSE](LICENSE) file for details MIT License - see [LICENSE](LICENSE) file for details
@@ -427,20 +654,61 @@ MIT License - see [LICENSE](LICENSE) file for details
- Ensure both `suburb` and `state` are configured - Ensure both `suburb` and `state` are configured
- Verify the configuration using the visual editor - Verify the configuration using the visual editor
### Card Shows "Failed to fetch radar data" or "Cache not ready" ### Card Shows Error Messages
- **Check BOM Local Service is running**: Verify the service is accessible at the configured `service_url` The card now provides detailed error messages with intelligent retry behavior:
- **Verify Service URL**: Ensure the URL is correct and reachable from your Home Assistant instance
- **Check Cache Status**: The service may be generating the cache for your location. Wait a minute and refresh - **"Cache Not Ready" / "CACHE_NOT_FOUND"**:
- **Network Access**: If the service is on a different machine, ensure network connectivity and firewall rules allow access - This is normal for fresh installations or new locations
- The service automatically triggers a cache update in the background
- The card will auto-retry after the service-suggested time (typically 30-120 seconds, calculated based on update progress)
- You can manually retry using the "Retry Now" button
- **Check BOM Local Service is running**: Verify the service is accessible at the configured `service_url`
- **Verify Service URL**: Ensure the URL is correct and reachable from your Home Assistant instance
- **Wait for Cache Generation**: First-time cache generation takes 30-60 seconds
- **"Previous Update Failed" / "Manual Refresh Recommended"**:
- A previous cache update attempt failed
- The error details will show the specific failure reason and error code
- **Auto-retry is disabled** - the card shows "Manual refresh recommended" message
- The retry button changes to "Retry Anyway" if you want to force a retry
- Check service logs for more details about the failure
- The error details include a refresh endpoint URL for manual cache refresh
- **"TIME_RANGE_ERROR"**:
- The requested time range exceeds available data or maximum allowed duration
- Error details show available data range and requested range
- **Auto-retry is disabled** - adjust your timespan to match available data
- The service suggests the available time range in error details
- **Network Errors**:
- Check network connectivity between Home Assistant and the service
- Verify firewall rules allow access
- Check CORS configuration if accessing from browser
- The service may suggest waiting longer before retrying network-related errors
**Understanding Error Actions:**
- The service provides an `action` field in error responses that guides retry behavior:
- `retry_after_seconds`: Card will auto-retry after the suggested delay
- `manual_refresh_recommended`: Card shows message but doesn't auto-retry (previous update failed)
- `check_network_and_retry`: Suggests checking network before retrying
- `adjust_time_range`: No auto-retry, suggests adjusting time range
### Card Shows "Radar data not found" ### Card Shows "Radar data not found"
- The cache may not be available for your location yet - The cache may not be available for your location yet
- Trigger a cache update via the BOM Local Service API: - The card will automatically retry after the service-suggested delay
```bash - If auto-retry is disabled (manual refresh recommended), you can:
curl -X POST http://your-service-url/api/cache/YourSuburb/YourState/refresh - Click "Retry Anyway" button in the error message
``` - Or trigger a cache update via the BOM Local Service API:
```bash
curl -X POST http://your-service-url/api/cache/YourSuburb/YourState/refresh
```
- The error details include the refresh endpoint URL for your location
- Check the error message details for specific information about cache status, including:
- Whether an update is in progress
- When the next update is expected
- Previous update failure reasons (if applicable)
### Images Don't Load ### Images Don't Load
@@ -453,6 +721,19 @@ MIT License - see [LICENSE](LICENSE) file for details
- Check that `auto_play` is set to `true` (default) - Check that `auto_play` is set to `true` (default)
- Verify frames are loading (check frame count display) - Verify frames are loading (check frame count display)
- Try manually clicking the Play button - Try manually clicking the Play button
- Check that controls are visible (may be hidden via `show_controls` configuration)
### Editor Issues
- **Dropdowns not working**: The editor now uses native HTML select elements for better compatibility
- **Layout looks jumbled**: The editor has been redesigned with better spacing and organization
- **State selection**: Use the dropdown to select from all Australian states (no need to type abbreviations)
### Image Display Issues
- **Image not centered**: This has been fixed in the latest version
- **Image too small/large**: Use the `image_zoom` option (0.5 to 3.0) to adjust size
- **Image doesn't fit properly**: Try different `image_fit` options (`contain`, `cover`, `fill`)
### Service URL Configuration ### Service URL Configuration
+6
View File
@@ -5,6 +5,9 @@ services:
container_name: bom-card-test-service container_name: bom-card-test-service
ports: ports:
- "8082:8080" - "8082:8080"
volumes:
# Persist cache between redeploys
- ./test-ha/cache:/app/cache
environment: environment:
# ASP.NET Core configuration # ASP.NET Core configuration
- ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_ENVIRONMENT=Production
@@ -20,6 +23,9 @@ services:
- CACHEDIRECTORY=/app/cache - CACHEDIRECTORY=/app/cache
- CACHERETENTIONHOURS=24 - CACHERETENTIONHOURS=24
- TIMEZONE=Australia/Brisbane - TIMEZONE=Australia/Brisbane
# Debug output enabled by default for test environment
- DEBUG__ENABLED=true
- DEBUG__WAITMS=2000
shm_size: '1gb' shm_size: '1gb'
ipc: host ipc: host
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bom-local-radar-card", "name": "bom-local-radar-card",
"version": "0.0.1", "version": "0.1.1",
"description": "Home Assistant custom card for displaying Australian Bureau of Meteorology (BOM) rain radar data with animated slideshow and historical data support", "description": "Home Assistant custom card for displaying Australian Bureau of Meteorology (BOM) rain radar data with animated slideshow and historical data support",
"module": "bom-local-radar-card.js", "module": "bom-local-radar-card.js",
"keywords": [ "keywords": [
+47 -1
View File
@@ -100,6 +100,20 @@ clean_test() {
fi fi
fi fi
# Clean up cache directory (persisted between redeploys, but removed on full clean)
if [ -d "test-ha/cache" ]; then
echo " Removing cache directory..."
if ! rm -rf test-ha/cache 2>/dev/null; then
# Use Docker to remove root-owned files
docker run --rm \
-v "$(pwd)/test-ha/cache:/cache:rw" \
-u root \
alpine:latest \
sh -c "rm -rf /cache/*" 2>/dev/null || true
fi
echo " ✅ Cleared cache directory"
fi
# Optionally clean build artifacts # Optionally clean build artifacts
if [ "${CLEAN_BUILD:-0}" = "1" ]; then if [ "${CLEAN_BUILD:-0}" = "1" ]; then
echo " Removing build artifacts..." echo " Removing build artifacts..."
@@ -110,6 +124,33 @@ clean_test() {
echo "✅ Cleanup complete. Run './run.sh test' to start fresh." echo "✅ Cleanup complete. Run './run.sh test' to start fresh."
} }
# Stop function - stops containers without removing data
stop_test() {
echo "🛑 Stopping test environment (preserving cache and data)..."
# Determine compose files to use
COMPOSE_FILES="-f docker-compose.test.yml"
if [ -f "docker-compose.test.local.yml" ]; then
COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.test.local.yml"
fi
# Stop containers (preserve volumes to keep cache)
if docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
elif command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE_CMD="docker-compose"
else
echo "❌ Error: docker compose not found"
exit 1
fi
if $DOCKER_COMPOSE_CMD $COMPOSE_FILES down 2>/dev/null; then
echo "✅ Test environment stopped (cache and data preserved)"
else
echo "⚠️ No running containers found or error stopping containers"
fi
}
# Update function - rebuilds card and updates running test environment # Update function - rebuilds card and updates running test environment
update_card() { update_card() {
local method="$1" local method="$1"
@@ -143,6 +184,9 @@ case "$COMMAND" in
test) test)
./scripts/test.sh ./scripts/test.sh
;; ;;
stop)
stop_test
;;
update) update)
update_card "$BUILD_METHOD" update_card "$BUILD_METHOD"
;; ;;
@@ -155,6 +199,7 @@ case "$COMMAND" in
echo "Commands:" echo "Commands:"
echo " build [docker|npm] - Build the card (auto-detects method if not specified)" echo " build [docker|npm] - Build the card (auto-detects method if not specified)"
echo " test - Build and start test Home Assistant environment" echo " test - Build and start test Home Assistant environment"
echo " stop - Stop test environment (preserves cache and data)"
echo " update [docker|npm] - Rebuild card and update running test environment" echo " update [docker|npm] - Rebuild card and update running test environment"
echo " (preserves HA state, auto-detects if containers running)" echo " (preserves HA state, auto-detects if containers running)"
echo " clean - Clean test environment (stops containers, removes data)" echo " clean - Clean test environment (stops containers, removes data)"
@@ -167,8 +212,9 @@ case "$COMMAND" in
echo " ./run.sh build # Auto-detect: Docker (preferred) or npm" echo " ./run.sh build # Auto-detect: Docker (preferred) or npm"
echo " ./run.sh build docker # Force Docker build" echo " ./run.sh build docker # Force Docker build"
echo " ./run.sh test # Build and start test environment" echo " ./run.sh test # Build and start test environment"
echo " ./run.sh stop # Stop test environment (preserves cache)"
echo " ./run.sh update # Rebuild and update running environment" echo " ./run.sh update # Rebuild and update running environment"
echo " ./run.sh clean # Clean test environment" echo " ./run.sh clean # Clean test environment (removes everything)"
echo "" echo ""
echo "Note: For more options (e.g., --service-path), use ./scripts/test.sh directly" echo "Note: For more options (e.g., --service-path), use ./scripts/test.sh directly"
exit 1 exit 1
+13 -18
View File
@@ -389,25 +389,15 @@ if [ -f "docker-compose.test.local.yml" ]; then
COMPOSE_FILES_CLEAN="$COMPOSE_FILES_CLEAN -f docker-compose.test.local.yml" COMPOSE_FILES_CLEAN="$COMPOSE_FILES_CLEAN -f docker-compose.test.local.yml"
fi fi
# Stop and remove containers with volumes # Stop and remove containers (preserve volumes to keep cache)
docker compose $COMPOSE_FILES_CLEAN down -v 2>/dev/null || docker-compose $COMPOSE_FILES_CLEAN down -v 2>/dev/null || true docker compose $COMPOSE_FILES_CLEAN down 2>/dev/null || docker-compose $COMPOSE_FILES_CLEAN down 2>/dev/null || true
# Clean up .storage directory if it exists (using Docker if needed)
if [ -d "test-ha/config/.storage" ]; then
echo "🧹 Cleaning up existing .storage directory..."
if ! rm -rf test-ha/config/.storage 2>/dev/null; then
echo " Removing with Docker (files may be owned by root)..."
docker run --rm \
-v "$(pwd)/test-ha/config:/config:rw" \
-u root \
alpine:latest \
sh -c "rm -rf /config/.storage" 2>/dev/null || true
fi
fi
# Preserve .storage directory to keep user accounts and HA state
# Only create it if it doesn't exist
echo "📦 Ensuring test directories exist..." echo "📦 Ensuring test directories exist..."
mkdir -p test-ha/config/.storage mkdir -p test-ha/config/.storage
mkdir -p test-ha/config/www mkdir -p test-ha/config/www
mkdir -p test-ha/cache
# Ensure directories exist (Docker will create files as current user due to user: setting in compose) # Ensure directories exist (Docker will create files as current user due to user: setting in compose)
# Copy built card file to www directory (for /local/ access in HA) # Copy built card file to www directory (for /local/ access in HA)
@@ -486,9 +476,11 @@ done
# Create onboarding bypass - skip everything EXCEPT user creation # Create onboarding bypass - skip everything EXCEPT user creation
# This way HA will prompt for user creation but skip other setup steps # This way HA will prompt for user creation but skip other setup steps
echo "📝 Creating onboarding bypass..." # Only create if it doesn't exist (preserve existing state)
mkdir -p test-ha/config/.storage if [ ! -f "test-ha/config/.storage/onboarding" ]; then
cat > test-ha/config/.storage/onboarding <<EOF echo "📝 Creating onboarding bypass (first time setup)..."
mkdir -p test-ha/config/.storage
cat > test-ha/config/.storage/onboarding <<EOF
{ {
"data": { "data": {
"done": ["core_config", "integration"] "done": ["core_config", "integration"]
@@ -497,6 +489,9 @@ cat > test-ha/config/.storage/onboarding <<EOF
"version": 3 "version": 3
} }
EOF EOF
else
echo "✅ Preserving existing Home Assistant state (user accounts, settings, etc.)"
fi
# Try docker compose (newer) first, fallback to docker-compose (older) # Try docker compose (newer) first, fallback to docker-compose (older)
# Define this early so it can be used for service builds # Define this early so it can be used for service builds
+1677 -181
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
export const CARD_VERSION = '0.0.1'; export const CARD_VERSION = '0.1.1';
export const DEFAULT_SERVICE_URL = 'http://localhost:8082'; export const DEFAULT_SERVICE_URL = 'http://localhost:8082';
export const DEFAULT_FRAME_INTERVAL = 2.0; // seconds export const DEFAULT_FRAME_INTERVAL = 2.0; // seconds
export const DEFAULT_RESTART_DELAY = 2000; // ms (pause before looping) export const DEFAULT_RESTART_DELAY = 2000; // ms (pause before looping)
+482 -33
View File
@@ -2,12 +2,26 @@ import { LitElement, html, css, type CSSResultGroup } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { BomLocalRadarCardConfig } from './types'; import { BomLocalRadarCardConfig, MetadataDisplayConfig, ControlsDisplayConfig } from './types';
// Australian states for dropdown
const AUSTRALIAN_STATES = [
{ value: 'ACT', label: 'Australian Capital Territory' },
{ value: 'NSW', label: 'New South Wales' },
{ value: 'NT', label: 'Northern Territory' },
{ value: 'QLD', label: 'Queensland' },
{ value: 'SA', label: 'South Australia' },
{ value: 'TAS', label: 'Tasmania' },
{ value: 'VIC', label: 'Victoria' },
{ value: 'WA', label: 'Western Australia' },
];
@customElement('bom-local-radar-card-editor') @customElement('bom-local-radar-card-editor')
export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardEditor { export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config: BomLocalRadarCardConfig = this._mergeWithDefaults(); @state() private _config: BomLocalRadarCardConfig = this._mergeWithDefaults();
@state() private _metadataExpanded = false;
@state() private _controlsExpanded = false;
private _mergeWithDefaults(config: Partial<BomLocalRadarCardConfig> = {}): BomLocalRadarCardConfig { private _mergeWithDefaults(config: Partial<BomLocalRadarCardConfig> = {}): BomLocalRadarCardConfig {
const defaults: Partial<BomLocalRadarCardConfig> = { const defaults: Partial<BomLocalRadarCardConfig> = {
@@ -16,8 +30,12 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
frame_interval: 2.0, frame_interval: 2.0,
refresh_interval: 30, refresh_interval: 30,
auto_play: true, auto_play: true,
show_timestamp: true, show_card_title: true,
show_metadata: true, show_metadata: true,
show_controls: true,
image_zoom: 1.0,
image_fit: 'contain',
overlay_opacity: 0.9,
}; };
return { return {
@@ -51,43 +69,268 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
@input=${(e: Event) => this._updateConfig('suburb', (e.target as HTMLInputElement).value)} @input=${(e: Event) => this._updateConfig('suburb', (e.target as HTMLInputElement).value)}
required required
></ha-textfield> ></ha-textfield>
<ha-textfield <div class="select-wrapper">
label="State" <label class="select-label">State *</label>
.value=${this._config.state || ''} <select
@input=${(e: Event) => this._updateConfig('state', (e.target as HTMLInputElement).value)} class="native-select"
helper="State abbreviation (e.g., QLD, NSW, VIC)" .value=${this._config.state || ''}
required @change=${(e: Event) => {
></ha-textfield> const select = e.target as HTMLSelectElement;
this._updateConfig('state', select.value);
}}
required
>
<option value="">Select a state...</option>
${AUSTRALIAN_STATES.map(state =>
html`<option value="${state.value}">${state.label} (${state.value})</option>`
)}
</select>
</div>
</div> </div>
<div class="section"> <div class="section">
<h3>Display</h3> <h3>Display</h3>
<ha-textfield
label="Card Title"
.value=${this._config.card_title || ''}
@input=${(e: Event) => this._updateConfig('card_title', (e.target as HTMLInputElement).value)}
></ha-textfield>
<ha-switch <ha-switch
label="Show Metadata" label="Show Card Title"
.checked=${this._config.show_metadata !== false} .checked=${this._config.show_card_title !== false}
@change=${(e: Event) => this._updateConfig('show_metadata', (e.target as HTMLInputElement).checked)} @change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateConfig('show_card_title', checked);
}}
></ha-switch> ></ha-switch>
${this._config.show_card_title !== false ? html`
<ha-textfield
label="Card Title"
.value=${this._config.card_title || ''}
@input=${(e: Event) => this._updateConfig('card_title', (e.target as HTMLInputElement).value)}
helper="Leave empty to use default"
></ha-textfield>
` : ''}
<div class="metadata-section">
<div class="section-header">
<ha-switch
label="Show Metadata"
.checked=${this._getMetadataEnabled()}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateMetadataToggle(checked);
}}
></ha-switch>
${this._getMetadataEnabled() ? html`
<ha-icon-button
.label=${this._metadataExpanded ? 'Collapse' : 'Expand'}
@click=${() => { this._metadataExpanded = !this._metadataExpanded; }}
>
<ha-icon .icon=${this._metadataExpanded ? 'mdi:chevron-up' : 'mdi:chevron-down'}></ha-icon>
</ha-icon-button>
` : ''}
</div>
${this._getMetadataEnabled() && this._metadataExpanded ? html`
<div class="metadata-options">
<ha-switch
label="Cache Status"
.checked=${this._getMetadataConfig('show_cache_status')}
@change=${(e: Event) => this._updateMetadataConfig('show_cache_status', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Observation Time"
.checked=${this._getMetadataConfig('show_observation_time')}
@change=${(e: Event) => this._updateMetadataConfig('show_observation_time', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Forecast Time"
.checked=${this._getMetadataConfig('show_forecast_time')}
@change=${(e: Event) => this._updateMetadataConfig('show_forecast_time', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Weather Station"
.checked=${this._getMetadataConfig('show_weather_station')}
@change=${(e: Event) => this._updateMetadataConfig('show_weather_station', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Distance"
.checked=${this._getMetadataConfig('show_distance')}
@change=${(e: Event) => this._updateMetadataConfig('show_distance', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Next Update"
.checked=${this._getMetadataConfig('show_next_update')}
@change=${(e: Event) => this._updateMetadataConfig('show_next_update', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Frame Times"
.checked=${this._getMetadataConfig('show_frame_times')}
@change=${(e: Event) => this._updateMetadataConfig('show_frame_times', (e.target as HTMLInputElement).checked)}
></ha-switch>
<div class="select-wrapper">
<label class="select-label">Metadata Position</label>
<select
class="native-select"
.value=${this._getMetadataConfig('position') || 'above'}
@change=${(e: Event) => {
const select = e.target as HTMLSelectElement;
this._updateMetadataConfig('position', select.value as 'above' | 'below' | 'overlay');
}}
>
<option value="above">Above Image</option>
<option value="below">Below Image</option>
<option value="overlay">Overlay on Image</option>
</select>
</div>
<div class="select-wrapper">
<label class="select-label">Metadata Style</label>
<select
class="native-select"
.value=${this._getMetadataConfig('style') || 'cards'}
@change=${(e: Event) => {
const select = e.target as HTMLSelectElement;
this._updateMetadataConfig('style', select.value as 'cards' | 'compact' | 'minimal');
}}
>
<option value="cards">Cards</option>
<option value="compact">Compact</option>
<option value="minimal">Minimal</option>
</select>
</div>
</div>
` : ''}
</div>
<div class="controls-section">
<div class="section-header">
<ha-switch
label="Show Controls"
.checked=${this._getControlsEnabled()}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateControlsToggle(checked);
}}
></ha-switch>
${this._getControlsEnabled() ? html`
<ha-icon-button
.label=${this._controlsExpanded ? 'Collapse' : 'Expand'}
@click=${() => { this._controlsExpanded = !this._controlsExpanded; }}
>
<ha-icon .icon=${this._controlsExpanded ? 'mdi:chevron-up' : 'mdi:chevron-down'}></ha-icon>
</ha-icon-button>
` : ''}
</div>
${this._getControlsEnabled() && this._controlsExpanded ? html`
<div class="controls-options">
<ha-switch
label="Play/Pause Button"
.checked=${this._getControlsConfig('show_play_pause')}
@change=${(e: Event) => this._updateControlsConfig('show_play_pause', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Previous/Next Buttons"
.checked=${this._getControlsConfig('show_prev_next')}
@change=${(e: Event) => this._updateControlsConfig('show_prev_next', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Frame Slider"
.checked=${this._getControlsConfig('show_slider')}
@change=${(e: Event) => this._updateControlsConfig('show_slider', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Navigation Buttons (-10, +10, First, Last)"
.checked=${this._getControlsConfig('show_nav_buttons')}
@change=${(e: Event) => this._updateControlsConfig('show_nav_buttons', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Frame Info"
.checked=${this._getControlsConfig('show_frame_info')}
@change=${(e: Event) => this._updateControlsConfig('show_frame_info', (e.target as HTMLInputElement).checked)}
></ha-switch>
<ha-switch
label="Overlay Controls on Image"
.checked=${this._config.overlay_controls || false}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateConfig('overlay_controls', checked);
}}
></ha-switch>
${this._config.overlay_controls ? html`
<div class="select-wrapper">
<label class="select-label">Overlay Position</label>
<select
class="native-select"
.value=${this._config.overlay_position || 'bottom'}
@change=${(e: Event) => {
const select = e.target as HTMLSelectElement;
this._updateConfig('overlay_position', select.value);
}}
>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
<option value="center">Center</option>
</select>
</div>
<ha-textfield
label="Overlay Opacity (0.0 - 1.0)"
type="number"
step="0.1"
min="0"
max="1"
.value=${String(this._config.overlay_opacity ?? 0.9)}
@input=${(e: Event) => this._updateConfig('overlay_opacity', parseFloat((e.target as HTMLInputElement).value))}
></ha-textfield>
` : ''}
</div>
` : ''}
</div>
<ha-textfield
label="Image Zoom (1.0 = 100%)"
type="number"
step="0.1"
min="0.5"
max="3.0"
.value=${String(this._config.image_zoom || 1.0)}
@input=${(e: Event) => this._updateConfig('image_zoom', parseFloat((e.target as HTMLInputElement).value))}
helper="Zoom level: 0.5 = 50%, 1.0 = 100%, 2.0 = 200%"
></ha-textfield>
<div class="select-wrapper">
<label class="select-label">Image Fit</label>
<select
class="native-select"
.value=${this._config.image_fit || 'contain'}
@change=${(e: Event) => {
const select = e.target as HTMLSelectElement;
this._updateConfig('image_fit', select.value);
}}
>
<option value="contain">Contain (fit entire image)</option>
<option value="cover">Cover (fill container)</option>
<option value="fill">Fill (stretch to fit)</option>
</select>
</div>
</div> </div>
<div class="section"> <div class="section">
<h3>Slideshow</h3> <h3>Slideshow</h3>
<ha-select <div class="select-wrapper">
label="Timespan" <label class="select-label">Timespan</label>
.value=${this._config.timespan || 'latest'} <select
@selected=${(e: Event) => this._updateConfig('timespan', (e.target as HTMLSelectElement).value)} class="native-select"
> .value=${this._config.timespan || 'latest'}
<option value="latest">Latest 7 frames</option> @change=${(e: Event) => {
<option value="1h">Last 1 hour</option> const select = e.target as HTMLSelectElement;
<option value="3h">Last 3 hours</option> this._updateConfig('timespan', select.value);
<option value="6h">Last 6 hours</option> }}
<option value="12h">Last 12 hours</option> >
<option value="24h">Last 24 hours</option> <option value="latest">Latest 7 frames</option>
</ha-select> <option value="1h">Last 1 hour</option>
<option value="3h">Last 3 hours</option>
<option value="6h">Last 6 hours</option>
<option value="12h">Last 12 hours</option>
<option value="24h">Last 24 hours</option>
<option value="custom">Custom time range</option>
</select>
</div>
<ha-textfield <ha-textfield
label="Frame Interval (seconds)" label="Frame Interval (seconds)"
type="number" type="number"
@@ -100,7 +343,10 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
<ha-switch <ha-switch
label="Auto Play" label="Auto Play"
.checked=${this._config.auto_play !== false} .checked=${this._config.auto_play !== false}
@change=${(e: Event) => this._updateConfig('auto_play', (e.target as HTMLInputElement).checked)} @change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateConfig('auto_play', checked);
}}
></ha-switch> ></ha-switch>
</div> </div>
@@ -126,10 +372,213 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
fireEvent(this, 'config-changed', { config }); fireEvent(this, 'config-changed', { config });
} }
// Metadata configuration helpers
private _getMetadataEnabled(): boolean {
const config = this._config.show_metadata;
if (config === undefined || config === true) return true;
if (typeof config === 'boolean') return config;
return true; // Object means enabled with custom config
}
private _updateMetadataToggle(enabled: boolean): void {
if (enabled) {
// If enabling, check if we have existing config or create default
if (typeof this._config.show_metadata === 'object') {
// Keep existing config
return;
}
// Create default config object
this._updateConfig('show_metadata', {});
} else {
// Disable metadata
this._updateConfig('show_metadata', false);
}
}
private _getMetadataConfig(key: keyof MetadataDisplayConfig): boolean {
const config = this._config.show_metadata;
if (typeof config === 'boolean') {
return config; // If boolean, all metadata follows this value
}
if (typeof config === 'object') {
return config[key] !== false; // Default to true if not explicitly false
}
return true; // Default
}
private _updateMetadataConfig(key: keyof MetadataDisplayConfig, value: boolean | string): void {
let config: MetadataDisplayConfig;
if (typeof this._config.show_metadata === 'object') {
config = { ...this._config.show_metadata };
} else {
config = {};
}
// Type-safe assignment based on key
if (key === 'position' || key === 'style') {
(config as any)[key] = value;
} else {
(config as any)[key] = value;
}
this._updateConfig('show_metadata', config);
}
// Controls configuration helpers
private _getControlsEnabled(): boolean {
const config = this._config.show_controls;
if (config === undefined || config === true) return true;
if (typeof config === 'boolean') return config;
return true; // Object means enabled with custom config
}
private _updateControlsToggle(enabled: boolean): void {
if (enabled) {
// If enabling, check if we have existing config or create default
if (typeof this._config.show_controls === 'object') {
// Keep existing config
return;
}
// Create default config object
this._updateConfig('show_controls', {});
} else {
// Disable controls
this._updateConfig('show_controls', false);
}
}
private _getControlsConfig(key: keyof ControlsDisplayConfig): boolean {
const config = this._config.show_controls;
if (typeof config === 'boolean') {
return config;
}
if (typeof config === 'object') {
return config[key] !== false;
}
return true; // Default
}
private _updateControlsConfig(key: keyof ControlsDisplayConfig, value: boolean): void {
let config: ControlsDisplayConfig;
if (typeof this._config.show_controls === 'object') {
config = { ...this._config.show_controls };
} else {
config = {};
}
// All control config values are boolean (position is handled separately in template)
(config as any)[key] = value;
this._updateConfig('show_controls', config);
}
static styles: CSSResultGroup = css` static styles: CSSResultGroup = css`
.editor { padding: 8px 16px; } .editor {
.section { margin: 12px 0; } padding: 16px;
.section h3 { margin: 0 0 8px; font-weight: 600; } display: flex;
flex-direction: column;
gap: 24px;
}
.section {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
background: var(--card-background-color, #ffffff);
border-radius: 8px;
border: 1px solid var(--divider-color, #e0e0e0);
}
.section h3 {
margin: 0;
font-weight: 600;
font-size: 1.1em;
color: var(--primary-text-color, #212121);
padding-bottom: 8px;
border-bottom: 2px solid var(--divider-color, #e0e0e0);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding: 8px 0;
}
.metadata-section,
.controls-section {
margin-top: 0;
padding: 12px;
background: var(--secondary-background-color, #fafafa);
border-radius: 8px;
border: 1px solid var(--divider-color, #e0e0e0);
}
.metadata-options,
.controls-options {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--divider-color, #e0e0e0);
}
ha-textfield {
display: block;
width: 100%;
margin-bottom: 0;
}
.select-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.select-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--primary-text-color, #212121);
}
.native-select {
width: 100%;
padding: 12px;
border: 1px solid var(--divider-color, #e0e0e0);
border-radius: 4px;
background: var(--card-background-color, #ffffff);
color: var(--primary-text-color, #212121);
font-size: 1rem;
font-family: inherit;
cursor: pointer;
transition: border-color 0.2s;
}
.native-select:hover {
border-color: var(--primary-color, #03a9f4);
}
.native-select:focus {
outline: none;
border-color: var(--primary-color, #03a9f4);
box-shadow: 0 0 0 2px rgba(3, 169, 244, 0.2);
}
ha-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
margin-bottom: 0;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
`; `;
} }
+72 -2
View File
@@ -50,6 +50,60 @@ export interface CacheRangeInfo {
}; };
} }
// Metadata display configuration
export interface MetadataDisplayConfig {
show_cache_status?: boolean;
show_observation_time?: boolean;
show_forecast_time?: boolean;
show_weather_station?: boolean;
show_distance?: boolean;
show_next_update?: boolean;
show_frame_times?: boolean; // Show frame observation times
position?: 'above' | 'below' | 'overlay';
style?: 'cards' | 'compact' | 'minimal';
}
// Controls display configuration
export interface ControlsDisplayConfig {
show_play_pause?: boolean;
show_prev_next?: boolean;
show_slider?: boolean;
show_nav_buttons?: boolean; // First, -10, +10, Last
show_frame_info?: boolean; // Frame X of Y, timestamp
position?: 'above' | 'below' | 'overlay';
}
// API Error Response structure matching the service
export interface ApiErrorResponse {
errorCode: string;
message: string;
errorType: string;
details?: Record<string, any>;
suggestions?: Record<string, any>;
timestamp?: string;
}
// Error state interface
export interface ErrorState {
message: string;
type: 'network' | 'cache' | 'config' | 'validation' | 'unknown';
retryable: boolean;
retryAction?: () => void;
retryAfter?: number; // seconds (undefined if manual refresh recommended)
errorCode?: string;
details?: Record<string, any>; // Includes action, refreshEndpoint, statusEndpoint from API suggestions
}
// Grid options for HA sections view
export interface GridOptions {
columns: number;
rows: number;
min_columns?: number;
min_rows?: number;
max_columns?: number;
max_rows?: number;
}
// Card configuration // Card configuration
export interface BomLocalRadarCardConfig extends LovelaceCardConfig { export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
type: 'custom:bom-local-radar-card'; type: 'custom:bom-local-radar-card';
@@ -60,9 +114,22 @@ export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
state: string; // Required: state abbreviation (e.g., "QLD") state: string; // Required: state abbreviation (e.g., "QLD")
// Display // Display
show_card_title?: boolean; // Show/hide card title (uses HA card header)
card_title?: string; card_title?: string;
show_timestamp?: boolean; show_timestamp?: boolean; // Deprecated - use show_metadata instead
show_metadata?: boolean; show_metadata?: boolean | MetadataDisplayConfig; // Granular metadata display
// Control visibility
show_controls?: boolean | ControlsDisplayConfig; // Granular control visibility
// Image display
image_zoom?: number; // 1.0 = 100%, 1.5 = 150%, etc. (0.5 to 3.0)
image_fit?: 'contain' | 'cover' | 'fill';
// Overlay options
overlay_controls?: boolean; // Overlay controls on image
overlay_position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
overlay_opacity?: number; // 0.0 to 1.0
// Slideshow configuration // Slideshow configuration
timespan?: 'latest' | '1h' | '3h' | '6h' | '12h' | '24h' | 'custom'; // Historical data timespan timespan?: 'latest' | '1h' | '3h' | '6h' | '12h' | '24h' | 'custom'; // Historical data timespan
@@ -75,6 +142,9 @@ export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
// Custom time range (for timespan: 'custom') // Custom time range (for timespan: 'custom')
custom_start_time?: string; // ISO 8601 datetime custom_start_time?: string; // ISO 8601 datetime
custom_end_time?: string; // ISO 8601 datetime custom_end_time?: string; // ISO 8601 datetime
// Localization
locale?: string; // Override locale (defaults to HA locale)
} }