mirror of
https://github.com/alexhopeoconnor/bom-local-card.git
synced 2026-10-03 22:22:00 +10:00
v0.1.1: Fix icon alignment, improve error display, and enhance UI editor
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user