From 77ae2fefd9fe547f2d79359da4a0f3cfce3e27cb Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Sun, 30 Aug 2026 23:08:06 +1000 Subject: [PATCH] Modernize BOM Local release tooling --- Dockerfile.build | 10 +- README.md | 18 +- custom_components/bom_local/manifest.json | 3 +- .../bom_local/www/bom-local-card.js | 2302 ++--------------- package-lock.json | 787 ++++-- package.json | 21 +- rollup.config.js | 5 +- run.sh | 3 +- src/bom-local-card.ts | 5 +- src/components/controls-section.ts | 3 +- 10 files changed, 803 insertions(+), 2354 deletions(-) diff --git a/Dockerfile.build b/Dockerfile.build index c99fbfb..5f29b20 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -4,14 +4,13 @@ FROM node:20-alpine WORKDIR /build -# Copy package files first for better caching -COPY package*.json ./ +# Copy the declared dependency set first for a reproducible cached build. +COPY package.json package-lock.json ./ COPY tsconfig.json ./ COPY rollup.config.js ./ -# Install dependencies (including devDependencies) -# Use npm install since we may have updated package.json -RUN npm install +# Install the exact locked dependencies, including the build-only toolchain. +RUN npm ci # Copy source files COPY src/ ./src/ @@ -20,4 +19,3 @@ COPY src/ ./src/ # This ensures the build runs when the container starts, not during image creation # Use BUILD_TARGET to allow running "watch" or other scripts CMD ["sh", "-c", "npm run ${BUILD_TARGET:-build}"] - diff --git a/README.md b/README.md index 3639763..ea76a92 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The Australian Bureau of Meteorology's radar API endpoint stopped working in Dec - **Home Assistant**: Version 2024.1.0 or later - **Docker**: Required to run the BOM Local Service (Docker Engine 20.10+ or Docker Desktop) - **BOM Local Service**: Must be installed and running (covered in installation steps below) +- **Node.js 20+**: Required only for a local frontend build; the supplied Docker builder uses Node.js 20. ## Installation @@ -535,7 +536,7 @@ For more advanced testing options: ``` **Options:** -- `--skip-build` - Skip building the card (use existing dist file) +- `--skip-build` - Skip building the card (use the existing generated card bundle) - `--force-build` - Force rebuild of the card (no cache) - `--docker-build` - Use Docker to build the card - `--npm-build` - Use npm to build the card (requires local Node.js) @@ -563,20 +564,31 @@ If you prefer to build manually: 2. **Install dependencies:** ```bash - npm install + npm ci ``` 3. **Build the card:** ```bash npm run build ``` - The built file will be in `dist/bom-local-card.js` + The built file is `custom_components/bom_local/www/bom-local-card.js`. 4. **Development with watch mode:** ```bash npm run watch ``` +### Release checklist + +For a production card release, keep the version in `package.json`, +`package-lock.json`, `src/const.ts`, and +`custom_components/bom_local/manifest.json` aligned. Then run `npm ci`, build +both development and production bundles, review the generated +`custom_components/bom_local/www/bom-local-card.js`, commit the source and +generated bundle together, and create the matching immutable `vX.Y.Z` Git tag. +The card's frontend cache query must be updated by the consuming Home Assistant +deployment in the same reviewed change. + ### Test Environment Details The test environment includes: diff --git a/custom_components/bom_local/manifest.json b/custom_components/bom_local/manifest.json index c81b1f5..b36053a 100644 --- a/custom_components/bom_local/manifest.json +++ b/custom_components/bom_local/manifest.json @@ -5,9 +5,8 @@ "config_flow": true, "single_config_entry": true, "iot_class": "local_polling", - "version": "0.3.0", + "version": "0.3.1", "dependencies": ["http", "websocket_api"], "documentation": "https://github.com/alexhopeoconnor/bom-local-card", "issue_tracker": "https://github.com/alexhopeoconnor/bom-local-card/issues" } - diff --git a/custom_components/bom_local/www/bom-local-card.js b/custom_components/bom_local/www/bom-local-card.js index cbb39fa..2a1f9a7 100644 --- a/custom_components/bom_local/www/bom-local-card.js +++ b/custom_components/bom_local/www/bom-local-card.js @@ -1,141 +1,23 @@ -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ - -function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} - -typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; -}; - -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -const t$3=globalThis,e$2=t$3.ShadowRoot&&(void 0===t$3.ShadyCSS||t$3.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$2=Symbol(),o$4=new WeakMap;class n$3{constructor(t,e,o){if(this._$cssResult$=!0,o!==s$2)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$2&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$4.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$4.set(s,t));}return t}toString(){return this.cssText}}const r$5=t=>new n$3("string"==typeof t?t:t+"",void 0,s$2),i$3=(t,...e)=>{const o=1===t.length?t[0]:e.reduce(((e,s,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1]),t[0]);return new n$3(o,t,s$2)},S$1=(s,o)=>{if(e$2)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$3.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$2=e$2?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$5(e)})(t):t; - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */const{is:i$2,defineProperty:e$1,getOwnPropertyDescriptor:h$1,getOwnPropertyNames:r$4,getOwnPropertySymbols:o$3,getPrototypeOf:n$2}=Object,a$1=globalThis,c$1=a$1.trustedTypes,l$1=c$1?c$1.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$1=(t,s)=>!i$2(t,s),b={attribute:!0,type:String,converter:u$1,reflect:!1,useDefault:!1,hasChanged:f$1};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;class y$1 extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=!0),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e$1(this.prototype,t,h);}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h$1(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$2(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...r$4(t),...o$3(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$2(s));}else void 0!==s&&i.push(c$2(s));return i}static _$Eu(t,s){const i=s.attribute;return !1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)));}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$EO?.delete(t);}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((t=>t.hostConnected?.()));}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach((t=>t.hostDisconnected?.()));}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&!0===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null;}}requestUpdate(t,s,i){if(void 0!==t){const e=this.constructor,h=this[t];if(i??=e.getPropertyOptions(t),!((i.hasChanged??f$1)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(e._$Eu(t,i))))return;this.C(t,s,i);}!1===this.isUpdatePending&&(this._$ES=this._$EP());}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),!0!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),!0===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t));}async _$EP(){this.isUpdatePending=!0;try{await this._$ES;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];!0!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e);}}let t=!1;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$EM();}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$EO?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$EM(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return !0}update(t){this._$Eq&&=this._$Eq.forEach((t=>this._$ET(t,this[t]))),this._$EM();}updated(t){}firstUpdated(t){}}y$1.elementStyles=[],y$1.shadowRootOptions={mode:"open"},y$1[d$1("elementProperties")]=new Map,y$1[d$1("finalized")]=new Map,p$1?.({ReactiveElement:y$1}),(a$1.reactiveElementVersions??=[]).push("2.1.1"); - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -const t$2=globalThis,i$1=t$2.trustedTypes,s$1=i$1?i$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,e="$lit$",h=`lit$${Math.random().toFixed(9).slice(2)}$`,o$2="?"+h,n$1=`<${o$2}>`,r$3=document,l=()=>r$3.createComment(""),c=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),T=Symbol.for("lit-noChange"),E=Symbol.for("lit-nothing"),A=new WeakMap,C=r$3.createTreeWalker(r$3,129);function P(t,i){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$1?s$1.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"":3===i?"":"",c=f;for(let i=0;i"===u[0]?(c=r??f,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f?s+n$1:d>=0?(o.push(a),s.slice(0,d)+e+s.slice(d)+h+x):s+h+(-2===d?i:x);}return [P(t,l+(t[s]||"")+(2===i?"":3===i?"":"")),o]};class N{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=V(t,s);if(this.el=N.createElement(f,n),C.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=C.nextNode())&&d.length0){r.textContent=i$1?i$1.emptyScript:"";for(let i=0;i2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=E;}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=S(this,t,i,0),o=!c(t)||t!==this._$AH&&t!==T,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new R(i.insertBefore(l(),t),t,void 0,s??{});}return h._$AI(t),h}; - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */const s=globalThis;class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=B(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1);}render(){return T}}i._$litElement$=!0,i["finalized"]=!0,s.litElementHydrateSupport?.({LitElement:i});const o$1=s.litElementPolyfillSupport;o$1?.({LitElement:i});(s.litElementVersions??=[]).push("4.2.1"); - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -const t$1=t=>(e,o)=>{void 0!==o?o.addInitializer((()=>{customElements.define(t,e);})):customElements.define(t,e);}; - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */const o={attribute:!0,type:String,converter:u$1,reflect:!1,hasChanged:f$1},r$2=(t=o,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),"setter"===n&&((t=Object.create(t)).wrapped=!0),s.set(r.name,t),"accessor"===n){const{name:o}=r;return {set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t);},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t);}}throw Error("Unsupported decorator location: "+n)};function n(t){return (e,o)=>"object"==typeof o?r$2(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)} - -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */function r$1(r){return n({...r,state:!0,attribute:!1})} - -var t,r;!function(e){e.language="language",e.system="system",e.comma_decimal="comma_decimal",e.decimal_comma="decimal_comma",e.space_comma="space_comma",e.none="none";}(t||(t={})),function(e){e.language="language",e.system="system",e.am_pm="12",e.twenty_four="24";}(r||(r={}));var ne=function(e,t,r,n){n=n||{},r=null==r?{}:r;var i=new Event(t,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return i.detail=r,e.dispatchEvent(i),i}; - -// 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' }, -]; -let BomLocalRadarCardEditor = class BomLocalRadarCardEditor extends i { - constructor() { - super(...arguments); - this._config = this._mergeWithDefaults(); - this._metadataExpanded = false; - this._controlsExpanded = false; - } - _mergeWithDefaults(config = {}) { - const defaults = { - service_url: 'http://localhost:8082', - timespan: 'latest', - frame_interval: 2.0, - refresh_interval: 30, - auto_play: true, - show_card_title: true, - show_metadata: true, - show_controls: true, - image_zoom: 1.0, - image_fit: 'contain', - }; - return Object.assign(Object.assign(Object.assign({}, defaults), config), { type: 'custom:bom-local-card' }); - } - setConfig(config) { - this._config = this._mergeWithDefaults(config); - this.requestUpdate(); - } - render() { - var _a, _b, _c, _d, _e, _f, _g; - if (!this.hass) - return x ``; - return x ` +function t(t,e,a,i){var o,r=arguments.length,s=r<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,a):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,a,i);else for(var n=t.length-1;n>=0;n--)(o=t[n])&&(s=(r<3?o(s):r>3?o(e,a,s):o(e,a))||s);return r>3&&s&&Object.defineProperty(e,a,s),s}"function"==typeof SuppressedError&&SuppressedError;const e=globalThis,a=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),o=new WeakMap;let r=class{constructor(t,e,a){if(this._$cssResult$=!0,a!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(a&&void 0===t){const a=void 0!==e&&1===e.length;a&&(t=o.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),a&&o.set(e,t))}return t}toString(){return this.cssText}};const s=(t,...e)=>{const a=1===t.length?t[0]:e.reduce((e,a,i)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(a)+t[i+1],t[0]);return new r(a,t,i)},n=a?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const a of t.cssRules)e+=a.cssText;return(t=>new r("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:l,defineProperty:c,getOwnPropertyDescriptor:d,getOwnPropertyNames:h,getOwnPropertySymbols:p,getPrototypeOf:m}=Object,u=globalThis,g=u.trustedTypes,f=g?g.emptyScript:"",v=u.reactiveElementPolyfillSupport,b=(t,e)=>t,_={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let a=t;switch(e){case Boolean:a=null!==t;break;case Number:a=null===t?null:Number(t);break;case Object:case Array:try{a=JSON.parse(t)}catch(t){a=null}}return a}},y=(t,e)=>!l(t,e),x={attribute:!0,type:String,converter:_,reflect:!1,useDefault:!1,hasChanged:y};Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=x){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const a=Symbol(),i=this.getPropertyDescriptor(t,a,e);void 0!==i&&c(this.prototype,t,i)}}static getPropertyDescriptor(t,e,a){const{get:i,set:o}=d(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:i,set(e){const r=i?.call(this);o?.call(this,e),this.requestUpdate(t,r,a)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??x}static _$Ei(){if(this.hasOwnProperty(b("elementProperties")))return;const t=m(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(b("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(b("properties"))){const t=this.properties,e=[...h(t),...p(t)];for(const a of e)this.createProperty(a,t[a])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,a]of e)this.elementProperties.set(t,a)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const a=this._$Eu(t,e);void 0!==a&&this._$Eh.set(a,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const a=new Set(t.flat(1/0).reverse());for(const t of a)e.unshift(n(t))}else void 0!==t&&e.push(n(t));return e}static _$Eu(t,e){const a=e.attribute;return!1===a?void 0:"string"==typeof a?a:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const a of e.keys())this.hasOwnProperty(a)&&(t.set(a,this[a]),delete this[a]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,i)=>{if(a)t.adoptedStyleSheets=i.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const a of i){const i=document.createElement("style"),o=e.litNonce;void 0!==o&&i.setAttribute("nonce",o),i.textContent=a.cssText,t.appendChild(i)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,a){this._$AK(t,a)}_$ET(t,e){const a=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,a);if(void 0!==i&&!0===a.reflect){const o=(void 0!==a.converter?.toAttribute?a.converter:_).toAttribute(e,a.type);this._$Em=t,null==o?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(t,e){const a=this.constructor,i=a._$Eh.get(t);if(void 0!==i&&this._$Em!==i){const t=a.getPropertyOptions(i),o="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:_;this._$Em=i;const r=o.fromAttribute(e,t.type);this[i]=r??this._$Ej?.get(i)??r,this._$Em=null}}requestUpdate(t,e,a){if(void 0!==t){const i=this.constructor,o=this[t];if(a??=i.getPropertyOptions(t),!((a.hasChanged??y)(o,e)||a.useDefault&&a.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(i._$Eu(t,a))))return;this.C(t,e,a)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:a,reflect:i,wrapped:o},r){a&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??e??this[t]),!0!==o||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||a||(e=void 0),this._$AL.set(t,e)),!0===i&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,a]of t){const{wrapped:t}=a,i=this[e];!0!==t||this._$AL.has(e)||void 0===i||this.C(e,void 0,a,i)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[b("elementProperties")]=new Map,w[b("finalized")]=new Map,v?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.1");const $=globalThis,C=$.trustedTypes,A=C?C.createPolicy("lit-html",{createHTML:t=>t}):void 0,E="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+k,S=`<${T}>`,R=document,F=()=>R.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,O=Array.isArray,P="[ \t\n\f\r]",D=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,I=/-->/g,U=/>/g,N=RegExp(`>|${P}(?:([^\\s"'>=/]+)(${P}*=${P}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),z=/'/g,j=/"/g,L=/^(?:script|style|textarea|title)$/i,H=(t=>(e,...a)=>({_$litType$:t,strings:e,values:a}))(1),W=Symbol.for("lit-noChange"),Z=Symbol.for("lit-nothing"),q=new WeakMap,B=R.createTreeWalker(R,129);function V(t,e){if(!O(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==A?A.createHTML(e):e}const J=(t,e)=>{const a=t.length-1,i=[];let o,r=2===e?"":3===e?"":"",s=D;for(let e=0;e"===l[0]?(s=o??D,c=-1):void 0===l[1]?c=-2:(c=s.lastIndex-l[2].length,n=l[1],s=void 0===l[3]?N:'"'===l[3]?j:z):s===j||s===z?s=N:s===I||s===U?s=D:(s=N,o=void 0);const h=s===N&&t[e+1].startsWith("/>")?" ":"";r+=s===D?a+S:c>=0?(i.push(n),a.slice(0,c)+E+a.slice(c)+k+h):a+k+(-2===c?e:h)}return[V(t,r+(t[a]||"")+(2===e?"":3===e?"":"")),i]};class Y{constructor({strings:t,_$litType$:e},a){let i;this.parts=[];let o=0,r=0;const s=t.length-1,n=this.parts,[l,c]=J(t,e);if(this.el=Y.createElement(l,a),B.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(i=B.nextNode())&&n.length0){i.textContent=C?C.emptyScript:"";for(let a=0;aO(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==Z&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(R.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:a}=t,i="number"==typeof a?this._$AC(t):(void 0===a.el&&(a.el=Y.createElement(V(a.h,a.h[0]),this.options)),a);if(this._$AH?._$AD===i)this._$AH.p(e);else{const t=new K(i,this),a=t.u(this.options);t.p(e),this.T(a),this._$AH=t}}_$AC(t){let e=q.get(t.strings);return void 0===e&&q.set(t.strings,e=new Y(t)),e}k(t){O(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let a,i=0;for(const o of t)i===e.length?e.push(a=new Q(this.O(F()),this.O(F()),this,this.options)):a=e[i],a._$AI(o),i++;i2||""!==a[0]||""!==a[1]?(this._$AH=Array(a.length-1).fill(new String),this.strings=a):this._$AH=Z}_$AI(t,e=this,a,i){const o=this.strings;let r=!1;if(void 0===o)t=G(this,t,e,0),r=!M(t)||t!==this._$AH&&t!==W,r&&(this._$AH=t);else{const i=t;let s,n;for(t=o[0],s=0;s{const i=a?.renderBefore??e;let o=i._$litPart$;if(void 0===o){const t=a?.renderBefore??null;i._$litPart$=o=new Q(e.insertBefore(F(),t),t,void 0,a??{})}return o._$AI(t),o})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return W}}st._$litElement$=!0,st.finalized=!0,rt.litElementHydrateSupport?.({LitElement:st});const nt=rt.litElementPolyfillSupport;nt?.({LitElement:st}),(rt.litElementVersions??=[]).push("4.2.1");const lt=t=>(e,a)=>{void 0!==a?a.addInitializer(()=>{customElements.define(t,e)}):customElements.define(t,e)},ct={attribute:!0,type:String,converter:_,reflect:!1,hasChanged:y},dt=(t=ct,e,a)=>{const{kind:i,metadata:o}=a;let r=globalThis.litPropertyMetadata.get(o);if(void 0===r&&globalThis.litPropertyMetadata.set(o,r=new Map),"setter"===i&&((t=Object.create(t)).wrapped=!0),r.set(a.name,t),"accessor"===i){const{name:i}=a;return{set(a){const o=e.get.call(this);e.set.call(this,a),this.requestUpdate(i,o,t)},init(e){return void 0!==e&&this.C(i,void 0,t,e),e}}}if("setter"===i){const{name:i}=a;return function(a){const o=this[i];e.call(this,a),this.requestUpdate(i,o,t)}}throw Error("Unsupported decorator location: "+i)};function ht(t){return(e,a)=>"object"==typeof a?dt(t,e,a):((t,e,a)=>{const i=e.hasOwnProperty(a);return e.constructor.createProperty(a,t),i?Object.getOwnPropertyDescriptor(e,a):void 0})(t,e,a)}function pt(t){return ht({...t,state:!0,attribute:!1})}var mt,ut;!function(t){t.language="language",t.system="system",t.comma_decimal="comma_decimal",t.decimal_comma="decimal_comma",t.space_comma="space_comma",t.none="none"}(mt||(mt={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(ut||(ut={}));var gt=function(t,e,a,i){i=i||{},a=null==a?{}:a;var o=new Event(e,{bubbles:void 0===i.bubbles||i.bubbles,cancelable:Boolean(i.cancelable),composed:void 0===i.composed||i.composed});return o.detail=a,t.dispatchEvent(o),o};const ft=[{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"}];let vt=class extends st{constructor(){super(...arguments),this._config=this._mergeWithDefaults(),this._metadataExpanded=!1,this._controlsExpanded=!1}_mergeWithDefaults(t={}){return Object.assign(Object.assign(Object.assign({},{service_url:"http://localhost:8082",timespan:"latest",frame_interval:2,refresh_interval:30,auto_play:!0,show_card_title:!0,show_metadata:!0,show_controls:!0,image_zoom:1,image_fit:"contain"}),t),{type:"custom:bom-local-card"})}setConfig(t){this._config=this._mergeWithDefaults(t),this.requestUpdate()}render(){var t,e,a,i,o,r,s;return this.hass?H`

Location Configuration

this._updateConfig('suburb', e.target.value)} + .value=${this._config.suburb||""} + @input=${t=>this._updateConfig("suburb",t.target.value)} required >
@@ -145,21 +27,18 @@ let BomLocalRadarCardEditor = class BomLocalRadarCardEditor extends i {
{ - const checked = e.target.checked; - this._updateConfig('show_card_title', checked); - }} + .checked=${!1!==this._config.show_card_title} + @change=${t=>{const e=t.target.checked;this._updateConfig("show_card_title",e)}} >
- ${this._config.show_card_title !== false ? x ` + ${!1!==this._config.show_card_title?H` this._updateConfig('card_title', e.target.value)} + .value=${this._config.card_title||""} + @input=${t=>this._updateConfig("card_title",t.target.value)} helper="Leave empty to use default" > - ` : ''} + `:""} - ${this._getMetadataEnabled() && this._metadataExpanded ? x ` + ${this._getMetadataEnabled()&&this._metadataExpanded?H` - ` : ''} + `:""}
@@ -328,100 +174,74 @@ let BomLocalRadarCardEditor = class BomLocalRadarCardEditor extends i { { - const checked = e.target.checked; - this._updateControlsToggle(checked); - }} + @change=${t=>{const e=t.target.checked;this._updateControlsToggle(e)}} >
- ${this._getControlsEnabled() ? x ` + ${this._getControlsEnabled()?H` { this._controlsExpanded = !this._controlsExpanded; }} + .label=${this._controlsExpanded?"Collapse":"Expand"} + @click=${()=>{this._controlsExpanded=!this._controlsExpanded}} > - + - ` : ''} + `:""} - ${this._getControlsEnabled() && this._controlsExpanded ? x ` + ${this._getControlsEnabled()&&this._controlsExpanded?H`
this._updateControlsConfig('show_play_pause', e.target.checked)} + .checked=${this._getControlsConfig("show_play_pause")} + @change=${t=>this._updateControlsConfig("show_play_pause",t.target.checked)} >
this._updateControlsConfig('show_prev_next', e.target.checked)} + .checked=${this._getControlsConfig("show_prev_next")} + @change=${t=>this._updateControlsConfig("show_prev_next",t.target.checked)} >
this._updateControlsConfig('show_slider', e.target.checked)} + .checked=${this._getControlsConfig("show_slider")} + @change=${t=>this._updateControlsConfig("show_slider",t.target.checked)} >
this._updateControlsConfig('show_nav_buttons', e.target.checked)} + .checked=${this._getControlsConfig("show_nav_buttons")} + @change=${t=>this._updateControlsConfig("show_nav_buttons",t.target.checked)} >
this._updateControlsConfig('show_frame_info', e.target.checked)} + .checked=${this._getControlsConfig("show_frame_info")} + @change=${t=>this._updateControlsConfig("show_frame_info",t.target.checked)} >
- ${this._getControlsConfig('position') === 'overlay' ? x ` + ${"overlay"===this._getControlsConfig("position")?H`
{ - const select = e.target; - this._updateConfig('image_fit', select.value); - }} + .value=${this._config.image_fit||"contain"} + @change=${t=>{const e=t.target;this._updateConfig("image_fit",e.value)}} > @@ -485,32 +299,17 @@ let BomLocalRadarCardEditor = class BomLocalRadarCardEditor extends i {
- ${this._cacheRange && this._cacheRange.totalCacheFolders > 0 ? x ` + ${this._cacheRange&&this._cacheRange.totalCacheFolders>0?H`
Available cache: ${this._formatCacheRange()}
- ` : this._cacheRangeError ? x ` + `:this._cacheRangeError?H`
⚠️ ${this._cacheRangeError}
- ` : ''} - ${this._getRangeWarning(this._config.timespan || 'latest') ? x ` + `:""} + ${this._getRangeWarning(this._config.timespan||"latest")?H`
⚠️ Selected timespan may not have enough data. Available: ${this._formatCacheRange()}
- ` : ''} - ${this._config.timespan === 'custom' ? x ` + `:""} + ${"custom"===this._config.timespan?H` this._updateConfig('custom_start_time', e.target.value)} + .value=${this._config.custom_start_time||""} + @input=${t=>this._updateConfig("custom_start_time",t.target.value)} helper="e.g., 2025-01-15T10:00:00Z" > this._updateConfig('custom_end_time', e.target.value)} + .value=${this._config.custom_end_time||""} + @input=${t=>this._updateConfig("custom_end_time",t.target.value)} helper="e.g., 2025-01-15T18:00:00Z" > - ${this._validateCustomRange() ? x ` + ${this._validateCustomRange()?H`
⚠️ ${this._validateCustomRange()}
- ` : ''} - ` : ''} + `:""} + `:""} this._updateConfig('frame_interval', parseFloat(e.target.value))} + .value=${String(this._config.frame_interval||2)} + @input=${t=>this._updateConfig("frame_interval",parseFloat(t.target.value))} >
{ - const checked = e.target.checked; - this._updateConfig('auto_play', checked); - }} + .checked=${!1!==this._config.auto_play} + @change=${t=>{const e=t.target.checked;this._updateConfig("auto_play",e)}} >
@@ -588,321 +384,12 @@ let BomLocalRadarCardEditor = class BomLocalRadarCardEditor extends i { min="10" max="300" step="10" - .value=${String(this._config.refresh_interval || 30)} - @input=${(e) => this._updateConfig('refresh_interval', parseInt(e.target.value))} + .value=${String(this._config.refresh_interval||30)} + @input=${t=>this._updateConfig("refresh_interval",parseInt(t.target.value))} > - `; - } - _updateConfig(key, value) { - const config = Object.assign(Object.assign({}, this._config), { [key]: value }); - this._config = config; - ne(this, 'config-changed', { config }); - } - // Metadata configuration helpers - _getMetadataEnabled() { - 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 - } - _updateMetadataToggle(enabled) { - 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); - } - } - _getMetadataConfig(key) { - const config = this._config.show_metadata; - if (typeof config === 'boolean') { - // For boolean config, return default values for position/style, true for others - if (key === 'position') - return 'above'; - if (key === 'style') - return 'cards'; - return config; - } - if (typeof config === 'object') { - const value = config[key]; - // For position/style, return the value or undefined (default) - if (key === 'position' || key === 'style') { - return value; - } - // For boolean fields, default to true if not explicitly false - return value !== false; - } - // Default values - if (key === 'position') - return 'above'; - if (key === 'style') - return 'cards'; - return true; - } - _updateMetadataConfig(key, value) { - let config; - if (typeof this._config.show_metadata === 'object') { - config = Object.assign({}, this._config.show_metadata); - } - else { - config = {}; - } - // Handle undefined to remove the key (revert to default) - if (value === undefined) { - delete config[key]; - } - else { - // Type-safe assignment based on key - if (key === 'position' || key === 'style') { - config[key] = value; - } - else { - config[key] = value; - } - } - // If config is empty, set to true (default) - if (Object.keys(config).length === 0) { - this._updateConfig('show_metadata', true); - } - else { - this._updateConfig('show_metadata', config); - } - } - // Controls configuration helpers - _getControlsEnabled() { - 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 - } - _updateControlsToggle(enabled) { - 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); - } - } - _getControlsConfig(key) { - const config = this._config.show_controls; - if (typeof config === 'boolean') { - // For boolean config, return default values for position, true for others - if (key === 'position') - return 'below'; - return config; - } - if (typeof config === 'object') { - const value = config[key]; - // For position, return the value or undefined (default) - if (key === 'position') { - return value; - } - // For boolean fields, default to true if not explicitly false - return value !== false; - } - // Default values - if (key === 'position') - return 'below'; - return true; - } - _updateControlsConfig(key, value) { - let config; - if (typeof this._config.show_controls === 'object') { - config = Object.assign({}, this._config.show_controls); - } - else { - config = {}; - } - // Handle undefined to remove the key (revert to default) - if (value === undefined) { - delete config[key]; - } - else { - // Type-safe assignment based on key - if (key === 'position') { - config[key] = value; - } - else { - config[key] = value; - } - } - // If config is empty, set to true (default) - if (Object.keys(config).length === 0) { - this._updateConfig('show_controls', true); - } - else { - this._updateConfig('show_controls', config); - } - } - /** - * Check if metadata and controls would overlap when both are overlays - */ - _checkOverlayConflict() { - var _a, _b, _c; - const metadataConfig = this._getMetadataConfig('position'); - const controlsConfig = this._getControlsConfig('position'); - // Both must be overlays to have a conflict - if (metadataConfig !== 'overlay' || controlsConfig !== 'overlay') { - return { hasConflict: false, message: '' }; - } - const metadataPos = (_a = this._config.metadata_overlay_position) !== null && _a !== void 0 ? _a : 'top'; - const controlsPos = (_b = this._config.controls_overlay_position) !== null && _b !== void 0 ? _b : 'bottom'; - // Check for conflicts - if (metadataPos === controlsPos) { - return { - hasConflict: true, - message: `⚠️ Warning: Both metadata and controls are overlayed at the same position (${metadataPos}). They will overlap.` - }; - } - // Check for adjacent positions that might conflict - const conflicts = { - 'top': ['top', 'center'], - 'bottom': ['bottom', 'center'], - 'left': ['left', 'center'], - 'right': ['right', 'center'], - 'center': ['top', 'bottom', 'left', 'right', 'center'] - }; - if ((_c = conflicts[metadataPos]) === null || _c === void 0 ? void 0 : _c.includes(controlsPos)) { - return { - hasConflict: true, - message: `⚠️ Warning: Metadata (${metadataPos}) and controls (${controlsPos}) may overlap. Consider using different positions.` - }; - } - return { hasConflict: false, message: '' }; - } - _getRangeWarning(timespan) { - if (!this._cacheRange || this._cacheRange.totalCacheFolders === 0 || timespan === 'latest' || timespan === 'custom') { - return false; - } - const hours = parseInt(timespan.replace('h', '')) || 0; - const requestedMinutes = hours * 60; - const availableMinutes = this._cacheRange.timeSpanMinutes || 0; - return requestedMinutes > availableMinutes; - } - _formatCacheRange() { - var _a, _b, _c, _d, _e, _f; - if (!this._cacheRange || this._cacheRange.totalCacheFolders === 0) { - return 'No cache available'; - } - const oldest = (_a = this._cacheRange.oldestCache) === null || _a === void 0 ? void 0 : _a.cacheTimestamp; - const newest = (_b = this._cacheRange.newestCache) === null || _b === void 0 ? void 0 : _b.cacheTimestamp; - const timeSpan = this._cacheRange.timeSpanMinutes; - if (oldest && newest) { - const oldestDate = new Date(oldest); - const newestDate = new Date(newest); - // Use local timezone for display - const locale = ((_d = (_c = this.hass) === null || _c === void 0 ? void 0 : _c.locale) === null || _d === void 0 ? void 0 : _d.language) || 'en-AU'; - const timeZone = ((_f = (_e = this.hass) === null || _e === void 0 ? void 0 : _e.config) === null || _f === void 0 ? void 0 : _f.time_zone) || Intl.DateTimeFormat().resolvedOptions().timeZone; - const oldestStr = oldestDate.toLocaleString(locale, { - timeZone: timeZone, - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - const newestStr = newestDate.toLocaleString(locale, { - timeZone: timeZone, - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - if (timeSpan) { - const hours = Math.floor(timeSpan / 60); - const minutes = timeSpan % 60; - return `${oldestStr} to ${newestStr} (${hours}h ${minutes}m, ${this._cacheRange.totalCacheFolders} folders)`; - } - return `${oldestStr} to ${newestStr} (${this._cacheRange.totalCacheFolders} folders)`; - } - return `${this._cacheRange.totalCacheFolders} cache folders available`; - } - _validateCustomRange() { - var _a, _b, _c, _d, _e, _f, _g, _h; - // Only validate if timespan is custom - if (this._config.timespan !== 'custom') { - return null; - } - if (!this._config.custom_start_time || !this._config.custom_end_time) { - return null; // No validation needed if fields are empty - } - try { - const startTime = new Date(this._config.custom_start_time); - const endTime = new Date(this._config.custom_end_time); - if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) { - return 'Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T10:00:00Z)'; - } - if (startTime >= endTime) { - return 'Start time must be before end time'; - } - if (this._cacheRange && this._cacheRange.oldestCache && this._cacheRange.newestCache) { - try { - const oldestCache = new Date(this._cacheRange.oldestCache.cacheTimestamp); - const newestCache = new Date(this._cacheRange.newestCache.cacheTimestamp); - if (isNaN(oldestCache.getTime()) || isNaN(newestCache.getTime())) { - return null; // Can't validate if cache dates are invalid - } - if (startTime < oldestCache) { - const locale = ((_b = (_a = this.hass) === null || _a === void 0 ? void 0 : _a.locale) === null || _b === void 0 ? void 0 : _b.language) || 'en-AU'; - const timeZone = ((_d = (_c = this.hass) === null || _c === void 0 ? void 0 : _c.config) === null || _d === void 0 ? void 0 : _d.time_zone) || Intl.DateTimeFormat().resolvedOptions().timeZone; - const oldestStr = oldestCache.toLocaleString(locale, { - timeZone: timeZone, - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - return `Start time is before available cache (${oldestStr})`; - } - if (endTime > newestCache) { - const locale = ((_f = (_e = this.hass) === null || _e === void 0 ? void 0 : _e.locale) === null || _f === void 0 ? void 0 : _f.language) || 'en-AU'; - const timeZone = ((_h = (_g = this.hass) === null || _g === void 0 ? void 0 : _g.config) === null || _h === void 0 ? void 0 : _h.time_zone) || Intl.DateTimeFormat().resolvedOptions().timeZone; - const newestStr = newestCache.toLocaleString(locale, { - timeZone: timeZone, - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - return `End time is after available cache (${newestStr})`; - } - } - catch (err) { - // Ignore cache date parsing errors - return null; - } - } - return null; // Valid - } - catch (err) { - return 'Invalid date format'; - } - } -}; -BomLocalRadarCardEditor.styles = i$3 ` + `:H``}_updateConfig(t,e){const a=Object.assign(Object.assign({},this._config),{[t]:e});this._config=a,gt(this,"config-changed",{config:a})}_getMetadataEnabled(){const t=this._config.show_metadata;return void 0===t||!0===t||("boolean"!=typeof t||t)}_updateMetadataToggle(t){if(t){if("object"==typeof this._config.show_metadata)return;this._updateConfig("show_metadata",{})}else this._updateConfig("show_metadata",!1)}_getMetadataConfig(t){const e=this._config.show_metadata;if("boolean"==typeof e)return"position"===t?"above":"style"===t?"cards":e;if("object"==typeof e){const a=e[t];return"position"===t||"style"===t?a:!1!==a}return"position"===t?"above":"style"!==t||"cards"}_updateMetadataConfig(t,e){let a;a="object"==typeof this._config.show_metadata?Object.assign({},this._config.show_metadata):{},void 0===e?delete a[t]:a[t]=e,0===Object.keys(a).length?this._updateConfig("show_metadata",!0):this._updateConfig("show_metadata",a)}_getControlsEnabled(){const t=this._config.show_controls;return void 0===t||!0===t||("boolean"!=typeof t||t)}_updateControlsToggle(t){if(t){if("object"==typeof this._config.show_controls)return;this._updateConfig("show_controls",{})}else this._updateConfig("show_controls",!1)}_getControlsConfig(t){const e=this._config.show_controls;if("boolean"==typeof e)return"position"===t?"below":e;if("object"==typeof e){const a=e[t];return"position"===t?a:!1!==a}return"position"!==t||"below"}_updateControlsConfig(t,e){let a;a="object"==typeof this._config.show_controls?Object.assign({},this._config.show_controls):{},void 0===e?delete a[t]:a[t]=e,0===Object.keys(a).length?this._updateConfig("show_controls",!0):this._updateConfig("show_controls",a)}_checkOverlayConflict(){var t,e,a;const i=this._getMetadataConfig("position"),o=this._getControlsConfig("position");if("overlay"!==i||"overlay"!==o)return{hasConflict:!1,message:""};const r=null!==(t=this._config.metadata_overlay_position)&&void 0!==t?t:"top",s=null!==(e=this._config.controls_overlay_position)&&void 0!==e?e:"bottom";if(r===s)return{hasConflict:!0,message:`⚠️ Warning: Both metadata and controls are overlayed at the same position (${r}). They will overlap.`};return(null===(a={top:["top","center"],bottom:["bottom","center"],left:["left","center"],right:["right","center"],center:["top","bottom","left","right","center"]}[r])||void 0===a?void 0:a.includes(s))?{hasConflict:!0,message:`⚠️ Warning: Metadata (${r}) and controls (${s}) may overlap. Consider using different positions.`}:{hasConflict:!1,message:""}}_getRangeWarning(t){if(!this._cacheRange||0===this._cacheRange.totalCacheFolders||"latest"===t||"custom"===t)return!1;return 60*(parseInt(t.replace("h",""))||0)>(this._cacheRange.timeSpanMinutes||0)}_formatCacheRange(){var t,e,a,i,o,r;if(!this._cacheRange||0===this._cacheRange.totalCacheFolders)return"No cache available";const s=null===(t=this._cacheRange.oldestCache)||void 0===t?void 0:t.cacheTimestamp,n=null===(e=this._cacheRange.newestCache)||void 0===e?void 0:e.cacheTimestamp,l=this._cacheRange.timeSpanMinutes;if(s&&n){const t=new Date(s),e=new Date(n),c=(null===(i=null===(a=this.hass)||void 0===a?void 0:a.locale)||void 0===i?void 0:i.language)||"en-AU",d=(null===(r=null===(o=this.hass)||void 0===o?void 0:o.config)||void 0===r?void 0:r.time_zone)||Intl.DateTimeFormat().resolvedOptions().timeZone,h=t.toLocaleString(c,{timeZone:d,year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),p=e.toLocaleString(c,{timeZone:d,year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});if(l){return`${h} to ${p} (${Math.floor(l/60)}h ${l%60}m, ${this._cacheRange.totalCacheFolders} folders)`}return`${h} to ${p} (${this._cacheRange.totalCacheFolders} folders)`}return`${this._cacheRange.totalCacheFolders} cache folders available`}_validateCustomRange(){var t,e,a,i,o,r,s,n;if("custom"!==this._config.timespan)return null;if(!this._config.custom_start_time||!this._config.custom_end_time)return null;try{const l=new Date(this._config.custom_start_time),c=new Date(this._config.custom_end_time);if(isNaN(l.getTime())||isNaN(c.getTime()))return"Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T10:00:00Z)";if(l>=c)return"Start time must be before end time";if(this._cacheRange&&this._cacheRange.oldestCache&&this._cacheRange.newestCache)try{const d=new Date(this._cacheRange.oldestCache.cacheTimestamp),h=new Date(this._cacheRange.newestCache.cacheTimestamp);if(isNaN(d.getTime())||isNaN(h.getTime()))return null;if(lh){const t=(null===(r=null===(o=this.hass)||void 0===o?void 0:o.locale)||void 0===r?void 0:r.language)||"en-AU",e=(null===(n=null===(s=this.hass)||void 0===s?void 0:s.config)||void 0===n?void 0:n.time_zone)||Intl.DateTimeFormat().resolvedOptions().timeZone;return`End time is after available cache (${h.toLocaleString(t,{timeZone:e,year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})})`}}catch(t){return null}return null}catch(t){return"Invalid date format"}}};vt.styles=s` .editor { padding: 16px; display: flex; @@ -1050,289 +537,7 @@ BomLocalRadarCardEditor.styles = i$3 ` font-size: 0.875em; color: var(--primary-text-color, #212121); } - `; -__decorate([ - n({ attribute: false }) -], BomLocalRadarCardEditor.prototype, "hass", void 0); -__decorate([ - r$1() -], BomLocalRadarCardEditor.prototype, "_config", void 0); -__decorate([ - r$1() -], BomLocalRadarCardEditor.prototype, "_metadataExpanded", void 0); -__decorate([ - r$1() -], BomLocalRadarCardEditor.prototype, "_controlsExpanded", void 0); -__decorate([ - r$1() -], BomLocalRadarCardEditor.prototype, "_cacheRange", void 0); -__decorate([ - r$1() -], BomLocalRadarCardEditor.prototype, "_cacheRangeError", void 0); -BomLocalRadarCardEditor = __decorate([ - t$1('bom-local-card-editor') -], BomLocalRadarCardEditor); - -const CARD_VERSION = '0.3.1'; -const DEFAULT_FRAME_INTERVAL = 2.0; // seconds -const DEFAULT_RESTART_DELAY = 2000; // ms (pause before looping) -const DEFAULT_REFRESH_INTERVAL = 30; // seconds - -/** - * Parses API error response and creates ErrorState - * Consolidates duplicated error handling logic from fetchRadarData and fetchHistoricalRadar - */ -function parseApiError(response, errorData, parsedJson, options) { - // Extract error information (support both camelCase and PascalCase) - const errorCode = errorData.errorCode || errorData.ErrorCode; - const message = errorData.message || errorData.Message || response.statusText || 'An error occurred'; - const errorType = errorData.errorType || errorData.ErrorType; - const details = errorData.details || errorData.Details || {}; - const suggestions = errorData.suggestions || errorData.Suggestions || {}; - // Build enhanced error message - let enhancedMessage = message; - // Enhance message for cache not found scenarios - if (errorCode === 'CACHE_NOT_FOUND' || (response.status === 404 && !errorCode)) { - if (!enhancedMessage.includes('cache') && !enhancedMessage.includes('Cache')) { - enhancedMessage = 'No cached data found for this location. Cache update has been triggered in background. Please retry in a few moments.'; - } - } - // Handle previous update failure - if (details.previousUpdateFailed) { - enhancedMessage = `${enhancedMessage}\n\nPrevious update failed: ${details.previousError || 'Unknown error'}`; - if (details.previousErrorCode) { - enhancedMessage += ` (${details.previousErrorCode})`; - } - } - // Handle time range errors with available/requested ranges - if (errorCode === 'TIME_RANGE_ERROR' && details.availableRange) { - const available = details.availableRange; - const requested = details.requestedRange; - enhancedMessage += `\n\nAvailable data: ${available.oldest || 'N/A'} to ${available.newest || 'N/A'}`; - if (available.totalCacheFolders) { - enhancedMessage += ` (${available.totalCacheFolders} cache folders)`; - } - if (requested) { - enhancedMessage += `\nRequested: ${requested.start || 'N/A'} to ${requested.end || 'N/A'}`; - } - if (details.requestedHours) { - enhancedMessage += `\nRequested range: ${details.requestedHours} hours (max: ${details.maxHours || 'N/A'} hours)`; - } - // Add service suggestion if available - if (suggestions.suggestion) { - enhancedMessage += `\n\n💡 ${suggestions.suggestion}`; - } - } - // Determine retry behavior - const retryAfter = suggestions.retryAfter || - details.retryAfter || - options.defaultRetryAfter || 30; - const action = suggestions.action || 'retry_after_seconds'; - const shouldAutoRetry = action !== 'manual_refresh_recommended' && - action !== 'check_network_and_retry' && - action !== 'adjust_time_range'; - // Map error type - const mappedType = mapErrorType(errorType || errorCode || (response.status === 404 ? 'CACHE_NOT_FOUND' : 'unknown')); - // Determine retryable status - for historical endpoint, also allow 400 - const isHistoricalEndpoint = response.url.includes('/timeseries'); - const retryable = response.status === 404 || (isHistoricalEndpoint && response.status === 400); - return { - message: enhancedMessage, - type: mappedType, - retryable: retryable, - retryAction: options.retryAction, - retryAfter: shouldAutoRetry ? retryAfter : undefined, - errorCode: errorCode || (response.status === 404 ? 'CACHE_NOT_FOUND' : `HTTP_${response.status}`), - details: Object.assign(Object.assign({}, details), { action: action, refreshEndpoint: suggestions.refreshEndpoint, statusEndpoint: suggestions.statusEndpoint, suggestedRange: suggestions.suggestedRange }), - }; -} -/** - * Maps API error type to card error type - */ -function mapErrorType(apiErrorType) { - if (!apiErrorType) - return 'unknown'; - const type = apiErrorType.toLowerCase(); - if (type.includes('cache') || type === 'cache_not_found' || type === 'cacheerror') - return 'cache'; - if (type.includes('validation') || type === 'validation_error' || type === 'validationerror') - return 'validation'; - if (type.includes('network') || type.includes('fetch')) - return 'network'; - if (type.includes('notfound') || type === 'not_found' || type === 'notfounderror') - return 'cache'; - return 'unknown'; -} - -/** - * Resolves a URL through the Home Assistant integration proxy if it's a relative path. - * The integration provides a proxy at /api/bom_local/proxy/ - */ -function resolveImageUrl(url, serviceUrl) { - if (!url) - return url; - // If URL is already absolute (starts with http:// or https://), use as-is - if (url.startsWith('http://') || url.startsWith('https://')) { - return url; - } - // If we are using the HA integration proxy (standard behavior now) - const proxyPrefix = '/api/bom_local/proxy'; - // Remove leading slash if present to avoid double slashes - const path = url.startsWith('/') ? url : `/${url}`; - return `${proxyPrefix}${path}`; -} - -class RadarApiService { - /** - * Fetches latest radar frames via HA WebSocket API - */ - async fetchLatestFrames(options) { - const { hass, suburb, state, onError } = options; - try { - const data = await hass.callWS({ - type: 'bom_local/get_radar_data', - suburb, - state, - }); - // Handle service-level errors (e.g. CACHE_NOT_FOUND) - if (data.errorCode || data.ErrorCode || data.error) { - const error = parseApiError({ ok: false, status: 404, statusText: 'Not Found', url: '/api/radar' }, data, true, { - retryAction: () => this.fetchLatestFrames(options), - defaultRetryAfter: 30, - }); - onError(error); - return null; - } - // Validate response has frames - if (!data.frames || data.frames.length === 0) { - throw new Error('No frames available in response'); - } - // Resolve relative image URLs through HA proxy - data.frames.forEach((frame) => { - if (frame.imageUrl) { - frame.imageUrl = resolveImageUrl(frame.imageUrl); - } - }); - return data; - } - catch (err) { - this.handleFetchError(err, options); - return null; - } - } - /** - * Fetches historical radar data via HA WebSocket API - */ - async fetchHistoricalFrames(options) { - var _a; - const { hass, suburb, state, timespan, customStartTime, customEndTime, onError } = options; - try { - let startTime = null; - let endTime = new Date().toISOString(); - if (timespan === 'custom') { - if (customStartTime) - startTime = new Date(customStartTime).toISOString(); - if (customEndTime) - endTime = new Date(customEndTime).toISOString(); - } - else if (timespan) { - const hours = parseInt(timespan.replace('h', '')) || 1; - startTime = new Date(Date.now() - (hours * 60 * 60 * 1000)).toISOString(); - } - if (!startTime) { - throw new Error('Invalid timespan configuration'); - } - const data = await hass.callWS({ - type: 'bom_local/get_radar_data', - suburb, - state, - startTime, - endTime, - }); - // Handle service-level errors (e.g. TIME_RANGE_ERROR or CACHE_NOT_FOUND) - if (data.errorCode || data.ErrorCode || data.error) { - const error = parseApiError({ ok: false, status: 404, statusText: 'Not Found', url: '/timeseries' }, data, true, { - retryAction: () => this.fetchHistoricalFrames(options), - defaultRetryAfter: 30, - }); - onError(error); - return null; - } - if (!data.cacheFolders || data.cacheFolders.length === 0) { - throw new Error('No historical data found for the specified time range.'); - } - // Flatten all frames from all cache folders - const allFrames = []; - data.cacheFolders.forEach(cacheFolder => { - cacheFolder.frames.forEach(frame => { - frame.cacheTimestamp = cacheFolder.cacheTimestamp; - frame.observationTime = cacheFolder.observationTime; - frame.cacheFolderName = cacheFolder.cacheFolderName; - if (frame.imageUrl) { - frame.imageUrl = resolveImageUrl(frame.imageUrl); - } - if (!frame.absoluteObservationTime && frame.observationTime && frame.minutesAgo !== undefined) { - const obsTime = new Date(frame.observationTime); - frame.absoluteObservationTime = new Date(obsTime.getTime() - (frame.minutesAgo * 60 * 1000)).toISOString(); - } - allFrames.push(frame); - }); - }); - // Re-index frames sequentially - allFrames.sort((a, b) => new Date(a.absoluteObservationTime).getTime() - new Date(b.absoluteObservationTime).getTime()); - allFrames.forEach((frame, idx) => { - frame.sequentialIndex = idx; - }); - // Fetch latest metadata for display - let metadata = {}; - try { - metadata = await hass.callWS({ - type: 'bom_local/get_radar_data', - suburb, - state - }); - } - catch (err) { - console.debug('Could not fetch metadata:', err); - } - const newestCacheFolder = data.cacheFolders[data.cacheFolders.length - 1]; - const radarResponse = { - frames: allFrames, - lastUpdated: endTime, - observationTime: metadata.observationTime || (newestCacheFolder === null || newestCacheFolder === void 0 ? void 0 : newestCacheFolder.observationTime) || endTime, - forecastTime: metadata.forecastTime || endTime, - weatherStation: metadata.weatherStation, - distance: metadata.distance, - cacheIsValid: (_a = metadata.cacheIsValid) !== null && _a !== void 0 ? _a : true, - cacheExpiresAt: metadata.cacheExpiresAt || endTime, - isUpdating: metadata.isUpdating || false, - nextUpdateTime: metadata.nextUpdateTime || endTime, - }; - return radarResponse; - } - catch (err) { - this.handleFetchError(err, options); - return null; - } - } - /** - * Handles fetch errors and categorizes them appropriately - */ - handleFetchError(err, options) { - const message = (err === null || err === void 0 ? void 0 : err.message) || (typeof err === 'string' ? err : 'Unknown error occurred'); - options.onError({ - message: message, - type: 'unknown', - retryable: true, - retryAction: () => this.fetchLatestFrames(options), - }); - } -} - -/** - * Base card styles including CSS variables and root layout - */ -const cardStyles = i$3 ` + `,t([ht({attribute:!1})],vt.prototype,"hass",void 0),t([pt()],vt.prototype,"_config",void 0),t([pt()],vt.prototype,"_metadataExpanded",void 0),t([pt()],vt.prototype,"_controlsExpanded",void 0),t([pt()],vt.prototype,"_cacheRange",void 0),t([pt()],vt.prototype,"_cacheRangeError",void 0),vt=t([lt("bom-local-card-editor")],vt);function bt(t,e,a,i){const o=e.errorCode||e.ErrorCode,r=e.message||e.Message||t.statusText||"An error occurred",s=e.errorType||e.ErrorType,n=e.details||e.Details||{},l=e.suggestions||e.Suggestions||{};let c=r;if(("CACHE_NOT_FOUND"===o||404===t.status&&!o)&&(c.includes("cache")||c.includes("Cache")||(c="No cached data found for this location. Cache update has been triggered in background. Please retry in a few moments.")),n.previousUpdateFailed&&(c=`${c}\n\nPrevious update failed: ${n.previousError||"Unknown error"}`,n.previousErrorCode&&(c+=` (${n.previousErrorCode})`)),"TIME_RANGE_ERROR"===o&&n.availableRange){const t=n.availableRange,e=n.requestedRange;c+=`\n\nAvailable data: ${t.oldest||"N/A"} to ${t.newest||"N/A"}`,t.totalCacheFolders&&(c+=` (${t.totalCacheFolders} cache folders)`),e&&(c+=`\nRequested: ${e.start||"N/A"} to ${e.end||"N/A"}`),n.requestedHours&&(c+=`\nRequested range: ${n.requestedHours} hours (max: ${n.maxHours||"N/A"} hours)`),l.suggestion&&(c+=`\n\n💡 ${l.suggestion}`)}const d=l.retryAfter||n.retryAfter||i.defaultRetryAfter||30,h=l.action||"retry_after_seconds",p="manual_refresh_recommended"!==h&&"check_network_and_retry"!==h&&"adjust_time_range"!==h,m=function(t){const e=t.toLowerCase();return e.includes("cache")||"cache_not_found"===e||"cacheerror"===e?"cache":e.includes("validation")||"validation_error"===e||"validationerror"===e?"validation":e.includes("network")||e.includes("fetch")?"network":e.includes("notfound")||"not_found"===e||"notfounderror"===e?"cache":"unknown"}(s||o||(404===t.status?"CACHE_NOT_FOUND":"unknown")),u=t.url.includes("/timeseries");return{message:c,type:m,retryable:404===t.status||u&&400===t.status,retryAction:i.retryAction,retryAfter:p?d:void 0,errorCode:o||(404===t.status?"CACHE_NOT_FOUND":`HTTP_${t.status}`),details:Object.assign(Object.assign({},n),{action:h,refreshEndpoint:l.refreshEndpoint,statusEndpoint:l.statusEndpoint,suggestedRange:l.suggestedRange})}}function _t(t,e){if(!t)return t;if(t.startsWith("http://")||t.startsWith("https://"))return t;return`/api/bom_local/proxy${t.startsWith("/")?t:`/${t}`}`}class yt{async fetchLatestFrames(t){const{hass:e,suburb:a,state:i,onError:o}=t;try{const r=await e.callWS({type:"bom_local/get_radar_data",suburb:a,state:i});if(r.errorCode||r.ErrorCode||r.error){return o(bt({ok:!1,status:404,statusText:"Not Found",url:"/api/radar"},r,0,{retryAction:()=>this.fetchLatestFrames(t),defaultRetryAfter:30})),null}if(!r.frames||0===r.frames.length)throw new Error("No frames available in response");return r.frames.forEach(t=>{t.imageUrl&&(t.imageUrl=_t(t.imageUrl))}),r}catch(e){return this.handleFetchError(e,t),null}}async fetchHistoricalFrames(t){var e;const{hass:a,suburb:i,state:o,timespan:r,customStartTime:s,customEndTime:n,onError:l}=t;try{let c=null,d=(new Date).toISOString();if("custom"===r)s&&(c=new Date(s).toISOString()),n&&(d=new Date(n).toISOString());else if(r){const t=parseInt(r.replace("h",""))||1;c=new Date(Date.now()-60*t*60*1e3).toISOString()}if(!c)throw new Error("Invalid timespan configuration");const h=await a.callWS({type:"bom_local/get_radar_data",suburb:i,state:o,startTime:c,endTime:d});if(h.errorCode||h.ErrorCode||h.error){return l(bt({ok:!1,status:404,statusText:"Not Found",url:"/timeseries"},h,0,{retryAction:()=>this.fetchHistoricalFrames(t),defaultRetryAfter:30})),null}if(!h.cacheFolders||0===h.cacheFolders.length)throw new Error("No historical data found for the specified time range.");const p=[];h.cacheFolders.forEach(t=>{t.frames.forEach(e=>{if(e.cacheTimestamp=t.cacheTimestamp,e.observationTime=t.observationTime,e.cacheFolderName=t.cacheFolderName,e.imageUrl&&(e.imageUrl=_t(e.imageUrl)),!e.absoluteObservationTime&&e.observationTime&&void 0!==e.minutesAgo){const t=new Date(e.observationTime);e.absoluteObservationTime=new Date(t.getTime()-60*e.minutesAgo*1e3).toISOString()}p.push(e)})}),p.sort((t,e)=>new Date(t.absoluteObservationTime).getTime()-new Date(e.absoluteObservationTime).getTime()),p.forEach((t,e)=>{t.sequentialIndex=e});let m={};try{m=await a.callWS({type:"bom_local/get_radar_data",suburb:i,state:o})}catch(t){console.debug("Could not fetch metadata:",t)}const u=h.cacheFolders[h.cacheFolders.length-1];return{frames:p,lastUpdated:d,observationTime:m.observationTime||(null==u?void 0:u.observationTime)||d,forecastTime:m.forecastTime||d,weatherStation:m.weatherStation,distance:m.distance,cacheIsValid:null===(e=m.cacheIsValid)||void 0===e||e,cacheExpiresAt:m.cacheExpiresAt||d,isUpdating:m.isUpdating||!1,nextUpdateTime:m.nextUpdateTime||d}}catch(e){return this.handleFetchError(e,t),null}}handleFetchError(t,e){const a=(null==t?void 0:t.message)||("string"==typeof t?t:"Unknown error occurred");e.onError({message:a,type:"unknown",retryable:!0,retryAction:()=>this.fetchLatestFrames(e)})}}const xt=s` :host { --bom-primary-color: var(--primary-color, #667eea); --bom-secondary-color: var(--secondary-text-color, #666); @@ -1345,12 +550,7 @@ const cardStyles = i$3 ` width: 100%; position: relative; } -`; - -/** - * Styles for radar image container and loading states - */ -const imageStyles = i$3 ` +`,wt=s` /* Radar Image Container */ .radar-image-container { position: relative; @@ -1444,12 +644,7 @@ const imageStyles = i$3 ` font-size: 0.9em; opacity: 0.9; } -`; - -/** - * Styles for frame slider, navigation buttons, and play controls - */ -const controlsStyles = i$3 ` +`,$t=s` .frame-slider-container { width: 100%; padding: 15px; @@ -1654,12 +849,7 @@ const controlsStyles = i$3 ` order: 0; } } -`; - -/** - * Styles for metadata display sections - */ -const metadataStyles = i$3 ` +`,Ct=s` /* Metadata Section */ .metadata-section { margin-bottom: 15px; @@ -1798,12 +988,7 @@ const metadataStyles = i$3 ` font-weight: 600; color: var(--bom-text-color); } -`; - -/** - * Styles for overlay controls and metadata overlays - */ -const overlayStyles = i$3 ` +`,At=s` /* Overlay Controls */ .controls-overlay { position: absolute; @@ -1901,12 +1086,7 @@ const overlayStyles = i$3 ` left: 50%; transform: translate(-50%, -50%); } -`; - -/** - * Styles for error display components - */ -const errorStyles = i$3 ` +`,Et=s` /* Error Display */ .error-container { padding: 16px; @@ -2172,1289 +1352,333 @@ const errorStyles = i$3 ` .retry-button span { color: white; } -`; - -/** - * Formats timestamp for display - */ -function formatTimestamp(isoString, locale = 'en-AU', timeZone) { - if (!isoString) - return '-'; - const date = new Date(isoString); - const tz = timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone; - return date.toLocaleString(locale, { - timeZone: tz, - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} -/** - * Relative time (e.g., "5 min ago", "2 hours ago") - */ -function formatRelativeTime(isoString) { - if (!isoString) - return '-'; - const date = new Date(isoString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - if (diffMins < 1) - return 'Just now'; - if (diffMins < 60) - return `${diffMins} min ago`; - if (diffHours < 24) - return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; - return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; -} - -let BomErrorDisplay = class BomErrorDisplay extends i { - constructor() { - super(...arguments); - this.locale = 'en-AU'; - } - _getErrorIcon() { - if (!this.error) - return 'mdi:alert'; - switch (this.error.type) { - case 'cache': - return 'mdi:database-alert'; - case 'validation': - return 'mdi:alert-circle'; - case 'network': - return 'mdi:wifi-off'; - default: - return 'mdi:alert'; - } - } - _getErrorColor() { - if (!this.error) - return '#f44336'; - switch (this.error.type) { - case 'cache': - return '#ff9800'; - case 'validation': - return '#f44336'; - case 'network': - return '#2196f3'; - default: - return '#f44336'; - } - } - _getErrorTitle() { - if (!this.error) - return 'Error'; - switch (this.error.type) { - case 'cache': - return 'Cache Error'; - case 'validation': - return 'Validation Error'; - case 'network': - return 'Network Error'; - default: - return 'Error'; - } - } - render() { - var _a, _b, _c, _d, _e, _f; - if (!this.error) { - return x ``; - } - const action = (_a = this.error.details) === null || _a === void 0 ? void 0 : _a.action; - const isManualRefreshRecommended = action === 'manual_refresh_recommended'; - const refreshEndpoint = (_b = this.error.details) === null || _b === void 0 ? void 0 : _b.refreshEndpoint; - const retryInfo = this.error.retryAfter - ? x `
Auto-retrying in ${this.error.retryAfter} seconds...
` - : isManualRefreshRecommended - ? x `
Manual refresh recommended. Previous update failed.
` - : ''; - // Format error message with line breaks (preserve empty lines) - const errorLines = this.error.message.split('\n'); - const errorMessage = errorLines.map((line, index) => line.trim() - ? x `
${line}
` - : x `
`); - // Show additional details if available - const showDetails = this.error.details && (this.error.details.previousUpdateFailed || - this.error.details.availableRange || - this.error.details.requestedRange || - this.error.details.suggestedRange || - refreshEndpoint); - // Determine icon and color based on error type - const errorIcon = this._getErrorIcon(); - const errorColor = this._getErrorColor(); - return x ` +`;function kt(t,e="en-AU",a){if(!t)return"-";const i=new Date(t),o=a||Intl.DateTimeFormat().resolvedOptions().timeZone;return i.toLocaleString(e,{timeZone:o,year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}let Tt=class extends st{constructor(){super(...arguments),this.locale="en-AU"}_getErrorIcon(){if(!this.error)return"mdi:alert";switch(this.error.type){case"cache":return"mdi:database-alert";case"validation":return"mdi:alert-circle";case"network":return"mdi:wifi-off";default:return"mdi:alert"}}_getErrorColor(){if(!this.error)return"#f44336";switch(this.error.type){case"cache":return"#ff9800";case"validation":default:return"#f44336";case"network":return"#2196f3"}}_getErrorTitle(){if(!this.error)return"Error";switch(this.error.type){case"cache":return"Cache Error";case"validation":return"Validation Error";case"network":return"Network Error";default:return"Error"}}render(){var t,e,a,i,o,r;if(!this.error)return H``;const s="manual_refresh_recommended"===(null===(t=this.error.details)||void 0===t?void 0:t.action),n=null===(e=this.error.details)||void 0===e?void 0:e.refreshEndpoint,l=this.error.retryAfter?H`
Auto-retrying in ${this.error.retryAfter} seconds...
`:s?H`
Manual refresh recommended. Previous update failed.
`:"",c=this.error.message.split("\n").map((t,e)=>t.trim()?H`
${t}
`:H`
`),d=this.error.details&&(this.error.details.previousUpdateFailed||this.error.details.availableRange||this.error.details.requestedRange||this.error.details.suggestedRange||n),h=this._getErrorIcon(),p=this._getErrorColor();return H`
-
+
- +
${this._getErrorTitle()}
-
${errorMessage}
- ${this.error.errorCode ? x ` +
${c}
+ ${this.error.errorCode?H`
Error Code: ${this.error.errorCode}
- ` : ''} - ${showDetails ? x ` + `:""} + ${d?H`
- ${((_c = this.error.details) === null || _c === void 0 ? void 0 : _c.previousUpdateFailed) ? x ` + ${(null===(a=this.error.details)||void 0===a?void 0:a.previousUpdateFailed)?H`
- Previous Update Failed: ${this.error.details.previousError || 'Unknown error'} - ${this.error.details.previousErrorCode ? x ` (${this.error.details.previousErrorCode})` : ''} + Previous Update Failed: ${this.error.details.previousError||"Unknown error"} + ${this.error.details.previousErrorCode?H` (${this.error.details.previousErrorCode})`:""}
- ` : ''} - ${((_d = this.error.details) === null || _d === void 0 ? void 0 : _d.availableRange) ? x ` + `:""} + ${(null===(i=this.error.details)||void 0===i?void 0:i.availableRange)?H`
- Available Data Range: ${this.error.details.availableRange.oldest ? formatTimestamp(this.error.details.availableRange.oldest, this.locale, this.timeZone) : 'N/A'} to ${this.error.details.availableRange.newest ? formatTimestamp(this.error.details.availableRange.newest, this.locale, this.timeZone) : 'N/A'} - ${this.error.details.availableRange.totalCacheFolders ? x ` (${this.error.details.availableRange.totalCacheFolders} folders)` : ''} + Available Data Range: ${this.error.details.availableRange.oldest?kt(this.error.details.availableRange.oldest,this.locale,this.timeZone):"N/A"} to ${this.error.details.availableRange.newest?kt(this.error.details.availableRange.newest,this.locale,this.timeZone):"N/A"} + ${this.error.details.availableRange.totalCacheFolders?H` (${this.error.details.availableRange.totalCacheFolders} folders)`:""}
- ` : ''} - ${((_e = this.error.details) === null || _e === void 0 ? void 0 : _e.requestedRange) ? x ` + `:""} + ${(null===(o=this.error.details)||void 0===o?void 0:o.requestedRange)?H`
- Requested Range: ${this.error.details.requestedRange.start ? formatTimestamp(this.error.details.requestedRange.start, this.locale, this.timeZone) : 'N/A'} to ${this.error.details.requestedRange.end ? formatTimestamp(this.error.details.requestedRange.end, this.locale, this.timeZone) : 'N/A'} + Requested Range: ${this.error.details.requestedRange.start?kt(this.error.details.requestedRange.start,this.locale,this.timeZone):"N/A"} to ${this.error.details.requestedRange.end?kt(this.error.details.requestedRange.end,this.locale,this.timeZone):"N/A"}
- ` : ''} - ${((_f = this.error.details) === null || _f === void 0 ? void 0 : _f.suggestedRange) ? x ` + `:""} + ${(null===(r=this.error.details)||void 0===r?void 0:r.suggestedRange)?H`
- Suggested Range: ${this.error.details.suggestedRange.start ? formatTimestamp(this.error.details.suggestedRange.start, this.locale, this.timeZone) : 'N/A'} to ${this.error.details.suggestedRange.end ? formatTimestamp(this.error.details.suggestedRange.end, this.locale, this.timeZone) : 'N/A'} + Suggested Range: ${this.error.details.suggestedRange.start?kt(this.error.details.suggestedRange.start,this.locale,this.timeZone):"N/A"} to ${this.error.details.suggestedRange.end?kt(this.error.details.suggestedRange.end,this.locale,this.timeZone):"N/A"}
Try using this time range instead
- ` : ''} - ${refreshEndpoint ? x ` + `:""} + ${n?H`
- Refresh Endpoint: ${refreshEndpoint} + Refresh Endpoint: ${n}
- ` : ''} + `:""}
- ` : ''} - ${retryInfo} - ${this.error.retryable && this.onRetry ? x ` + `:""} + ${l} + ${this.error.retryable&&this.onRetry?H`
- ` : ''} + `:""}
- `; - } -}; -BomErrorDisplay.styles = [errorStyles]; -__decorate([ - n({ attribute: false }) -], BomErrorDisplay.prototype, "error", void 0); -__decorate([ - n({ attribute: false }) -], BomErrorDisplay.prototype, "locale", void 0); -__decorate([ - n({ attribute: false }) -], BomErrorDisplay.prototype, "timeZone", void 0); -__decorate([ - n({ attribute: false }) -], BomErrorDisplay.prototype, "onRetry", void 0); -BomErrorDisplay = __decorate([ - t$1('bom-error-display') -], BomErrorDisplay); - -let BomMetadataSection = class BomMetadataSection extends i { - constructor() { - super(...arguments); - this.position = 'above'; - this.locale = 'en-AU'; - } - _getDisplayConfig() { - if (!this.config || (typeof this.config === 'boolean' && !this.config)) { - return null; - } - return typeof this.config === 'object' ? this.config : {}; - } - render() { - var _a, _b; - const displayConfig = this._getDisplayConfig(); - if (!displayConfig) { - return x ``; - } - const configPosition = (_a = displayConfig.position) !== null && _a !== void 0 ? _a : 'above'; - // Only render if position matches - if (configPosition !== this.position) { - return x ``; - } - // If overlay, render differently - if (this.position === 'overlay') { - return this._renderOverlay(); - } - // Default style is 'cards' if not specified - const style = (_b = displayConfig.style) !== null && _b !== void 0 ? _b : 'cards'; - return x ` -