Files
bom-local-card/custom_components/bom_local/www/bom-local-card.js
T
alex 9d4d176013 Fix forecastTime to use API metadata instead of current time
- Use metadata.forecastTime from API response instead of endTime in fetchHistoricalFrames
- Ensures forecast time displays correctly in user's browser timezone
- Fixes issue where forecast time was showing incorrect timezone
2025-12-23 21:03:33 +10:00

3446 lines
134 KiB
JavaScript

/******************************************************************************
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?"<svg>":3===i?"<math>":"",c=f;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,y=0;for(;y<s.length&&(c.lastIndex=y,u=c.exec(s),null!==u);)y=c.lastIndex,c===f?"!--"===u[1]?c=v:void 0!==u[1]?c=_:void 0!==u[2]?($.test(u[2])&&(r=RegExp("</"+u[2],"g")),c=m):void 0!==u[3]&&(c=m):c===m?">"===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?"</svg>":3===i?"</math>":"")),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.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(e)){const i=v[a++],s=r.getAttribute(t).split(h),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:c,name:e[2],strings:s,ctor:"."===e[1]?H:"?"===e[1]?I:"@"===e[1]?L:k}),r.removeAttribute(t);}else t.startsWith(h)&&(d.push({type:6,index:c}),r.removeAttribute(t));if($.test(r.tagName)){const t=r.textContent.split(h),s=t.length-1;if(s>0){r.textContent=i$1?i$1.emptyScript:"";for(let i=0;i<s;i++)r.append(t[i],l()),C.nextNode(),d.push({type:2,index:++c});r.append(t[s],l());}}}else if(8===r.nodeType)if(r.data===o$2)d.push({type:2,index:c});else {let t=-1;for(;-1!==(t=r.data.indexOf(h,t+1));)d.push({type:7,index:c}),t+=h.length-1;}c++;}}static createElement(t,i){const s=r$3.createElement("template");return s.innerHTML=t,s}}function S(t,i,s=t,e){if(i===T)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=c(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(!1),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=S(t,h._$AS(t,i.values),h,e)),i}class M{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??r$3).importNode(i,!0);C.currentNode=e;let h=C.nextNode(),o=0,n=0,l=s[0];for(;void 0!==l;){if(o===l.index){let i;2===l.type?i=new R(h,h.nextSibling,this,t):1===l.type?i=new l.ctor(h,l.name,l.strings,this,t):6===l.type&&(i=new z(h,this,t)),this._$AV.push(i),l=s[++n];}o!==l?.index&&(h=C.nextNode(),o++);}return C.currentNode=r$3,e}p(t){let i=0;for(const s of this._$AV)void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class R{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=E,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??!0;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=S(this,t,i),c(t)?t===E||null==t||""===t?(this._$AH!==E&&this._$AR(),this._$AH=E):t!==this._$AH&&t!==T&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):u(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!==E&&c(this._$AH)?this._$AA.nextSibling.data=t:this.T(r$3.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=N.createElement(P(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new M(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=A.get(t.strings);return void 0===i&&A.set(t.strings,i=new N(t)),i}k(t){a(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new R(this.O(l()),this.O(l()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,i){for(this._$AP?.(!1,!0,i);t!==this._$AB;){const i=t.nextSibling;t.remove(),t=i;}}setConnected(t){void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class k{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=E,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==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<h.length-1;n++)r=S(this,e[s+n],i,n),r===T&&(r=this._$AH[n]),o||=!c(r)||r!==this._$AH[n],r===E?t=E:t!==E&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===E?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class H extends k{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===E?void 0:t;}}class I extends k{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==E);}}class L extends k{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=S(this,t,i,0)??E)===T)return;const s=this._$AH,e=t===E&&s!==E||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==E&&(s===E||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){S(this,t);}}const j=t$2.litHtmlPolyfillSupport;j?.(N,R),(t$2.litHtmlVersions??=[]).push("3.3.1");const B=(t,i,s)=>{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 `
<div class="editor">
<div class="section">
<h3>Location Configuration</h3>
<ha-textfield
label="Suburb"
.value=${this._config.suburb || ''}
@input=${(e) => this._updateConfig('suburb', e.target.value)}
required
></ha-textfield>
<div class="select-wrapper">
<label class="select-label">State *</label>
<select
class="native-select"
.value=${this._config.state || ''}
@change=${(e) => {
const select = e.target;
this._updateConfig('state', select.value);
}}
required
>
<option value="">Select a state...</option>
${AUSTRALIAN_STATES.map(state => x `<option value="${state.value}">${state.label} (${state.value})</option>`)}
</select>
</div>
</div>
<div class="section">
<h3>Display</h3>
<div class="switch-wrapper">
<label class="switch-label">Show Card Title</label>
<ha-switch
.checked=${this._config.show_card_title !== false}
@change=${(e) => {
const checked = e.target.checked;
this._updateConfig('show_card_title', checked);
}}
></ha-switch>
</div>
${this._config.show_card_title !== false ? x `
<ha-textfield
label="Card Title"
.value=${this._config.card_title || ''}
@input=${(e) => this._updateConfig('card_title', e.target.value)}
helper="Leave empty to use default"
></ha-textfield>
` : ''}
<div class="metadata-section">
<div class="section-header">
<div class="switch-wrapper">
<label class="switch-label">Show Metadata</label>
<ha-switch
.checked=${this._getMetadataEnabled()}
@change=${(e) => {
const checked = e.target.checked;
this._updateMetadataToggle(checked);
}}
></ha-switch>
</div>
${this._getMetadataEnabled() ? x `
<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 ? x `
<div class="metadata-options">
<div class="switch-wrapper">
<label class="switch-label">Cache Status</label>
<ha-switch
.checked=${this._getMetadataConfig('show_cache_status')}
@change=${(e) => this._updateMetadataConfig('show_cache_status', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Observation Time</label>
<ha-switch
.checked=${this._getMetadataConfig('show_observation_time')}
@change=${(e) => this._updateMetadataConfig('show_observation_time', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Forecast Time</label>
<ha-switch
.checked=${this._getMetadataConfig('show_forecast_time')}
@change=${(e) => this._updateMetadataConfig('show_forecast_time', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Weather Station</label>
<ha-switch
.checked=${this._getMetadataConfig('show_weather_station')}
@change=${(e) => this._updateMetadataConfig('show_weather_station', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Distance</label>
<ha-switch
.checked=${this._getMetadataConfig('show_distance')}
@change=${(e) => this._updateMetadataConfig('show_distance', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Next Update</label>
<ha-switch
.checked=${this._getMetadataConfig('show_next_update')}
@change=${(e) => this._updateMetadataConfig('show_next_update', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Frame Times</label>
<ha-switch
.checked=${this._getMetadataConfig('show_frame_times')}
@change=${(e) => this._updateMetadataConfig('show_frame_times', e.target.checked)}
></ha-switch>
</div>
<div class="select-wrapper">
<label class="select-label">Metadata Position *</label>
<select
class="native-select"
.value=${String((_a = this._getMetadataConfig('position')) !== null && _a !== void 0 ? _a : 'above')}
@change=${(e) => {
e.preventDefault();
e.stopPropagation();
try {
const select = e.target;
const value = select.value;
if (value && value !== '') {
this._updateMetadataConfig('position', value);
}
}
catch (err) {
console.error('Error updating metadata position:', err);
}
}}
>
<option value="above">Above Image (Default)</option>
<option value="below">Below Image</option>
<option value="overlay">Overlay on Image</option>
</select>
</div>
${this._getMetadataConfig('position') === 'overlay' ? x `
<div class="select-wrapper">
<label class="select-label">Metadata Overlay Position *</label>
<select
class="native-select"
.value=${(_b = this._config.metadata_overlay_position) !== null && _b !== void 0 ? _b : 'top'}
@change=${(e) => {
e.preventDefault();
e.stopPropagation();
const select = e.target;
this._updateConfig('metadata_overlay_position', select.value);
}}
>
<option value="top">Top (Default)</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="Metadata Overlay Opacity (0.0 - 1.0)"
type="number"
step="0.1"
min="0"
max="1"
.value=${String((_c = this._config.metadata_overlay_opacity) !== null && _c !== void 0 ? _c : 0.85)}
@input=${(e) => this._updateConfig('metadata_overlay_opacity', parseFloat(e.target.value))}
></ha-textfield>
` : ''}
${this._getMetadataConfig('position') !== 'overlay' ? x `
<div class="select-wrapper">
<label class="select-label">Metadata Style *</label>
<select
class="native-select"
.value=${(_d = this._getMetadataConfig('style')) !== null && _d !== void 0 ? _d : 'cards'}
@change=${(e) => {
e.preventDefault();
e.stopPropagation();
const select = e.target;
const value = select.value;
// Allow empty string to reset to default
if (value === '') {
this._updateMetadataConfig('style', undefined);
}
else {
this._updateMetadataConfig('style', value);
}
}}
>
<option value="cards">Cards (Default)</option>
<option value="compact">Compact</option>
<option value="minimal">Minimal</option>
</select>
</div>
` : x `
<div class="info-text" style="padding: 8px; background: var(--info-color, rgba(33, 150, 243, 0.1)); border-radius: 4px; font-size: 0.875em;">
<strong>Note:</strong> Overlay position uses a fixed compact style optimized for overlaying on images.
</div>
`}
</div>
` : ''}
</div>
<div class="controls-section">
<div class="section-header">
<div class="switch-wrapper">
<label class="switch-label">Show Controls</label>
<ha-switch
.checked=${this._getControlsEnabled()}
@change=${(e) => {
const checked = e.target.checked;
this._updateControlsToggle(checked);
}}
></ha-switch>
</div>
${this._getControlsEnabled() ? x `
<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 ? x `
<div class="controls-options">
<div class="switch-wrapper">
<label class="switch-label">Play/Pause Button</label>
<ha-switch
.checked=${this._getControlsConfig('show_play_pause')}
@change=${(e) => this._updateControlsConfig('show_play_pause', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Previous/Next Buttons</label>
<ha-switch
.checked=${this._getControlsConfig('show_prev_next')}
@change=${(e) => this._updateControlsConfig('show_prev_next', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Frame Slider</label>
<ha-switch
.checked=${this._getControlsConfig('show_slider')}
@change=${(e) => this._updateControlsConfig('show_slider', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Navigation Buttons (-10, +10, First, Last)</label>
<ha-switch
.checked=${this._getControlsConfig('show_nav_buttons')}
@change=${(e) => this._updateControlsConfig('show_nav_buttons', e.target.checked)}
></ha-switch>
</div>
<div class="switch-wrapper">
<label class="switch-label">Frame Info</label>
<ha-switch
.checked=${this._getControlsConfig('show_frame_info')}
@change=${(e) => this._updateControlsConfig('show_frame_info', e.target.checked)}
></ha-switch>
</div>
<div class="select-wrapper">
<label class="select-label">Controls Position *</label>
<select
class="native-select"
.value=${String((_e = this._getControlsConfig('position')) !== null && _e !== void 0 ? _e : 'below')}
@change=${(e) => {
e.preventDefault();
e.stopPropagation();
try {
const select = e.target;
const value = select.value;
if (value && value !== '') {
this._updateControlsConfig('position', value);
}
}
catch (err) {
console.error('Error updating controls position:', err);
}
}}
>
<option value="above">Above Image</option>
<option value="below">Below Image (Default)</option>
<option value="overlay">Overlay on Image</option>
</select>
</div>
${this._getControlsConfig('position') === 'overlay' ? x `
<div class="select-wrapper">
<label class="select-label">Controls Overlay Position *</label>
<select
class="native-select"
.value=${(_f = this._config.controls_overlay_position) !== null && _f !== void 0 ? _f : 'bottom'}
@change=${(e) => {
e.preventDefault();
e.stopPropagation();
try {
const select = e.target;
this._updateConfig('controls_overlay_position', select.value);
}
catch (err) {
console.error('Error updating controls overlay position:', err);
}
}}
>
<option value="top">Top</option>
<option value="bottom">Bottom (Default)</option>
<option value="left">Left</option>
<option value="right">Right</option>
<option value="center">Center</option>
</select>
</div>
<ha-textfield
label="Controls Overlay Opacity (0.0 - 1.0)"
type="number"
step="0.1"
min="0"
max="1"
.value=${String((_g = this._config.controls_overlay_opacity) !== null && _g !== void 0 ? _g : 0.9)}
@input=${(e) => this._updateConfig('controls_overlay_opacity', parseFloat(e.target.value))}
></ha-textfield>
${(() => {
const conflict = this._checkOverlayConflict();
return conflict.hasConflict ? x `
<div class="warning-text" style="padding: 8px; background: var(--warning-color, rgba(255, 152, 0, 0.1)); border-left: 3px solid var(--warning-color, #ff9800); border-radius: 4px; font-size: 0.875em; margin-top: 8px;">
${conflict.message}
</div>
` : '';
})()}
` : ''}
</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) => this._updateConfig('image_zoom', parseFloat(e.target.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) => {
const select = e.target;
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>
<div class="select-wrapper">
<label class="select-label">Timespan</label>
<select
class="native-select"
.value=${this._config.timespan || 'latest'}
@change=${(e) => {
const select = e.target;
const newTimespan = select.value;
// Clear custom time fields if switching away from custom
if (newTimespan !== 'custom' && this._config.timespan === 'custom') {
const updatedConfig = Object.assign({}, this._config);
updatedConfig.timespan = newTimespan;
delete updatedConfig.custom_start_time;
delete updatedConfig.custom_end_time;
this._config = updatedConfig;
ne(this, 'config-changed', { config: updatedConfig });
}
else {
this._updateConfig('timespan', newTimespan);
}
}}
>
<option value="latest">Latest 7 frames</option>
${this._cacheRange && this._cacheRange.totalCacheFolders > 0 ? x `
<option value="1h">Last 1 hour${this._getRangeWarning('1h') ? ' ⚠️' : ''}</option>
<option value="3h">Last 3 hours${this._getRangeWarning('3h') ? ' ⚠️' : ''}</option>
<option value="6h">Last 6 hours${this._getRangeWarning('6h') ? ' ⚠️' : ''}</option>
<option value="12h">Last 12 hours${this._getRangeWarning('12h') ? ' ⚠️' : ''}</option>
<option value="24h">Last 24 hours${this._getRangeWarning('24h') ? ' ⚠️' : ''}</option>
` : x `
<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>
${this._cacheRange && this._cacheRange.totalCacheFolders > 0 ? x `
<div class="cache-range-info">
<div class="info-text">
<strong>Available cache:</strong> ${this._formatCacheRange()}
</div>
</div>
` : this._cacheRangeError ? x `
<div class="cache-range-warning">
<div class="warning-text">⚠️ ${this._cacheRangeError}</div>
</div>
` : ''}
${this._getRangeWarning(this._config.timespan || 'latest') ? x `
<div class="range-warning">
<div class="warning-text">
⚠️ Selected timespan may not have enough data. Available: ${this._formatCacheRange()}
</div>
</div>
` : ''}
${this._config.timespan === 'custom' ? x `
<ha-textfield
label="Custom Start Time (ISO 8601)"
.value=${this._config.custom_start_time || ''}
@input=${(e) => this._updateConfig('custom_start_time', e.target.value)}
helper="e.g., 2025-01-15T10:00:00Z"
></ha-textfield>
<ha-textfield
label="Custom End Time (ISO 8601)"
.value=${this._config.custom_end_time || ''}
@input=${(e) => this._updateConfig('custom_end_time', e.target.value)}
helper="e.g., 2025-01-15T18:00:00Z"
></ha-textfield>
${this._validateCustomRange() ? x `
<div class="range-warning">
<div class="warning-text">
⚠️ ${this._validateCustomRange()}
</div>
</div>
` : ''}
` : ''}
<ha-textfield
label="Frame Interval (seconds)"
type="number"
step="0.5"
min="0.5"
max="10"
.value=${String(this._config.frame_interval || 2.0)}
@input=${(e) => this._updateConfig('frame_interval', parseFloat(e.target.value))}
></ha-textfield>
<div class="switch-wrapper">
<label class="switch-label">Auto Play</label>
<ha-switch
.checked=${this._config.auto_play !== false}
@change=${(e) => {
const checked = e.target.checked;
this._updateConfig('auto_play', checked);
}}
></ha-switch>
</div>
</div>
<div class="section">
<h3>Auto Refresh</h3>
<ha-textfield
label="Refresh Interval (seconds)"
type="number"
min="10"
max="300"
step="10"
.value=${String(this._config.refresh_interval || 30)}
@input=${(e) => this._updateConfig('refresh_interval', parseInt(e.target.value))}
></ha-textfield>
</div>
</div>
`;
}
_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 `
.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);
}
.switch-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
margin-bottom: 0;
}
.switch-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--primary-text-color, #212121);
flex: 1;
cursor: pointer;
}
ha-switch {
margin-left: 16px;
flex-shrink: 0;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
.cache-range-info {
margin-top: 8px;
padding: 8px 12px;
background: var(--info-color, rgba(33, 150, 243, 0.1));
border-left: 3px solid var(--info-color, #2196f3);
border-radius: 4px;
}
.cache-range-info .info-text {
font-size: 0.875em;
color: var(--primary-text-color, #212121);
}
.range-warning,
.cache-range-warning {
margin-top: 8px;
padding: 8px 12px;
background: var(--warning-color, rgba(255, 152, 0, 0.1));
border-left: 3px solid var(--warning-color, #ff9800);
border-radius: 4px;
}
.range-warning .warning-text,
.cache-range-warning .warning-text {
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.2.0';
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 `
:host {
--bom-primary-color: var(--primary-color, #667eea);
--bom-secondary-color: var(--secondary-text-color, #666);
--bom-card-background: var(--card-background-color, white);
--bom-text-color: var(--primary-text-color, #333);
--bom-border-color: var(--divider-color, #e0e0e0);
}
#root {
width: 100%;
position: relative;
}
`;
/**
* Styles for radar image container and loading states
*/
const imageStyles = i$3 `
/* Radar Image Container */
.radar-image-container {
position: relative;
width: 100%;
background: #000;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
aspect-ratio: 16/9;
min-height: 200px; /* Smaller on mobile */
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 480px) {
.radar-image-container {
min-height: 300px;
}
}
@media (min-width: 768px) {
.radar-image-container {
min-height: 400px;
}
}
.radar-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
image-rendering: crisp-edges;
transition: transform 0.3s ease;
}
.radar-image-contain {
width: 100%;
height: 100%;
object-fit: contain;
}
.radar-image-cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.radar-image-fill {
width: 100%;
height: 100%;
object-fit: fill;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1.2em;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.loading-message {
font-size: 0.9em;
opacity: 0.9;
}
`;
/**
* Styles for frame slider, navigation buttons, and play controls
*/
const controlsStyles = i$3 `
.frame-slider-container {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
border: 2px solid #e0e0e0;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
margin-bottom: 15px;
box-sizing: border-box;
}
.frame-slider-wrapper {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.frame-nav-btn {
padding: 10px 16px;
border: 2px solid var(--primary-color, #667eea);
background: white;
color: var(--primary-color, #667eea);
border-radius: 10px;
cursor: pointer;
font-weight: 700;
font-size: 0.9em;
transition: all 0.2s;
min-width: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.frame-nav-btn:hover:not(:disabled) {
background: var(--primary-color, #667eea);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.frame-nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
background: #f0f0f0;
border-color: #d0d0d0;
color: #999;
}
.frame-slider {
flex: 1;
min-width: 120px;
height: 10px;
border-radius: 5px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
cursor: pointer;
order: 3;
}
.frame-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
border: 3px solid white;
transition: all 0.2s;
margin-top: -7px; /* Center the 24px thumb on the 10px track: (24-10)/2 = 7 */
}
.frame-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.6);
}
.frame-slider::-webkit-slider-runnable-track {
height: 10px;
border-radius: 5px;
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
}
.frame-slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
.frame-slider::-moz-range-track {
height: 10px;
border-radius: 5px;
background: #e0e0e0;
}
.frame-slider::-moz-range-progress {
height: 10px;
border-radius: 5px;
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
}
.play-controls {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.play-btn {
padding: 10px 16px;
border: 2px solid var(--primary-color, #667eea);
background: var(--primary-color, #667eea);
color: white;
border-radius: 10px;
cursor: pointer;
font-weight: 700;
font-size: 0.9em;
transition: all 0.2s;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.play-btn:hover:not(:disabled) {
background: var(--primary-color-dark, #5568d3);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.play-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.frame-info {
text-align: center;
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
font-size: 0.85em;
margin-top: 6px;
margin-bottom: 4px;
padding: 6px 12px;
background: var(--card-background-color, #fff);
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
line-height: 1.4;
}
.frame-index {
font-weight: 600;
color: var(--primary-text-color, rgba(0, 0, 0, 0.87));
white-space: nowrap;
}
.frame-time {
white-space: nowrap;
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
}
.observation-time {
white-space: nowrap;
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
}
.frame-progress {
white-space: nowrap;
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
font-size: 0.9em;
}
.frame-nav-buttons {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.controls-section {
margin-top: 15px;
}
/* Responsive adjustments */
@media (min-width: 480px) {
.frame-slider-wrapper {
flex-wrap: nowrap;
}
.frame-slider {
order: 0;
}
}
`;
/**
* Styles for metadata display sections
*/
const metadataStyles = i$3 `
/* Metadata Section */
.metadata-section {
margin-bottom: 15px;
}
.metadata-section.metadata-cards {
display: grid;
grid-template-columns: 1fr; /* Single column on mobile */
gap: 12px;
}
@media (min-width: 480px) {
.metadata-section.metadata-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 768px) {
.metadata-section.metadata-cards {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
}
.metadata-section.metadata-cards .metadata-item {
background: var(--card-background-color, #ffffff);
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
border-radius: 8px;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.metadata-section.metadata-cards .metadata-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
.metadata-section.metadata-cards .metadata-label {
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.7;
margin-bottom: 2px;
}
.metadata-section.metadata-cards .metadata-value {
font-size: 1.1em;
font-weight: 600;
}
.metadata-section.metadata-compact {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 0.9em;
}
.metadata-section.metadata-compact .metadata-item {
background: var(--secondary-background-color, #f5f5f5);
border-radius: 6px;
padding: 8px 12px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.metadata-section.metadata-minimal {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.85em;
}
.metadata-section.metadata-minimal .metadata-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08));
}
.metadata-section.metadata-minimal .metadata-item:last-child {
border-bottom: none;
}
.metadata-item {
display: flex;
align-items: center;
gap: 6px;
}
.metadata-label {
color: var(--bom-secondary-color);
font-weight: 600;
font-size: 0.85em;
}
.metadata-value {
color: var(--bom-text-color);
font-weight: 600;
}
.metadata-relative {
color: var(--bom-secondary-color);
font-size: 0.85em;
}
.info-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.info-card {
background: var(--bom-card-background);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--bom-primary-color);
}
.info-card h3 {
font-size: 0.8em;
text-transform: uppercase;
color: var(--bom-secondary-color);
margin-bottom: 6px;
letter-spacing: 0.5px;
}
.info-card .value {
font-size: 1.2em;
font-weight: 600;
color: var(--bom-text-color);
}
`;
/**
* Styles for overlay controls and metadata overlays
*/
const overlayStyles = i$3 `
/* Overlay Controls */
.controls-overlay {
position: absolute;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
border-radius: 8px;
padding: 12px;
z-index: 10;
transition: opacity 0.3s ease;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 200px;
}
.controls-overlay.overlay-top {
top: 12px;
left: 50%;
transform: translateX(-50%);
}
.controls-overlay.overlay-bottom {
bottom: 12px;
left: 50%;
transform: translateX(-50%);
}
.controls-overlay.overlay-left {
left: 12px;
top: 50%;
transform: translateY(-50%);
}
.controls-overlay.overlay-right {
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.controls-overlay.overlay-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Metadata Overlay */
.metadata-overlay {
position: absolute;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
border-radius: 6px;
padding: 8px 12px;
z-index: 5;
font-size: 0.85em;
color: white;
display: flex;
flex-direction: column;
gap: 4px;
}
.metadata-overlay .metadata-item {
color: white;
}
.metadata-overlay .metadata-label {
color: rgba(255, 255, 255, 0.8);
}
.metadata-overlay.overlay-top {
top: 12px;
left: 12px;
right: auto;
}
.metadata-overlay.overlay-bottom {
bottom: 12px;
left: 12px;
right: auto;
}
.metadata-overlay.overlay-left {
left: 12px;
top: 50%;
transform: translateY(-50%);
}
.metadata-overlay.overlay-right {
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.metadata-overlay.overlay-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`;
/**
* Styles for error display components
*/
const errorStyles = i$3 `
/* Error Display */
.error-container {
padding: 16px;
}
.error-card {
background: var(--card-background-color, #ffffff);
border-radius: 12px;
border-left: 4px solid var(--error-color);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1));
transition: opacity 0.3s ease, transform 0.3s ease;
}
.error-header {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
background: var(--secondary-background-color, #fafafa);
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1));
}
.error-icon {
color: var(--error-color);
width: 24px;
height: 24px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.error-title {
font-size: 1.1em;
font-weight: 600;
color: var(--primary-text-color, #212121);
flex: 1;
}
.error-content {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.error-message {
font-weight: 500;
line-height: 1.7;
display: flex;
flex-direction: column;
gap: 6px;
color: var(--primary-text-color, #212121);
font-size: 1.05em;
margin-bottom: 4px;
}
.error-line {
min-height: 1.2em;
}
.error-line-spacer {
height: 0.5em;
}
.error-code {
font-size: 0.85em;
font-family: monospace;
color: var(--secondary-text-color, #666);
background: var(--secondary-background-color, #f5f5f5);
padding: 6px 12px;
border-radius: 6px;
display: inline-flex;
align-items: center;
gap: 6px;
width: fit-content;
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1));
}
.code-icon {
width: 16px;
height: 16px;
opacity: 0.7;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.error-retry-info {
font-size: 0.9em;
color: var(--primary-text-color, #212121);
padding: 10px 14px;
background: linear-gradient(135deg,
rgba(33, 150, 243, 0.08) 0%,
rgba(33, 150, 243, 0.04) 100%);
border-radius: 8px;
border-left: 3px solid var(--primary-color, #2196f3);
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.error-retry-info::before {
content: '⏱';
font-size: 1.1em;
}
.error-details {
margin-top: 8px;
padding-top: 16px;
border-top: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
display: flex;
flex-direction: column;
gap: 10px;
}
.error-detail-item {
font-size: 0.9em;
line-height: 1.6;
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
background: var(--secondary-background-color, #fafafa);
border-radius: 8px;
transition: background 0.2s ease;
}
.error-detail-item:hover {
background: var(--secondary-background-color, #f0f0f0);
}
.detail-icon {
color: var(--error-color);
width: 18px;
height: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 2px;
}
.detail-content {
flex: 1;
color: var(--primary-text-color, #212121);
}
.detail-content strong {
font-weight: 600;
color: var(--primary-text-color, #212121);
}
.detail-content code {
font-family: monospace;
font-size: 0.9em;
background: var(--secondary-background-color, rgba(0, 0, 0, 0.05));
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.15));
color: var(--primary-text-color, #212121);
display: inline-block;
word-break: break-all;
}
.error-detail-item.suggested-range {
background: linear-gradient(135deg,
rgba(255, 193, 7, 0.1) 0%,
rgba(255, 193, 7, 0.05) 100%);
border-left: 3px solid var(--warning-color, #ffc107);
}
.suggestion-hint {
font-size: 0.85em;
color: var(--secondary-text-color, #666);
margin-top: 4px;
font-style: italic;
}
.error-actions {
margin-top: 16px;
display: flex;
gap: 12px;
justify-content: flex-start;
}
.retry-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
min-height: 44px;
background: var(--error-color, #f44336);
color: white !important;
border: 2px solid var(--error-color, #f44336);
border-radius: 8px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.2);
font-family: inherit;
-webkit-appearance: none;
appearance: none;
user-select: none;
position: relative;
z-index: 1;
filter: brightness(1.05);
}
.retry-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 6px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0));
pointer-events: none;
z-index: -1;
}
.retry-button:hover {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
filter: brightness(0.95);
}
.retry-button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.retry-button:focus {
outline: 2px solid var(--error-color);
outline-offset: 2px;
}
.retry-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.retry-icon {
width: 18px;
height: 18px;
color: white;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.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 `<div class="error-retry-info">Auto-retrying in ${this.error.retryAfter} seconds...</div>`
: isManualRefreshRecommended
? x `<div class="error-retry-info">Manual refresh recommended. Previous update failed.</div>`
: '';
// 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 `<div class="error-line">${line}</div>`
: x `<div class="error-line-spacer"></div>`);
// 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 `
<div class="error-container">
<div class="error-card" style="--error-color: ${errorColor}">
<div class="error-header">
<ha-icon class="error-icon" .icon=${errorIcon}></ha-icon>
<div class="error-title">${this._getErrorTitle()}</div>
</div>
<div class="error-content">
<div class="error-message">${errorMessage}</div>
${this.error.errorCode ? x `
<div class="error-code">
<ha-icon class="code-icon" icon="mdi:code-tags"></ha-icon>
<span>Error Code: ${this.error.errorCode}</span>
</div>
` : ''}
${showDetails ? x `
<div class="error-details">
${((_c = this.error.details) === null || _c === void 0 ? void 0 : _c.previousUpdateFailed) ? x `
<div class="error-detail-item">
<ha-icon class="detail-icon" icon="mdi:alert-circle"></ha-icon>
<div class="detail-content">
<strong>Previous Update Failed:</strong> ${this.error.details.previousError || 'Unknown error'}
${this.error.details.previousErrorCode ? x ` (${this.error.details.previousErrorCode})` : ''}
</div>
</div>
` : ''}
${((_d = this.error.details) === null || _d === void 0 ? void 0 : _d.availableRange) ? x `
<div class="error-detail-item">
<ha-icon class="detail-icon" icon="mdi:database-clock"></ha-icon>
<div class="detail-content">
<strong>Available Data Range:</strong> ${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)` : ''}
</div>
</div>
` : ''}
${((_e = this.error.details) === null || _e === void 0 ? void 0 : _e.requestedRange) ? x `
<div class="error-detail-item">
<ha-icon class="detail-icon" icon="mdi:calendar-range"></ha-icon>
<div class="detail-content">
<strong>Requested Range:</strong> ${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'}
</div>
</div>
` : ''}
${((_f = this.error.details) === null || _f === void 0 ? void 0 : _f.suggestedRange) ? x `
<div class="error-detail-item suggested-range">
<ha-icon class="detail-icon" icon="mdi:lightbulb-on"></ha-icon>
<div class="detail-content">
<strong>Suggested Range:</strong> ${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'}
<div class="suggestion-hint">Try using this time range instead</div>
</div>
</div>
` : ''}
${refreshEndpoint ? x `
<div class="error-detail-item">
<ha-icon class="detail-icon" icon="mdi:refresh"></ha-icon>
<div class="detail-content">
<strong>Refresh Endpoint:</strong> <code>${refreshEndpoint}</code>
</div>
</div>
` : ''}
</div>
` : ''}
${retryInfo}
${this.error.retryable && this.onRetry ? x `
<div class="error-actions">
<button
class="retry-button"
type="button"
style="--error-color: ${errorColor}"
@click=${(e) => {
e.preventDefault();
e.stopPropagation();
if (this.onRetry) {
this.onRetry();
}
}}
>
<ha-icon class="retry-icon" icon="mdi:refresh"></ha-icon>
<span>${isManualRefreshRecommended ? 'Retry Anyway' : 'Retry Now'}</span>
</button>
</div>
` : ''}
</div>
</div>
</div>
`;
}
};
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 `
<div class="metadata-section metadata-${this.position} metadata-${style}">
${displayConfig.show_cache_status !== false ? this._renderCacheStatus() : ''}
${displayConfig.show_observation_time !== false ? this._renderObservationTime() : ''}
${displayConfig.show_forecast_time !== false ? this._renderForecastTime() : ''}
${displayConfig.show_weather_station !== false ? this._renderWeatherStation() : ''}
${displayConfig.show_distance !== false ? this._renderDistance() : ''}
${displayConfig.show_next_update !== false ? this._renderNextUpdate() : ''}
</div>
`;
}
_renderOverlay() {
var _a, _b;
const displayConfig = this._getDisplayConfig();
if (!displayConfig) {
return x ``;
}
const opacity = (_a = this.overlayOpacity) !== null && _a !== void 0 ? _a : 0.85;
const position = (_b = this.overlayPosition) !== null && _b !== void 0 ? _b : 'top';
return x `
<div class="metadata-overlay overlay-${position}" style="opacity: ${opacity};">
${displayConfig.show_cache_status !== false ? this._renderCacheStatus() : ''}
${displayConfig.show_observation_time !== false ? this._renderObservationTime() : ''}
${displayConfig.show_forecast_time !== false ? this._renderForecastTime() : ''}
${displayConfig.show_weather_station !== false ? this._renderWeatherStation() : ''}
${displayConfig.show_distance !== false ? this._renderDistance() : ''}
${displayConfig.show_next_update !== false ? this._renderNextUpdate() : ''}
</div>
`;
}
_renderCacheStatus() {
if (!this.radarData)
return x ``;
return x `
<div class="metadata-item cache-status">
<span class="metadata-label">Cache:</span>
<span class="metadata-value">
${this.radarData.isUpdating ? 'Updating' :
this.radarData.cacheIsValid ? 'Valid' : 'Invalid'}
</span>
</div>
`;
}
_renderObservationTime() {
var _a;
if (!((_a = this.radarData) === null || _a === void 0 ? void 0 : _a.observationTime))
return x ``;
return x `
<div class="metadata-item observation-time">
<span class="metadata-label">Observation:</span>
<span class="metadata-value">
${formatTimestamp(this.radarData.observationTime, this.locale, this.timeZone)}
</span>
<span class="metadata-relative">
(${formatRelativeTime(this.radarData.observationTime)})
</span>
</div>
`;
}
_renderForecastTime() {
var _a;
if (!((_a = this.radarData) === null || _a === void 0 ? void 0 : _a.forecastTime))
return x ``;
return x `
<div class="metadata-item forecast-time">
<span class="metadata-label">Forecast:</span>
<span class="metadata-value">${formatTimestamp(this.radarData.forecastTime, this.locale, this.timeZone)}</span>
</div>
`;
}
_renderWeatherStation() {
var _a;
if (!((_a = this.radarData) === null || _a === void 0 ? void 0 : _a.weatherStation))
return x ``;
return x `
<div class="metadata-item weather-station">
<span class="metadata-label">Station:</span>
<span class="metadata-value">${this.radarData.weatherStation}</span>
</div>
`;
}
_renderDistance() {
var _a;
if (!((_a = this.radarData) === null || _a === void 0 ? void 0 : _a.distance))
return x ``;
return x `
<div class="metadata-item distance">
<span class="metadata-label">Distance:</span>
<span class="metadata-value">${this.radarData.distance}</span>
</div>
`;
}
_renderNextUpdate() {
var _a;
if (!((_a = this.radarData) === null || _a === void 0 ? void 0 : _a.nextUpdateTime))
return x ``;
return x `
<div class="metadata-item next-update">
<span class="metadata-label">Next Update:</span>
<span class="metadata-value">${formatTimestamp(this.radarData.nextUpdateTime, this.locale, this.timeZone)}</span>
</div>
`;
}
};
BomMetadataSection.styles = [metadataStyles, overlayStyles];
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "radarData", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "config", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "position", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "locale", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "timeZone", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "overlayOpacity", void 0);
__decorate([
n({ attribute: false })
], BomMetadataSection.prototype, "overlayPosition", void 0);
BomMetadataSection = __decorate([
t$1('bom-metadata-section')
], BomMetadataSection);
let BomControlsSection = class BomControlsSection extends i {
constructor() {
super(...arguments);
this.frames = [];
this.currentFrameIndex = 0;
this.isPlaying = false;
this.isExtendedMode = false;
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;
const displayConfig = this._getDisplayConfig();
if (!displayConfig) {
return x ``;
}
const configPosition = (_a = displayConfig.position) !== null && _a !== void 0 ? _a : 'below';
// Only render if position matches (or if no position filter specified)
if (this.position !== undefined && configPosition !== this.position) {
return x ``;
}
// If overlay, render differently
if (configPosition === 'overlay') {
return this._renderOverlay(displayConfig);
}
return x `
<div class="controls-section controls-${configPosition}">
${displayConfig.show_slider !== false ? this._renderFrameSlider() : ''}
${displayConfig.show_nav_buttons !== false ? this._renderNavButtons() : ''}
${displayConfig.show_play_pause !== false ? this._renderPlayPause() : ''}
${displayConfig.show_prev_next !== false ? this._renderPrevNext() : ''}
${displayConfig.show_frame_info !== false ? this._renderFrameInfo() : ''}
</div>
`;
}
_renderOverlay(displayConfig) {
var _a, _b;
const opacity = (_a = this.overlayOpacity) !== null && _a !== void 0 ? _a : 0.9;
const position = (_b = this.overlayPosition) !== null && _b !== void 0 ? _b : 'bottom';
return x `
<div class="controls-overlay overlay-${position}" style="opacity: ${opacity};">
${displayConfig.show_slider !== false ? this._renderFrameSlider() : ''}
${displayConfig.show_play_pause !== false ? this._renderPlayPause() : ''}
${displayConfig.show_prev_next !== false ? this._renderPrevNext() : ''}
${displayConfig.show_frame_info !== false ? this._renderFrameInfo() : ''}
</div>
`;
}
_renderFrameSlider() {
if (this.frames.length === 0)
return x ``;
return x `
<div class="frame-slider-container">
<div class="frame-slider-wrapper">
<input
type="range"
class="frame-slider"
min="0"
max="${this.frames.length - 1}"
.value="${this.currentFrameIndex}"
@input="${(e) => {
const index = parseInt(e.target.value);
if (this.onFrameChange) {
this.onFrameChange(index);
}
}}"
aria-label="Frame slider"
/>
</div>
</div>
`;
}
_renderNavButtons() {
if (this.frames.length === 0)
return x ``;
// Calculate dynamic skip amount based on total frames (1/3 of total, min 1, max 50)
const skipAmount = this.isExtendedMode
? Math.max(1, Math.min(50, Math.round(this.frames.length / 3)))
: 0;
return x `
<div class="frame-nav-buttons">
${this.isExtendedMode ? x `
<button
class="frame-nav-btn"
@click="${() => this.onFrameChange && this.onFrameChange(0)}"
?disabled="${this.currentFrameIndex === 0}"
title="First frame"
aria-label="First frame"
>⏮</button>
<button
class="frame-nav-btn"
@click="${() => this.onJumpFrame && this.onJumpFrame(-skipAmount)}"
?disabled="${this.currentFrameIndex === 0}"
title="Go back ${skipAmount} frames"
aria-label="Go back ${skipAmount} frames"
>-${skipAmount}</button>
<button
class="frame-nav-btn"
@click="${() => this.onJumpFrame && this.onJumpFrame(skipAmount)}"
?disabled="${this.currentFrameIndex >= this.frames.length - 1}"
title="Go forward ${skipAmount} frames"
aria-label="Go forward ${skipAmount} frames"
>+${skipAmount}</button>
<button
class="frame-nav-btn"
@click="${() => this.onFrameChange && this.onFrameChange(this.frames.length - 1)}"
?disabled="${this.currentFrameIndex >= this.frames.length - 1}"
title="Last frame"
aria-label="Last frame"
>⏭</button>
` : x `
<button
class="frame-nav-btn"
@click="${() => this.onFrameChange && this.onFrameChange(0)}"
?disabled="${this.currentFrameIndex === 0}"
title="First frame"
aria-label="First frame"
>⏮</button>
<button
class="frame-nav-btn"
@click="${() => this.onFrameChange && this.onFrameChange(this.frames.length - 1)}"
?disabled="${this.currentFrameIndex >= this.frames.length - 1}"
title="Last frame"
aria-label="Last frame"
>⏭</button>
`}
</div>
`;
}
_renderPlayPause() {
return x `
<div class="play-controls">
<button
class="play-btn"
@click="${() => this.onToggleAnimation && this.onToggleAnimation()}"
aria-label="${this.isPlaying ? 'Pause animation' : 'Play animation'}"
aria-pressed="${this.isPlaying}"
>
${this.isPlaying ? '⏸ Pause' : '▶ Play'}
</button>
</div>
`;
}
_renderPrevNext() {
return x `
<div class="play-controls">
<button
class="play-btn"
@click="${() => this.onPrevious && this.onPrevious()}"
aria-label="Previous frame"
>◀ Previous</button>
<button
class="play-btn"
@click="${() => this.onNext && this.onNext()}"
aria-label="Next frame"
>Next ▶</button>
</div>
`;
}
_renderFrameInfo() {
var _a, _b;
const currentFrame = this.frames[this.currentFrameIndex];
if (!currentFrame)
return x ``;
const showFrameTimes = this.showFrameTimes !== false;
// Format frame info
let frameInfoText = '';
if (this.isExtendedMode && currentFrame.absoluteObservationTime) {
frameInfoText = `Frame ${((_a = currentFrame.sequentialIndex) !== null && _a !== void 0 ? _a : this.currentFrameIndex) + 1} of ${this.frames.length}`;
}
else {
frameInfoText = `Frame ${currentFrame.frameIndex + 1} of ${this.frames.length}`;
}
const progress = this.frames.length > 0
? Math.round(((this.currentFrameIndex + 1) / this.frames.length) * 100)
: 0;
return x `
<div class="frame-info">
<span class="frame-index">${frameInfoText}</span>
${showFrameTimes && currentFrame.absoluteObservationTime ? x `
<span class="frame-time">
${formatTimestamp(currentFrame.absoluteObservationTime, this.locale, this.timeZone)}
</span>
` : showFrameTimes && currentFrame.minutesAgo !== undefined ? x `
<span class="frame-time">
${currentFrame.minutesAgo} min ago
</span>
` : ''}
${((_b = this.radarData) === null || _b === void 0 ? void 0 : _b.observationTime) && showFrameTimes ? x `
<span class="observation-time">
Obs: ${formatTimestamp(this.radarData.observationTime, this.locale, this.timeZone)}
</span>
` : ''}
<span class="frame-progress">${progress}%</span>
</div>
`;
}
};
BomControlsSection.styles = [controlsStyles, overlayStyles];
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "frames", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "currentFrameIndex", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "isPlaying", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "isExtendedMode", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "radarData", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "config", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "position", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "locale", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "timeZone", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "overlayOpacity", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "overlayPosition", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "showFrameTimes", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "onFrameChange", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "onPrevious", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "onNext", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "onJumpFrame", void 0);
__decorate([
n({ attribute: false })
], BomControlsSection.prototype, "onToggleAnimation", void 0);
BomControlsSection = __decorate([
t$1('bom-controls-section')
], BomControlsSection);
var _a;
console.info(`%c BOM-LOCAL-RADAR-CARD \n%c Version ${CARD_VERSION} `, 'color: orange; font-weight: bold; background: black', 'color: white; font-weight: bold; background: dimgray');
window.customCards = (_a = window.customCards) !== null && _a !== void 0 ? _a : [];
window.customCards.push({
type: 'bom-local-card',
name: 'BOM Local Card',
description: 'A rain radar card using the local BOM service',
});
let BomLocalRadarCard = class BomLocalRadarCard extends i {
constructor() {
super(...arguments);
this.currentFrameIndex = 0;
this.isLoading = false;
this.isPlaying = false;
this.frames = [];
this.isExtendedMode = false;
this.preloadedImages = [];
this.apiService = new RadarApiService();
/**
* Handle keyboard navigation
*/
this._handleKeyDown = (e) => {
var _a;
// Only handle if card is focused or contains focused element
if (!((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) && document.activeElement !== this) {
return;
}
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
this.previousFrame();
break;
case 'ArrowRight':
e.preventDefault();
this.nextFrame();
break;
case ' ':
e.preventDefault();
this.toggleAnimation();
break;
case 'Home':
e.preventDefault();
this.showFrame(0);
break;
case 'End':
e.preventDefault();
this.showFrame(this.frames.length - 1);
break;
}
};
}
static async getConfigElement() {
return document.createElement('bom-local-card-editor');
}
static getStubConfig() {
return {
type: 'custom:bom-local-card',
suburb: 'Pomona',
state: 'QLD',
};
}
setConfig(config) {
if (!config.suburb || !config.state) {
throw new Error('suburb and state are required');
}
this._config = config;
}
/**
* Dynamic card size based on configuration
* Returns height in units (1 unit = 50px)
*/
getCardSize() {
let size = 4; // Base size for image (200px)
// Add space for metadata if shown above/below
const metadataConfig = this._getMetadataConfig();
if (metadataConfig && typeof metadataConfig !== 'boolean') {
if (metadataConfig.position !== 'overlay') {
size += 1; // +50px for metadata section
}
}
else if (metadataConfig === true) {
size += 1;
}
// Add space for controls if shown below (not overlay)
const controlsConfig = this._getControlsConfig();
if (controlsConfig && typeof controlsConfig !== 'boolean') {
if (controlsConfig.position !== 'overlay') {
size += 2; // +100px for controls
}
}
else if (controlsConfig === true) {
// Default position is 'below', so add space
size += 2;
}
return size;
}
/**
* Define grid options for HA's sections view
* This allows the card to integrate with HA's grid system
*/
getGridOptions() {
// Calculate based on whether controls are visible
const hasControls = this._shouldShowControls();
const hasMetadata = this._shouldShowMetadata();
// Base size: 6 columns (half width), 2 rows
// Adjust based on content
const baseRows = 2;
const additionalRows = (hasControls ? 1 : 0) + (hasMetadata ? 0.5 : 0);
return {
columns: 6, // Default: half width
rows: baseRows + additionalRows,
min_columns: 3, // Minimum: quarter width
min_rows: 2, // Minimum: always show image
max_columns: 12, // Can span full width
max_rows: 8, // Can be tall for detailed view
};
}
/**
* Fetches radar data from the local service via HA integration
* Supports both latest frames and historical timeseries
*/
async fetchRadarData() {
const suburb = encodeURIComponent(this._config.suburb);
const state = encodeURIComponent(this._config.state);
const timespan = this._config.timespan || 'latest';
this.isLoading = true;
const options = {
hass: this.hass,
suburb,
state,
timespan: timespan !== 'latest' ? timespan : undefined,
customStartTime: this._config.custom_start_time,
customEndTime: this._config.custom_end_time,
onError: (error) => {
this.error = error;
this.isLoading = false;
// Auto-retry if applicable
if (error.retryable && error.retryAfter) {
if (this.retryTimer) {
window.clearTimeout(this.retryTimer);
}
this.retryTimer = window.setTimeout(() => {
this.retryTimer = undefined;
this.fetchRadarData();
}, error.retryAfter * 1000);
}
},
};
let data = null;
if (timespan !== 'latest') {
data = await this.apiService.fetchHistoricalFrames(options);
}
else {
data = await this.apiService.fetchLatestFrames(options);
}
if (data) {
this.error = undefined;
this.radarData = data;
this.frames = data.frames.sort((a, b) => a.frameIndex - b.frameIndex);
this.isExtendedMode = timespan !== 'latest';
this.isLoading = false;
this.preloadImages(this.frames);
if (this._config.auto_play !== false && !this.isPlaying) {
this.startAnimation();
}
}
return data;
}
/**
* Gets the current frame image URL
*/
getCurrentFrameUrl() {
if (!this.frames || this.frames.length === 0) {
return null;
}
const frame = this.frames[this.currentFrameIndex];
return (frame === null || frame === void 0 ? void 0 : frame.imageUrl) || null;
}
/**
* Helper to get metadata config
*/
_getMetadataConfig() {
const showMetadata = this._config.show_metadata;
if (showMetadata === undefined) {
return true; // Default: show all metadata
}
return showMetadata;
}
/**
* Helper to get controls config
*/
_getControlsConfig() {
const showControls = this._config.show_controls;
if (showControls === undefined) {
return true; // Default: show all controls
}
return showControls;
}
/**
* Check if metadata should be shown
*/
_shouldShowMetadata() {
const config = this._getMetadataConfig();
return config !== false;
}
/**
* Check if controls should be shown
*/
_shouldShowControls() {
const config = this._getControlsConfig();
return config !== false;
}
/**
* Get locale for formatting
*/
_getLocale() {
var _a, _b;
return this._config.locale || ((_b = (_a = this.hass) === null || _a === void 0 ? void 0 : _a.locale) === null || _b === void 0 ? void 0 : _b.language) || 'en-AU';
}
/**
* Check if frame should be preloaded (only nearby frames)
*/
_shouldPreloadFrame(index) {
const currentIndex = this.currentFrameIndex;
// Preload current, next, and previous frames
return Math.abs(index - currentIndex) <= 1;
}
/**
* Preloads nearby frame images to prevent jiggle when switching
* Cleans up old preloaded images to prevent memory leaks
* Only preloads frames near the current frame for performance
*/
preloadImages(frames) {
// Clean up old preloaded images
this.preloadedImages.forEach(img => {
img.src = '';
img.onload = null;
img.onerror = null;
});
this.preloadedImages = [];
// Only preload nearby frames
frames.forEach((frame, index) => {
if (this._shouldPreloadFrame(index)) {
const img = new Image();
img.src = frame.imageUrl;
this.preloadedImages.push(img);
}
});
}
/**
* Debounce helper
*/
_debounce(func, wait) {
let timeout;
return (...args) => {
window.clearTimeout(timeout);
timeout = window.setTimeout(() => func(...args), wait);
};
}
/**
* Starts the frame animation loop using requestAnimationFrame
* Ensures only one animation timer is active at a time
*/
startAnimation() {
// Always stop existing animation first to prevent accumulation
this.stopAnimation();
if (this.frames.length === 0) {
return;
}
this.isPlaying = true;
const frameInterval = (this._config.frame_interval || DEFAULT_FRAME_INTERVAL) * 1000;
const restartDelay = DEFAULT_RESTART_DELAY;
const maxFrame = this.frames.length - 1;
let lastFrameTime = performance.now();
const animate = (currentTime) => {
// Check if we're still playing (might have been stopped)
if (!this.isPlaying) {
return;
}
// Check if animation was cleared (component disconnected)
if (!this.animationTimer) {
return;
}
const elapsed = currentTime - lastFrameTime;
if (this.currentFrameIndex >= maxFrame) {
// Check if we've waited long enough before restarting
if (elapsed >= restartDelay) {
this.currentFrameIndex = 0;
this.requestUpdate();
lastFrameTime = currentTime;
}
}
else {
// Check if it's time to advance to next frame
if (elapsed >= frameInterval) {
this.currentFrameIndex++;
this.requestUpdate();
lastFrameTime = currentTime;
}
}
// Continue animation
this.animationTimer = requestAnimationFrame(animate);
};
// Start animation
this.animationTimer = requestAnimationFrame(animate);
}
/**
* Stops the animation
*/
stopAnimation() {
if (this.animationTimer) {
cancelAnimationFrame(this.animationTimer);
this.animationTimer = undefined;
}
this.isPlaying = false;
}
/**
* Toggles play/pause
*/
toggleAnimation() {
if (this.isPlaying) {
this.stopAnimation();
}
else {
this.startAnimation();
}
}
/**
* Shows a specific frame
*/
showFrame(index) {
if (index < 0 || index >= this.frames.length) {
return;
}
this.currentFrameIndex = index;
this.requestUpdate();
}
/**
* Navigate to previous frame
*/
previousFrame() {
this.stopAnimation();
const newIndex = this.currentFrameIndex > 0 ? this.currentFrameIndex - 1 : this.frames.length - 1;
this.showFrame(newIndex);
}
/**
* Navigate to next frame
*/
nextFrame() {
this.stopAnimation();
const newIndex = (this.currentFrameIndex + 1) % this.frames.length;
this.showFrame(newIndex);
}
/**
* Jump forward/backward by N frames
*/
jumpFrame(offset) {
const newIndex = Math.max(0, Math.min(this.frames.length - 1, this.currentFrameIndex + offset));
this.showFrame(newIndex);
}
/**
* Auto-refresh data
*/
startAutoRefresh() {
if (this.refreshTimer) {
window.clearInterval(this.refreshTimer);
}
const refreshInterval = (this._config.refresh_interval || DEFAULT_REFRESH_INTERVAL) * 1000;
this.refreshTimer = window.setInterval(() => {
this.fetchRadarData();
}, refreshInterval);
}
/**
* Stops auto-refresh
*/
stopAutoRefresh() {
if (this.refreshTimer) {
window.clearInterval(this.refreshTimer);
this.refreshTimer = undefined;
}
}
async firstUpdated() {
await this.fetchRadarData();
this.startAutoRefresh();
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('keydown', this._handleKeyDown);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('keydown', this._handleKeyDown);
this.stopAnimation();
this.stopAutoRefresh();
// Clean up retry timer
if (this.retryTimer) {
window.clearTimeout(this.retryTimer);
this.retryTimer = undefined;
}
// Clean up preloaded images
this.preloadedImages.forEach(img => {
img.src = '';
img.onload = null;
img.onerror = null;
});
this.preloadedImages = [];
}
updated(changedProperties) {
super.updated(changedProperties);
// Handle config changes
if (changedProperties.has('_config')) {
const oldConfig = changedProperties.get('_config');
if (oldConfig && this._config) {
// If location or timespan changed, refetch data
if (oldConfig.suburb !== this._config.suburb ||
oldConfig.state !== this._config.state ||
oldConfig.timespan !== this._config.timespan ||
oldConfig.custom_start_time !== this._config.custom_start_time ||
oldConfig.custom_end_time !== this._config.custom_end_time) {
this.fetchRadarData();
}
// If refresh interval changed, restart auto-refresh
if (oldConfig.refresh_interval !== this._config.refresh_interval) {
this.startAutoRefresh();
}
// If frame interval changed and playing, restart animation
if (oldConfig.frame_interval !== this._config.frame_interval && this.isPlaying) {
this.stopAnimation();
this.startAnimation();
}
}
}
}
/**
* Render metadata section
*/
_renderMetadata(position) {
var _a, _b, _c;
const config = this._getMetadataConfig();
if (!config || (typeof config === 'boolean' && !config)) {
return '';
}
const displayConfig = typeof config === 'object' ? config : {};
// Default position is 'above' if not specified
const configPosition = (_a = displayConfig.position) !== null && _a !== void 0 ? _a : 'above';
// Only render if position matches
if (configPosition !== position) {
return '';
}
return x `
<bom-metadata-section
.radarData=${this.radarData}
.config=${config}
.position=${position}
.locale=${this._getLocale()}
.timeZone=${(_c = (_b = this.hass) === null || _b === void 0 ? void 0 : _b.config) === null || _c === void 0 ? void 0 : _c.time_zone}
.overlayOpacity=${this._config.metadata_overlay_opacity}
.overlayPosition=${this._config.metadata_overlay_position}
></bom-metadata-section>
`;
}
/**
* Render radar image with zoom and overlay support
*/
_renderRadarImage() {
const zoom = this._config.image_zoom || 1.0;
const fit = this._config.image_fit || 'contain';
const controlsConfig = this._getControlsConfig();
const controlsPosition = typeof controlsConfig === 'object' && controlsConfig.position === 'overlay';
const metadataConfig = this._getMetadataConfig();
const metadataPosition = typeof metadataConfig === 'object' && metadataConfig.position === 'overlay';
const currentFrameUrl = this.getCurrentFrameUrl();
return x `
<div class="radar-image-container" style="--zoom: ${zoom};">
${this.isLoading
? x `
<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="loading-message">Loading radar data...</div>
</div>
`
: currentFrameUrl
? x `
<img
class="radar-image radar-image-${fit}"
src="${currentFrameUrl}"
alt="Radar frame ${this.currentFrameIndex}"
style="transform: scale(${zoom}); transform-origin: center center;"
@error="${() => {
this.error = {
message: 'Failed to load radar image',
type: 'unknown',
retryable: true,
retryAction: () => this.fetchRadarData(),
};
}}"
/>
${controlsPosition ? this._renderControls('overlay') : ''}
${metadataPosition ? this._renderMetadata('overlay') : ''}
`
: ''}
</div>
`;
}
/**
* Render controls section
*/
_renderControls(position) {
var _a, _b;
const config = this._getControlsConfig();
if (!config || (typeof config === 'boolean' && !config)) {
return x ``;
}
const metadataConfig = this._getMetadataConfig();
const showFrameTimes = metadataConfig && typeof metadataConfig === 'object' && metadataConfig.show_frame_times !== false;
return x `
<bom-controls-section
.frames=${this.frames}
.currentFrameIndex=${this.currentFrameIndex}
.isPlaying=${this.isPlaying}
.isExtendedMode=${this.isExtendedMode}
.radarData=${this.radarData}
.config=${config}
.position=${position}
.locale=${this._getLocale()}
.timeZone=${(_b = (_a = this.hass) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.time_zone}
.overlayOpacity=${this._config.controls_overlay_opacity}
.overlayPosition=${this._config.controls_overlay_position}
.showFrameTimes=${showFrameTimes}
.onFrameChange=${(index) => this.showFrame(index)}
.onPrevious=${() => this.previousFrame()}
.onNext=${() => this.nextFrame()}
.onJumpFrame=${(offset) => this.jumpFrame(offset)}
.onToggleAnimation=${() => this.toggleAnimation()}
></bom-controls-section>
`;
}
render() {
var _a, _b;
if (!this._config) {
return x `<hui-warning>Configuration error</hui-warning>`;
}
// Use HA card header if title is configured
const cardTitle = this._config.show_card_title !== false && this._config.card_title
? this._config.card_title
: undefined;
// Render error state
if (this.error) {
return x `
<ha-card .header=${cardTitle} tabindex="0" role="region" aria-label="BOM Radar Card">
<bom-error-display
.error=${this.error}
.locale=${this._getLocale()}
.timeZone=${(_b = (_a = this.hass) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.time_zone}
.onRetry=${() => {
var _a;
if ((_a = this.error) === null || _a === void 0 ? void 0 : _a.retryAction) {
this.error.retryAction();
}
}}
></bom-error-display>
</ha-card>
`;
}
return x `
<ha-card
.header=${cardTitle}
tabindex="0"
role="region"
aria-label="BOM Radar Card"
class=${this._getRootClasses()}
>
<div id="root">
${this._renderMetadata('above')}
${this._renderControls('above')}
${this._renderRadarImage()}
${this._renderMetadata('below')}
${this._renderControls('below')}
</div>
</ha-card>
`;
}
/**
* Get CSS classes for root element
*/
_getRootClasses() {
const classes = [];
const controlsConfig = this._getControlsConfig();
const controlsPosition = typeof controlsConfig === 'object' && controlsConfig.position === 'overlay';
const metadataConfig = this._getMetadataConfig();
const metadataPosition = typeof metadataConfig === 'object' && metadataConfig.position === 'overlay';
if (controlsPosition || metadataPosition) {
classes.push('overlay-enabled');
}
return classes.join(' ');
}
};
BomLocalRadarCard.styles = [
cardStyles,
imageStyles,
controlsStyles,
metadataStyles,
overlayStyles,
errorStyles,
];
__decorate([
n({ attribute: false })
], BomLocalRadarCard.prototype, "hass", void 0);
__decorate([
n({ attribute: false })
], BomLocalRadarCard.prototype, "_config", void 0);
__decorate([
n({ attribute: false })
], BomLocalRadarCard.prototype, "editMode", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "radarData", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "currentFrameIndex", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "isLoading", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "error", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "animationTimer", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "refreshTimer", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "retryTimer", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "isPlaying", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "frames", void 0);
__decorate([
r$1()
], BomLocalRadarCard.prototype, "isExtendedMode", void 0);
BomLocalRadarCard = __decorate([
t$1('bom-local-card')
], BomLocalRadarCard);
if (!customElements.get('bom-local-card')) {
customElements.define('bom-local-card', BomLocalRadarCard);
}
export { BomLocalRadarCard };
//# sourceMappingURL=bom-local-card.js.map