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
- 🌧️ **Live Radar Display**: View the latest BOM rain radar images for any Australian location
- 🎬 **Animated Slideshow**: Play through radar frames to see precipitation movement
- 📊 **Historical Data**: View radar history from 1 hour to 24 hours ago
- 🎯 **Location-Based**: Support for any Australian suburb/state combination
- 🎬 **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, or custom time ranges
- 🎯 **Location-Based**: Support for any Australian suburb/state combination with dropdown selection
- 🔄 **Auto-Refresh**: Automatically updates radar data at configurable intervals
- 🎨 **Beautiful UI**: Modern, responsive design that integrates seamlessly with Home Assistant themes
- ⚙️ **Visual Editor**: Full GUI configuration editor (no YAML editing required)
- 🎨 **Modern UI**: Sleek, responsive design that integrates seamlessly with Home Assistant themes
- ⚙️ **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
@@ -138,14 +145,37 @@ For more detailed service setup and configuration options, see the [BOM Local Se
**Service Configuration**:
- **Service URL**: Base URL of your BOM Local Service (default: `http://localhost:8082`)
- **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**:
- **Card Title**: Optional custom title for the card
- **Show Metadata**: Toggle to show/hide cache status, observation time, and weather station info (default: `true`)
- **Show Card Title**: Toggle to show/hide card title (uses HA card header)
- **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**:
- **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)
- **Auto Play**: Automatically start animation when data loads (default: `true`)
@@ -183,20 +213,75 @@ custom_end_time: "2024-01-15T14:00:00Z"
### Configuration Options
#### Service Configuration
| Option | Type | Default | Required | Description |
|--------|------|---------|----------|-------------|
| `service_url` | string | `http://localhost:8082` | No | Base URL of the BOM Local Service |
| `suburb` | string | - | **Yes** | Suburb name (e.g., "Pomona", "Brisbane") |
| `state` | string | - | **Yes** | State abbreviation (e.g., "QLD", "NSW", "VIC") |
| `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 |
| `state` | string | - | **Yes** | State abbreviation (e.g., "QLD", "NSW", "VIC") - Use dropdown in editor |
#### 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` |
| `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 |
| `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_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
### Basic Configuration
@@ -210,6 +295,81 @@ state: QLD
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)
View radar history from the past 3 hours:
@@ -265,7 +425,7 @@ refresh_interval: 60
## 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
- **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
- -10 / +10: Jump backward/forward by 10 frames
- ⏭ 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
@@ -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.
## 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
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
- 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`
- **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
- **Network Access**: If the service is on a different machine, ensure network connectivity and firewall rules allow access
The card now provides detailed error messages with intelligent retry behavior:
- **"Cache Not Ready" / "CACHE_NOT_FOUND"**:
- 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"
- The cache may not be available for your location yet
- Trigger a cache update via the BOM Local Service API:
```bash
curl -X POST http://your-service-url/api/cache/YourSuburb/YourState/refresh
```
- The card will automatically retry after the service-suggested delay
- If auto-retry is disabled (manual refresh recommended), you can:
- 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
@@ -453,6 +721,19 @@ MIT License - see [LICENSE](LICENSE) file for details
- Check that `auto_play` is set to `true` (default)
- Verify frames are loading (check frame count display)
- 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
+6
View File
@@ -5,6 +5,9 @@ services:
container_name: bom-card-test-service
ports:
- "8082:8080"
volumes:
# Persist cache between redeploys
- ./test-ha/cache:/app/cache
environment:
# ASP.NET Core configuration
- ASPNETCORE_ENVIRONMENT=Production
@@ -20,6 +23,9 @@ services:
- CACHEDIRECTORY=/app/cache
- CACHERETENTIONHOURS=24
- TIMEZONE=Australia/Brisbane
# Debug output enabled by default for test environment
- DEBUG__ENABLED=true
- DEBUG__WAITMS=2000
shm_size: '1gb'
ipc: host
restart: unless-stopped
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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",
"module": "bom-local-radar-card.js",
"keywords": [
+47 -1
View File
@@ -100,6 +100,20 @@ clean_test() {
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
if [ "${CLEAN_BUILD:-0}" = "1" ]; then
echo " Removing build artifacts..."
@@ -110,6 +124,33 @@ clean_test() {
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_card() {
local method="$1"
@@ -143,6 +184,9 @@ case "$COMMAND" in
test)
./scripts/test.sh
;;
stop)
stop_test
;;
update)
update_card "$BUILD_METHOD"
;;
@@ -155,6 +199,7 @@ case "$COMMAND" in
echo "Commands:"
echo " build [docker|npm] - Build the card (auto-detects method if not specified)"
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 " (preserves HA state, auto-detects if containers running)"
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 docker # Force Docker build"
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 clean # Clean test environment"
echo " ./run.sh clean # Clean test environment (removes everything)"
echo ""
echo "Note: For more options (e.g., --service-path), use ./scripts/test.sh directly"
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"
fi
# Stop and remove containers with volumes
docker compose $COMPOSE_FILES_CLEAN down -v 2>/dev/null || docker-compose $COMPOSE_FILES_CLEAN down -v 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
# Stop and remove containers (preserve volumes to keep cache)
docker compose $COMPOSE_FILES_CLEAN down 2>/dev/null || docker-compose $COMPOSE_FILES_CLEAN down 2>/dev/null || true
# Preserve .storage directory to keep user accounts and HA state
# Only create it if it doesn't exist
echo "📦 Ensuring test directories exist..."
mkdir -p test-ha/config/.storage
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)
# 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
# This way HA will prompt for user creation but skip other setup steps
echo "📝 Creating onboarding bypass..."
mkdir -p test-ha/config/.storage
cat > test-ha/config/.storage/onboarding <<EOF
# Only create if it doesn't exist (preserve existing state)
if [ ! -f "test-ha/config/.storage/onboarding" ]; then
echo "📝 Creating onboarding bypass (first time setup)..."
mkdir -p test-ha/config/.storage
cat > test-ha/config/.storage/onboarding <<EOF
{
"data": {
"done": ["core_config", "integration"]
@@ -497,6 +489,9 @@ cat > test-ha/config/.storage/onboarding <<EOF
"version": 3
}
EOF
else
echo "✅ Preserving existing Home Assistant state (user accounts, settings, etc.)"
fi
# Try docker compose (newer) first, fallback to docker-compose (older)
# Define this early so it can be used for service builds
+1684 -188
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_FRAME_INTERVAL = 2.0; // seconds
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 { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
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')
export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config: BomLocalRadarCardConfig = this._mergeWithDefaults();
@state() private _metadataExpanded = false;
@state() private _controlsExpanded = false;
private _mergeWithDefaults(config: Partial<BomLocalRadarCardConfig> = {}): BomLocalRadarCardConfig {
const defaults: Partial<BomLocalRadarCardConfig> = {
@@ -16,8 +30,12 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
frame_interval: 2.0,
refresh_interval: 30,
auto_play: true,
show_timestamp: true,
show_card_title: true,
show_metadata: true,
show_controls: true,
image_zoom: 1.0,
image_fit: 'contain',
overlay_opacity: 0.9,
};
return {
@@ -51,43 +69,268 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
@input=${(e: Event) => this._updateConfig('suburb', (e.target as HTMLInputElement).value)}
required
></ha-textfield>
<ha-textfield
label="State"
.value=${this._config.state || ''}
@input=${(e: Event) => this._updateConfig('state', (e.target as HTMLInputElement).value)}
helper="State abbreviation (e.g., QLD, NSW, VIC)"
required
></ha-textfield>
<div class="select-wrapper">
<label class="select-label">State *</label>
<select
class="native-select"
.value=${this._config.state || ''}
@change=${(e: Event) => {
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 class="section">
<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
label="Show Metadata"
.checked=${this._config.show_metadata !== false}
@change=${(e: Event) => this._updateConfig('show_metadata', (e.target as HTMLInputElement).checked)}
label="Show Card Title"
.checked=${this._config.show_card_title !== false}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
this._updateConfig('show_card_title', checked);
}}
></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 class="section">
<h3>Slideshow</h3>
<ha-select
label="Timespan"
.value=${this._config.timespan || 'latest'}
@selected=${(e: Event) => this._updateConfig('timespan', (e.target as HTMLSelectElement).value)}
>
<option value="latest">Latest 7 frames</option>
<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>
</ha-select>
<div class="select-wrapper">
<label class="select-label">Timespan</label>
<select
class="native-select"
.value=${this._config.timespan || 'latest'}
@change=${(e: Event) => {
const select = e.target as HTMLSelectElement;
this._updateConfig('timespan', select.value);
}}
>
<option value="latest">Latest 7 frames</option>
<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
label="Frame Interval (seconds)"
type="number"
@@ -100,7 +343,10 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
<ha-switch
label="Auto Play"
.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>
</div>
@@ -126,10 +372,213 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
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`
.editor { padding: 8px 16px; }
.section { margin: 12px 0; }
.section h3 { margin: 0 0 8px; font-weight: 600; }
.editor {
padding: 16px;
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
export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
type: 'custom:bom-local-radar-card';
@@ -60,9 +114,22 @@ export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
state: string; // Required: state abbreviation (e.g., "QLD")
// Display
show_card_title?: boolean; // Show/hide card title (uses HA card header)
card_title?: string;
show_timestamp?: boolean;
show_metadata?: boolean;
show_timestamp?: boolean; // Deprecated - use show_metadata instead
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
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_start_time?: string; // ISO 8601 datetime
custom_end_time?: string; // ISO 8601 datetime
// Localization
locale?: string; // Override locale (defaults to HA locale)
}