diff --git a/.gitignore b/.gitignore index e1acbbe..805a0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ /build storybook-static -*.d.ts \ No newline at end of file +*.d.ts +LICENSE \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6711c26 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 140 +} \ No newline at end of file diff --git a/README.md b/README.md index 75ff7bf..018468c 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,11 @@ Then there are four 5 icon configuration variables: grid_icon: 'mdi:transmission-tower'; generation_icon: 'mdi:solar-panel-large'; house_icon: 'mdi:home'; +battery_icon: 'mdi:battery'; appliance1_icon: 'mdi:car-sports'; appliance2_icon: 'mdi:car-sports'; ``` -The battery does not have one because the icon changes with the charge and overwriting it has not been implemented. +If you define an extra entity for the battery bubble with the state of charge then the icon will be dynamically replaced with the value of that entity and will override the icon definition above. ### templates for missing sensors or for negative sensors diff --git a/src/TeslaStyleSolarPowerCard.ts b/src/TeslaStyleSolarPowerCard.ts index 413d602..e3a3b76 100644 --- a/src/TeslaStyleSolarPowerCard.ts +++ b/src/TeslaStyleSolarPowerCard.ts @@ -75,6 +75,8 @@ export class TeslaStyleSolarPowerCard extends LitElement { if (this.config.generation_icon == null) this.config.generation_icon = 'mdi:solar-panel-large'; if (this.config.house_icon == null) this.config.house_icon = 'mdi:home'; + if (this.config.battery_icon == null) + this.config.battery_icon = 'mdi:battery-medium'; if (this.config.appliance1_icon == null) this.config.appliance1_icon = 'mdi:car-sports'; if (this.config.appliance2_icon == null) @@ -165,10 +167,10 @@ export class TeslaStyleSolarPowerCard extends LitElement { this.solarCardElements.forEach(solarSensor => { solarSensor.setValueAndUnitOfMeasurement( this.hass.states[solarSensor.entity].state, - this.config.w_not_kw, + this.config.show_w_not_kw, this.hass.states[solarSensor.entity].attributes.unit_of_measurement ); - solarSensor.setSpeed(this.config.w_not_kw); + solarSensor.setSpeed(this.config.show_w_not_kw); }); super.performUpdate(); @@ -178,12 +180,11 @@ export class TeslaStyleSolarPowerCard extends LitElement { protected render(): TemplateResult | void { // TODO Check for stateObj or other necessary things and render a warning if missing // if (this.config.show_warning) return this._showWarning(localize('common.show_warning')); - // if (this.config.show_error) return this._showError(localize('common.show_error')); + if (this.config.show_error) return this._showError('common.show_error'); this.pxRate = this.clientWidth / 100; const half = 22 * this.pxRate; // .label=${`TeslaStyleSolarPowerCard: ${this.config.entity || 'No Entity Defined'}`} - return html`
@@ -401,6 +402,18 @@ export class TeslaStyleSolarPowerCard extends LitElement { ); } + private getHassState(entityName: string) { + const stateValue = this.hass.states[entityName]; + if (stateValue === undefined) { + this._showError( + "Configuration Error: Entity '" + + entityName + + "' not found in HomeAssistant sensors." + ); + } + return stateValue; + } + private animateCircles(obj: any) { requestAnimationFrame(timestamp => { obj.updateAllCircles(timestamp); diff --git a/src/models/TeslaStyleSolarPowerCardConfig.ts b/src/models/TeslaStyleSolarPowerCardConfig.ts index c2ab080..24ef304 100644 --- a/src/models/TeslaStyleSolarPowerCardConfig.ts +++ b/src/models/TeslaStyleSolarPowerCardConfig.ts @@ -9,7 +9,7 @@ export interface TeslaStyleSolarPowerCardConfig extends LovelaceCardConfig { show_warning?: boolean; show_error?: boolean; test_gui?: boolean; - w_not_kw: boolean; + show_w_not_kw: boolean; hide_inactive_lines?: boolean; grid_icon?: string; diff --git a/src/services/HtmlResizeForPowerCard.ts b/src/services/HtmlResizeForPowerCard.ts index 617ef37..376d94f 100644 --- a/src/services/HtmlResizeForPowerCard.ts +++ b/src/services/HtmlResizeForPowerCard.ts @@ -1,4 +1,4 @@ -/* eslint-disable prefer-template, import/extensions, no-param-reassign, class-methods-use-this, lit-a11y/click-events-have-key-events */ +/* eslint-disable func-names, prefer-template, import/extensions, no-param-reassign, class-methods-use-this, lit-a11y/click-events-have-key-events */ import { SensorElement } from '../models/SensorElement'; import { TeslaStyleSolarPowerCard } from '../TeslaStyleSolarPowerCard'; @@ -68,7 +68,7 @@ export class HtmlResizeForPowerCard { .querySelectorAll('.acc_text') .forEach(icontext => { icontext.style['font-size'] = 3 * pxRate + 'px'; - icontext.style['margin-top'] = -1 * pxRate + 'px'; + icontext.style['margin-top'] = -0.5 * pxRate + 'px'; }); teslaCardElement .querySelectorAll('.acc_text_extra') @@ -248,19 +248,23 @@ export class HtmlResizeForPowerCard { ); const topElement = ( - teslaCardElement.querySelector('.acc_top') + teslaCardElement.querySelector('.generation_yield_entity') ); if (topElement === null && value === 1 && selectorElement !== null) { - changeSelectorStyle('.acc_center', 'padding-top', 21 * pxRate + 'px'); + changeSelectorStyle( + '.acc_center_container', + 'margin-top', + 19 * pxRate + 'px' + ); } const bottomElement = ( - teslaCardElement.querySelector('.acc_bottom') + teslaCardElement.querySelector('.battery_consumption_entity') ); if (bottomElement === null && value === 2 && selectorElement !== null) { changeSelectorStyle( - '.acc_center', - 'padding-bottom', - 21 * pxRate + 'px' + '.acc_center_container', + 'margin-bottom', + 19 * pxRate + 'px' ); } }); diff --git a/src/services/htmlWriterForPowerCard.ts b/src/services/htmlWriterForPowerCard.ts index 3669d48..bb22f2a 100644 --- a/src/services/htmlWriterForPowerCard.ts +++ b/src/services/htmlWriterForPowerCard.ts @@ -56,7 +56,7 @@ export class HtmlWriterForPowerCard {
@@ -95,7 +95,6 @@ export class HtmlWriterForPowerCard { batteryValue: number, batteryChargeDischargeValue: number ) { - // called as dynamic function in this.writeCardDiv() let TempSocValue = batteryValue; if (batteryValue <= 5) TempSocValue = 0; diff --git a/tesla-style-solar-power-card.js b/tesla-style-solar-power-card.js index 320e6f2..89d9111 100644 --- a/tesla-style-solar-power-card.js +++ b/tesla-style-solar-power-card.js @@ -183,28 +183,30 @@ const Z=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ -(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");const st={};class nt extends Y{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const t=this.getStyles();if(Array.isArray(t)){const e=(t,i)=>t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),s=[];i.forEach((t=>s.unshift(t))),this._styles=s}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!X){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new et(String(e),tt)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?X?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==st&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return st}}nt.finalized=!0,nt.render=(t,e,s)=>{if(!s||"object"!=typeof s||!s.scopeName)throw new Error("The `scopeName` option is required.");const n=s.scopeName,r=B.has(e),o=q&&11===e.nodeType&&!!e.host,a=o&&!D.has(n),l=a?document.createDocumentFragment():e;if(((t,e,s)=>{let n=B.get(e);void 0===n&&(i(e,e.firstChild),B.set(e,n=new E(Object.assign({templateFactory:O},s))),n.appendInto(e)),n.setValue(t),n.commit()})(t,l,Object.assign({templateFactory:L(n)},s)),a){const t=B.get(l);B.delete(l);const s=t.value instanceof v?t.value.template:void 0;j(n,l,s),i(e,e.firstChild),e.appendChild(l),B.set(e,t)}!r&&o&&window.ShadyCSS.styleElement(e.host)};class rt{constructor(t,e){this.speed=0,this.startPosition=0,this.currentPosition=0,this.currentDelta=0,this.maxPosition=30,this.unitOfMeasurement="",this.accText="",this.accTextclassName="accText",this.entity="",this.color="stroke:var(--info-color)",this.circleColor="var(--primary-color)",this.prevTimestamp=0,this.accTextElement=null,this.entity=t,this.entitySlot=e,this.value=0}setValueAndUnitOfMeasurement(t,e=!1,i){let s=0;if(void 0===t)return void(this.value=s);if(void 0===i)return void(this.value=t);const n=parseFloat(t);if("kW"===i)s=n;else if("W"!==i||e)if("W"===i&&e)s=n;else{if("%"!==i)return void(this.value=t);s=n}else s=n/1e3,i="kW";this.unitOfMeasurement=i,this.value=this.roundValue(s)}roundValue(t){return t=t>.1?(0|Math.round(10*(t+Number.EPSILON)))/10:(0|Math.round(100*(t+Number.EPSILON)))/100}setSpeed(t){this.speed=0,0!=Math.abs(this.value)&&(this.speed=t?rt.SPEEDFACTOR/1e3*this.value:rt.SPEEDFACTOR*this.value)}}rt.SPEEDFACTOR=.04;class ot{constructor(t,e){this.teslaCard=t,this.solarCardElements=t.solarCardElements,this.pxRate=t.pxRate,this.hass=e}writeBubbleDiv(t,e,i,s,n,r,o=null,a=null){return I` -
-
- ${null!==o?I` -
t.reduceRight(((t,i)=>Array.isArray(i)?e(i,t):(t.add(i),t)),i),i=e(t,new Set),s=[];i.forEach((t=>s.unshift(t))),this._styles=s}else this._styles=void 0===t?[]:[t];this._styles=this._styles.map((t=>{if(t instanceof CSSStyleSheet&&!X){const e=Array.prototype.slice.call(t.cssRules).reduce(((t,e)=>t+e.cssText),"");return new et(String(e),tt)}return t}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?X?this.renderRoot.adoptedStyleSheets=t.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map((t=>t.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){const e=this.render();super.update(t),e!==st&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)})))}render(){return st}}nt.finalized=!0,nt.render=(t,e,s)=>{if(!s||"object"!=typeof s||!s.scopeName)throw new Error("The `scopeName` option is required.");const n=s.scopeName,r=B.has(e),o=q&&11===e.nodeType&&!!e.host,a=o&&!D.has(n),l=a?document.createDocumentFragment():e;if(((t,e,s)=>{let n=B.get(e);void 0===n&&(i(e,e.firstChild),B.set(e,n=new E(Object.assign({templateFactory:O},s))),n.appendInto(e)),n.setValue(t),n.commit()})(t,l,Object.assign({templateFactory:L(n)},s)),a){const t=B.get(l);B.delete(l);const s=t.value instanceof v?t.value.template:void 0;j(n,l,s),i(e,e.firstChild),e.appendChild(l),B.set(e,t)}!r&&o&&window.ShadyCSS.styleElement(e.host)};class rt{constructor(t,e){this.speed=0,this.startPosition=0,this.currentPosition=0,this.currentDelta=0,this.maxPosition=30,this.unitOfMeasurement="",this.accText="",this.accTextclassName="accText",this.entity="",this.color="stroke:var(--info-color)",this.circleColor="var(--primary-color)",this.prevTimestamp=0,this.accTextElement=null,this.entity=t,this.entitySlot=e,this.value=0}setValueAndUnitOfMeasurement(t,e=!1,i){let s=0;if(void 0===t)return void(this.value=s);if(void 0===i)return void(this.value=t);const n=parseFloat(t);if("kW"===i)s=n;else if("W"!==i||e)if("W"===i&&e)s=n;else{if("%"!==i)return void(this.value=t);s=n}else s=n/1e3,i="kW";this.unitOfMeasurement=i,this.value=this.roundValue(s)}roundValue(t){return t=t>.1?(0|Math.round(10*(t+Number.EPSILON)))/10:(0|Math.round(100*(t+Number.EPSILON)))/100}setSpeed(t){this.speed=0,0!=Math.abs(this.value)&&(this.speed=t?rt.SPEEDFACTOR/1e3*this.value:rt.SPEEDFACTOR*this.value)}}rt.SPEEDFACTOR=.04;class ot{constructor(t,e){this.teslaCard=t,this.solarCardElements=t.solarCardElements,this.pxRate=t.pxRate,this.hass=e}writeBubbleDiv(t,e,i,s,n,r,o=null,a=null){return I`
+
+ ${null!==o?I`
- ${o} ${a} -
`:I``} - -
- ${i} ${s} -
-
-
`}writeBatteryBubbleDiv(t,e,i,s,n,r,o,a){return void 0!==o&&(r=this.getBatteryIcon(parseFloat(o),i)),this.writeBubbleDiv(t,e,i,s,n,r,o,a)}getBatteryIcon(t,e){let i=t;t<=5&&(i=0);const s=10*Math.ceil(i/10);let n="-"+s.toString(),r="-charging";return e<=0&&(r=""),100===s&&(n=""),s<=5&&(n="-outline"),"mdi:battery"+r+n}writeAppliancePowerLineAndCircle(t,e){if(null==this.solarCardElements.get("appliance"+t+"_consumption_entity"))return I``;let i;return i=1===t?"top:"+22.5*this.pxRate+"px;":"bottom:"+15*this.pxRate+"px;",I` -
+ ${o} ${a} +
`:I``} + +
+ ${i} ${s} +
+
+
`}writeBatteryBubbleDiv(t,e,i,s,n,r,o,a){return void 0!==o&&(r=this.getBatteryIcon(parseFloat(o),i)),this.writeBubbleDiv(t,e,i,s,n,r,o,a)}getBatteryIcon(t,e){let i=t;t<=5&&(i=0);const s=10*Math.ceil(i/10);let n="-"+s.toString(),r="-charging";return e<=0&&(r=""),100===s&&(n=""),s<=5&&(n="-outline"),"mdi:battery"+r+n}writeAppliancePowerLineAndCircle(t,e){if(null==this.solarCardElements.get("appliance"+t+"_consumption_entity"))return I``;let i;return i=1===t?"top:"+22.5*this.pxRate+"px;":"bottom:"+15*this.pxRate+"px;",I`
"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object class="acc_appliance${t}_line_svg" > ${this.writeCircleAndLine("appliance"+t+"_consumption_entity",e)} - -
`}writeCircleAndLine(t,e){const i=this.solarCardElements.get(t);return null==i?I``:I` +
`}writeCircleAndLine(t,e){const i=this.solarCardElements.get(t);return null==i?I``:I` + - - `}_handleClick(t){const e=new Event("hass-more-info",{bubbles:!0,cancelable:!0,composed:!0});e.detail={entityId:t.entity_id},null!=this.teslaCard.shadowRoot&&this.teslaCard.shadowRoot.dispatchEvent(e)}}class at{static changeStylesDependingOnWidth(t,e,i,s){if("complete"!==document.readyState||s===i)return s;if(null==t.shadowRoot)return s;const n=t.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==n)return s;const r=i/100,o=function(t,e,i){const s=n.querySelector(t);null!==s&&(s.style[e]=i)};o(".acc_left","top",12*r+"px"),o(".acc_right","top",12*r+"px"),void 0===e.get("battery_consumption_entity")&&void 0!==e.get("appliance2_consumption_entity")&&o(".acc_center_container","margin-bottom",15*r+"px"),n.querySelectorAll(".acc_container").forEach(((t,e,i)=>{const s=i[e];s.style.height=9*r+"px",s.style.width=9*r+"px",s.style.padding=5*r+"px"})),n.querySelectorAll("ha-icon").forEach(((t,e,i)=>{var s;const n=null===(s=i[e].shadowRoot)||void 0===s?void 0:s.querySelector("ha-svg-icon");null!=n&&(n.style.height=9*r+"px",n.style.width=9*r+"px")})),n.querySelectorAll(".acc_text").forEach((t=>{t.style["font-size"]=3*r+"px",t.style["margin-top"]=-1*r+"px"})),n.querySelectorAll(".acc_text_extra").forEach((t=>{t.style["font-size"]=3*r+"px",t.style.top=1*r+"px",t.style.width=10*r+"px"})),o(".power_lines","height",42*r+"px"),o(".power_lines","width",42*r+"px"),o(".power_lines","top",21*r+"px"),o(".power_lines","left",21*r+"px"),o(".power_lines svg","width",42*r+"px"),o(".power_lines svg","height",42*r+"px"),o(".power_lines svg","viewBox","0 0 "+42*r+" "+42*r);let a=n.querySelector(".power_lines svg");null!==a&&a.setAttribute("viewBox","0 0 "+42*r+" "+42*r);const l=22*r;return o("#generation_to_house_entity_line","d","M"+l+",0 C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#grid_feed_in_entity_line","d","M"+l+",0 C"+l+","+l+" "+l+","+l+" 0,"+l),o("#grid_to_house_entity_line","d","M0,"+l+" C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#grid_to_battery_entity_line","d","M0,"+l+" C"+l+","+l+" "+l+","+l+" "+l+","+2*l),o("#battery_to_house_entity_line","d","M"+l+","+2*l+" C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#generation_to_battery_entity_line","d","M"+l+",0 C"+l+",0 "+l+","+2*l+" "+l+","+2*l),[1,2].forEach((t=>{o(".acc_appliance"+t+"_line svg","viewBox","0 0 "+26*r+" "+26*r),o(".acc_appliance"+t+"_line","right",10*r+"px"),o(".acc_appliance"+t+"_line","width",4*r+"px"),o(".acc_appliance"+t+"_line","height",18*r+"px"),o(".acc_appliance"+t+"_line svg","width",4*r+"px"),o(".acc_appliance"+t+"_line svg","height",18*r+"px"),a=n.querySelector(".acc_appliance"+t+"_line_svg"),null!==a&&a.setAttribute("viewBox","0 0 "+26*r+" "+26*r);null===n.querySelector(".acc_top")&&1===t&&o(".acc_center","padding-top",19*r+"px");null===n.querySelector(".acc_bottom")&&2===t&&o(".acc_center","padding-bottom",19*r+"px")})),o(".acc_appliance1","top","10px"),o(".acc_appliance1_line","top",23*r+"px"),o(".acc_appliance2","bottom","10px"),o(".acc_appliance2_line","bottom",15*r+"px"),i}}window.customCards=window.customCards||[],window.customCards.push({type:"tesla-style-solar-power-card",name:"Tesla Style Solar Power Card",description:"A Solar Power Visualization with svg paths that mimmicks the powerwall app of tesla 2"});class lt extends nt{constructor(){super(...arguments),this.solarCardElements=new Map,this.oldWidth=100,this.pxRate=30,this.htmlWriter=new ot(this,this.hass),this.title="Hey there",this.counter=5}__increment(){this.counter+=1}setConfig(t){let e;t.test_gui,this.config={...t},null==this.config.grid_icon&&(this.config.grid_icon="mdi:transmission-tower"),null==this.config.generation_icon&&(this.config.generation_icon="mdi:solar-panel-large"),null==this.config.house_icon&&(this.config.house_icon="mdi:home"),null==this.config.appliance1_icon&&(this.config.appliance1_icon="mdi:car-sports"),null==this.config.appliance2_icon&&(this.config.appliance2_icon="mdi:air-filter"),this.createSolarCardElements(),e=this,setInterval(this.animateCircles,15,e),e=this}createSolarCardElements(){Object.keys(this.config).forEach((t=>{if(null!=this.config[t]&&t.indexOf("_entity")>5){const e=this.config[t].toString();this.solarCardElements.set(t,new rt(e,t))}}))}getCardSize(){return 5}static getStubConfig(){return{}}async firstUpdated(){await new Promise((t=>setTimeout(t,0))),this.oldWidth=at.changeStylesDependingOnWidth(this,this.solarCardElements,this.clientWidth,this.oldWidth)}connectedCallback(){super.connectedCallback(),this.redraw=this.redraw.bind(this),window.addEventListener("resize",this.redraw)}shouldUpdate(t){let e;e=this,requestAnimationFrame((t=>{e.updateAllCircles(t)})),e=this;let i=!0;return Array.from(t.keys()).some((e=>{const s=t.get(e);return"hass"===e&&s&&(i=i&&this.sensorChangeDetected(s)),!i})),i}sensorChangeDetected(t){let e=!1;return this.solarCardElements.forEach(((i,s)=>{void 0!==this.hass.states[this.config[s]]&&this.hass.states[this.config[s]].state!==t.states[this.config[s]].state&&(e=!0)})),e}async performUpdate(){this.solarCardElements.forEach((t=>{t.setValueAndUnitOfMeasurement(this.hass.states[t.entity].state,this.config.w_not_kw,this.hass.states[t.entity].attributes.unit_of_measurement),t.setSpeed(this.config.w_not_kw)})),super.performUpdate()}render(){this.pxRate=this.clientWidth/100;const t=22*this.pxRate;return I` + id="${t+"_circle"}" + > + + `}_handleClick(t){const e=new Event("hass-more-info",{bubbles:!0,cancelable:!0,composed:!0});e.detail={entityId:t.entity_id},null!=this.teslaCard.shadowRoot&&this.teslaCard.shadowRoot.dispatchEvent(e)}}class at{static changeStylesDependingOnWidth(t,e,i,s){if("complete"!==document.readyState||s===i)return s;if(null==t.shadowRoot)return s;const n=t.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==n)return s;const r=i/100,o=function(t,e,i){const s=n.querySelector(t);null!==s&&(s.style[e]=i)};o(".acc_left","top",12*r+"px"),o(".acc_right","top",12*r+"px"),void 0===e.get("battery_consumption_entity")&&void 0!==e.get("appliance2_consumption_entity")&&o(".acc_center_container","margin-bottom",15*r+"px"),n.querySelectorAll(".acc_container").forEach(((t,e,i)=>{const s=i[e];s.style.height=9*r+"px",s.style.width=9*r+"px",s.style.padding=5*r+"px"})),n.querySelectorAll("ha-icon").forEach(((t,e,i)=>{var s;const n=null===(s=i[e].shadowRoot)||void 0===s?void 0:s.querySelector("ha-svg-icon");null!=n&&(n.style.height=9*r+"px",n.style.width=9*r+"px")})),n.querySelectorAll(".acc_text").forEach((t=>{t.style["font-size"]=3*r+"px",t.style["margin-top"]=-.5*r+"px"})),n.querySelectorAll(".acc_text_extra").forEach((t=>{t.style["font-size"]=3*r+"px",t.style.top=1*r+"px",t.style.width=10*r+"px"})),o(".power_lines","height",42*r+"px"),o(".power_lines","width",42*r+"px"),o(".power_lines","top",21*r+"px"),o(".power_lines","left",21*r+"px"),o(".power_lines svg","width",42*r+"px"),o(".power_lines svg","height",42*r+"px"),o(".power_lines svg","viewBox","0 0 "+42*r+" "+42*r);let a=n.querySelector(".power_lines svg");null!==a&&a.setAttribute("viewBox","0 0 "+42*r+" "+42*r);const l=22*r;return o("#generation_to_house_entity_line","d","M"+l+",0 C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#grid_feed_in_entity_line","d","M"+l+",0 C"+l+","+l+" "+l+","+l+" 0,"+l),o("#grid_to_house_entity_line","d","M0,"+l+" C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#grid_to_battery_entity_line","d","M0,"+l+" C"+l+","+l+" "+l+","+l+" "+l+","+2*l),o("#battery_to_house_entity_line","d","M"+l+","+2*l+" C"+l+","+l+" "+l+","+l+" "+2*l+","+l),o("#generation_to_battery_entity_line","d","M"+l+",0 C"+l+",0 "+l+","+2*l+" "+l+","+2*l),[1,2].forEach((t=>{o(".acc_appliance"+t+"_line svg","viewBox","0 0 "+26*r+" "+26*r),o(".acc_appliance"+t+"_line","right",10*r+"px"),o(".acc_appliance"+t+"_line","width",4*r+"px"),o(".acc_appliance"+t+"_line","height",18*r+"px"),o(".acc_appliance"+t+"_line svg","width",4*r+"px"),o(".acc_appliance"+t+"_line svg","height",18*r+"px"),a=n.querySelector(".acc_appliance"+t+"_line_svg"),null!==a&&a.setAttribute("viewBox","0 0 "+26*r+" "+26*r);null===n.querySelector(".generation_yield_entity")&&1===t&&null!==a&&o(".acc_center_container","margin-top",19*r+"px");null===n.querySelector(".battery_consumption_entity")&&2===t&&null!==a&&o(".acc_center_container","margin-bottom",19*r+"px")})),o(".acc_appliance1","top","10px"),o(".acc_appliance1_line","top",23*r+"px"),o(".acc_appliance2","bottom","10px"),o(".acc_appliance2_line","bottom",15*r+"px"),i}}window.customCards=window.customCards||[],window.customCards.push({type:"tesla-style-solar-power-card",name:"Tesla Style Solar Power Card",description:"A Solar Power Visualization with svg paths that mimmicks the powerwall app of tesla 2"});class lt extends nt{constructor(){super(...arguments),this.solarCardElements=new Map,this.oldWidth=100,this.pxRate=30,this.htmlWriter=new ot(this,this.hass),this.title="Hey there",this.counter=5}__increment(){this.counter+=1}setConfig(t){let e;t.test_gui,this.config={...t},null==this.config.grid_icon&&(this.config.grid_icon="mdi:transmission-tower"),null==this.config.generation_icon&&(this.config.generation_icon="mdi:solar-panel-large"),null==this.config.house_icon&&(this.config.house_icon="mdi:home"),null==this.config.battery_icon&&(this.config.battery_icon="mdi:battery-medium"),null==this.config.appliance1_icon&&(this.config.appliance1_icon="mdi:car-sports"),null==this.config.appliance2_icon&&(this.config.appliance2_icon="mdi:air-filter"),this.createSolarCardElements(),e=this,setInterval(this.animateCircles,15,e),e=this}createSolarCardElements(){Object.keys(this.config).forEach((t=>{if(null!=this.config[t]&&t.indexOf("_entity")>5){const e=this.config[t].toString();this.solarCardElements.set(t,new rt(e,t))}}))}getCardSize(){return 5}static getStubConfig(){return{}}async firstUpdated(){await new Promise((t=>setTimeout(t,0))),this.oldWidth=at.changeStylesDependingOnWidth(this,this.solarCardElements,this.clientWidth,this.oldWidth)}connectedCallback(){super.connectedCallback(),this.redraw=this.redraw.bind(this),window.addEventListener("resize",this.redraw)}shouldUpdate(t){let e;e=this,requestAnimationFrame((t=>{e.updateAllCircles(t)})),e=this;let i=!0;return Array.from(t.keys()).some((e=>{const s=t.get(e);return"hass"===e&&s&&(i=i&&this.sensorChangeDetected(s)),!i})),i}sensorChangeDetected(t){let e=!1;return this.solarCardElements.forEach(((i,s)=>{void 0!==this.hass.states[this.config[s]]&&this.hass.states[this.config[s]].state!==t.states[this.config[s]].state&&(e=!0)})),e}async performUpdate(){this.solarCardElements.forEach((t=>{t.setValueAndUnitOfMeasurement(this.hass.states[t.entity].state,this.config.show_w_not_kw,this.hass.states[t.entity].attributes.unit_of_measurement),t.setSpeed(this.config.show_w_not_kw)})),super.performUpdate()}render(){if(this.config.show_error)return this._showError("common.show_error");this.pxRate=this.clientWidth/100;const t=22*this.pxRate;return I`
${this.writeGenerationIconBubble()} @@ -248,14 +253,14 @@ const Z=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object preserveAspectRatio="xMinYMax slice" style="height:${42*this.pxRate+"px"};width:${42*this.pxRate+"px"}" > - ${this.htmlWriter.writeCircleAndLine("generation_to_house_entity","M"+t+",0 C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} - ${this.htmlWriter.writeCircleAndLine("grid_to_house_entity","M0,"+t+" C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} - ${this.htmlWriter.writeCircleAndLine("generation_to_grid_entity","M"+t+",0 C"+t+","+t+" "+t+","+t+" 0,"+t)} - ${this.htmlWriter.writeCircleAndLine("grid_to_battery_entity","M0,"+t+" C"+t+","+t+" "+t+","+t+" "+t+","+2*t)} - ${this.htmlWriter.writeCircleAndLine("battery_to_grid_entity","M"+t+","+2*t+" C"+t+","+t+" "+t+","+t+" 0,"+t)} - ${this.htmlWriter.writeCircleAndLine("generation_to_battery_entity","M"+t+",0 C"+t+",0 "+t+","+2*t+" "+t+","+2*t)} - ${this.htmlWriter.writeCircleAndLine("battery_to_house_entity","M"+t+","+2*t+" C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} - + ${this.htmlWriter.writeCircleAndLine("generation_to_house_entity","M"+t+",0 C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} + ${this.htmlWriter.writeCircleAndLine("grid_to_house_entity","M0,"+t+" C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} + ${this.htmlWriter.writeCircleAndLine("generation_to_grid_entity","M"+t+",0 C"+t+","+t+" "+t+","+t+" 0,"+t)} + ${this.htmlWriter.writeCircleAndLine("grid_to_battery_entity","M0,"+t+" C"+t+","+t+" "+t+","+t+" "+t+","+2*t)} + ${this.htmlWriter.writeCircleAndLine("battery_to_grid_entity","M"+t+","+2*t+" C"+t+","+t+" "+t+","+t+" 0,"+t)} + ${this.htmlWriter.writeCircleAndLine("generation_to_battery_entity","M"+t+",0 C"+t+",0 "+t+","+2*t+" "+t+","+2*t)} + ${this.htmlWriter.writeCircleAndLine("battery_to_house_entity","M"+t+","+2*t+" C"+t+","+t+" "+t+","+t+" "+2*t+","+t)} +
${this.writeHouseIconBubble()} ${this.writeApplianceIconBubble(1)} @@ -267,7 +272,7 @@ const Z=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?Object
${this.writeBatteryIconBubble()}
- `}writeGenerationIconBubble(){return this.writeIconBubble("generation_yield_entity",["generation_to_grid_entity","generation_to_house_entity","generation_to_battery_entity"],"acc_top","generation_icon","generation_extra_entity")}writeGridIconBubble(){return this.writeIconBubble("grid_consumption_entity",["-generation_to_grid_entity","grid_to_house_entity","-battery_to_grid_entity"],"acc_left","grid_icon","grid_extra_entity")}writeHouseIconBubble(){return this.writeIconBubble("house_consumption_entity",["generation_to_house_entity","grid_to_house_entity","battery_to_house_entity"],"acc_right","house_icon")}writeBatteryIconBubble(){return this.writeIconBubble("battery_consumption_entity",["generation_to_battery_entity","grid_to_battery_entity","-battery_to_house_entity","-battery_to_grid_entity"],"acc_bottom","battery_icon","battery_extra_entity",!0)}writeApplianceIconBubble(t){const e=["appliance"+t+"_consumption_entity"];return this.writeIconBubble("appliance"+t+"_consumption_entity",e,"acc_appliance"+t,"appliance"+t+"_icon","appliance"+t+"_extra_entity")}writeIconBubble(t,e,i,s,n=null,r=!1){if(void 0===this.config[t])return I``;let o,a,l,c=0,h=!1;if(null!==n){const t=this.solarCardElements.get(n);a=null==t?void 0:t.value,l=null==t?void 0:t.unitOfMeasurement}return e.forEach((t=>{"-"===t.substring(0,1)&&(t=t.substring(1),h=!0);const e=this.solarCardElements.get(t);null!==e&&void 0!==(null==e?void 0:e.value)&&(h?c-=null==e?void 0:e.value:c+=null==e?void 0:e.value,c=(100*c|0)/100,o=null==e?void 0:e.unitOfMeasurement),h=!1})),r?this.htmlWriter.writeBatteryBubbleDiv(t,this.hass.states[this.config[t]],c,o,i,this.config[s],a,l):this.htmlWriter.writeBubbleDiv(t,this.hass.states[this.config[t]],c,o,i,this.config[s],a,l)}animateCircles(t){requestAnimationFrame((e=>{t.updateAllCircles(e)}))}updateAllCircles(t){this.solarCardElements.forEach(((e,i)=>{const s=this.solarCardElements.get(i);void 0!==s&&this.updateOneCircle(t,s)}))}updateOneCircle(t,e){if(null==this.shadowRoot)return;const i=this.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==i)return;if(e.line=i.querySelector("#"+e.entitySlot+"_line"),null===e.line)return;const s=e.line.getTotalLength();if(isNaN(s))return;if(e.circle=i.querySelector("#"+e.entitySlot+"_circle"),0===e.speed)return e.circle.setAttribute("visibility","hidden"),void(this.config.hide_inactive_lines&&e.line.setAttribute("visibility","hidden"));e.circle.setAttribute("visibility","visible"),this.config.hide_inactive_lines&&e.line.setAttribute("visibility","visible"),0===e.prevTimestamp&&(e.prevTimestamp=t,e.currentDelta=0),e.currentDelta+=Math.abs(e.speed)*(t-e.prevTimestamp);let n=e.currentDelta/s;e.speed>0?(n>=1||isNaN(n))&&(e.currentDelta=0,n=.01):(n=1-n,(n<=0||isNaN(n))&&(e.currentDelta=0,n=1));const r=e.line.getPointAtLength(s*n);e.circle.setAttributeNS(null,"cx",r.x.toString()),e.circle.setAttributeNS(null,"cy",r.y.toString()),e.prevTimestamp=t}redraw(t){this.hass&&this.config&&"resize"===t.type&&(this.oldWidth=at.changeStylesDependingOnWidth(this,this.solarCardElements,this.clientWidth,this.oldWidth))}_showWarning(t){return I` ${t} `}_showError(t){const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t,origConfig:this.config}),I` ${e} `}static get styles(){return it` + `}writeGenerationIconBubble(){return this.writeIconBubble("generation_yield_entity",["generation_to_grid_entity","generation_to_house_entity","generation_to_battery_entity"],"acc_top","generation_icon","generation_extra_entity")}writeGridIconBubble(){return this.writeIconBubble("grid_consumption_entity",["-generation_to_grid_entity","grid_to_house_entity","-battery_to_grid_entity"],"acc_left","grid_icon","grid_extra_entity")}writeHouseIconBubble(){return this.writeIconBubble("house_consumption_entity",["generation_to_house_entity","grid_to_house_entity","battery_to_house_entity"],"acc_right","house_icon")}writeBatteryIconBubble(){return this.writeIconBubble("battery_consumption_entity",["generation_to_battery_entity","grid_to_battery_entity","-battery_to_house_entity","-battery_to_grid_entity"],"acc_bottom","battery_icon","battery_extra_entity",!0)}writeApplianceIconBubble(t){const e=["appliance"+t+"_consumption_entity"];return this.writeIconBubble("appliance"+t+"_consumption_entity",e,"acc_appliance"+t,"appliance"+t+"_icon","appliance"+t+"_extra_entity")}writeIconBubble(t,e,i,s,n=null,r=!1){if(void 0===this.config[t])return I``;let o,a,l,c=0,h=!1;if(null!==n){const t=this.solarCardElements.get(n);a=null==t?void 0:t.value,l=null==t?void 0:t.unitOfMeasurement}return e.forEach((t=>{"-"===t.substring(0,1)&&(t=t.substring(1),h=!0);const e=this.solarCardElements.get(t);null!==e&&void 0!==(null==e?void 0:e.value)&&(h?c-=null==e?void 0:e.value:c+=null==e?void 0:e.value,c=(100*c|0)/100,o=null==e?void 0:e.unitOfMeasurement),h=!1})),r?this.htmlWriter.writeBatteryBubbleDiv(t,this.hass.states[this.config[t]],c,o,i,this.config[s],a,l):this.htmlWriter.writeBubbleDiv(t,this.hass.states[this.config[t]],c,o,i,this.config[s],a,l)}getHassState(t){const e=this.hass.states[t];return void 0===e&&this._showError("Configuration Error: Entity '"+t+"' not found in HomeAssistant sensors."),e}animateCircles(t){requestAnimationFrame((e=>{t.updateAllCircles(e)}))}updateAllCircles(t){this.solarCardElements.forEach(((e,i)=>{const s=this.solarCardElements.get(i);void 0!==s&&this.updateOneCircle(t,s)}))}updateOneCircle(t,e){if(null==this.shadowRoot)return;const i=this.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==i)return;if(e.line=i.querySelector("#"+e.entitySlot+"_line"),null===e.line)return;const s=e.line.getTotalLength();if(isNaN(s))return;if(e.circle=i.querySelector("#"+e.entitySlot+"_circle"),0===e.speed)return e.circle.setAttribute("visibility","hidden"),void(this.config.hide_inactive_lines&&e.line.setAttribute("visibility","hidden"));e.circle.setAttribute("visibility","visible"),this.config.hide_inactive_lines&&e.line.setAttribute("visibility","visible"),0===e.prevTimestamp&&(e.prevTimestamp=t,e.currentDelta=0),e.currentDelta+=Math.abs(e.speed)*(t-e.prevTimestamp);let n=e.currentDelta/s;e.speed>0?(n>=1||isNaN(n))&&(e.currentDelta=0,n=.01):(n=1-n,(n<=0||isNaN(n))&&(e.currentDelta=0,n=1));const r=e.line.getPointAtLength(s*n);e.circle.setAttributeNS(null,"cx",r.x.toString()),e.circle.setAttributeNS(null,"cy",r.y.toString()),e.prevTimestamp=t}redraw(t){this.hass&&this.config&&"resize"===t.type&&(this.oldWidth=at.changeStylesDependingOnWidth(this,this.solarCardElements,this.clientWidth,this.oldWidth))}_showWarning(t){return I` ${t} `}_showError(t){const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t,origConfig:this.config}),I` ${e} `}static get styles(){return it` #tesla-style-solar-power-card{ margin:auto; display:table; diff --git a/test/batteryWithoutExtra.test.ts b/test/batteryWithoutExtra.test.ts new file mode 100644 index 0000000..3e625a2 --- /dev/null +++ b/test/batteryWithoutExtra.test.ts @@ -0,0 +1,154 @@ +import { expect, assert } from '@open-wc/testing'; +import { LovelaceCardConfig } from 'custom-card-helpers'; +import { setViewport } from '@web/test-runner-commands'; + +import { TeslaStyleSolarPowerCard } from '../src/TeslaStyleSolarPowerCard.js'; +import '../tesla-style-solar-power-card.js'; +import { setCard } from './setters.js'; + +describe('TeslaStyleSolarPowerCard battery tests', () => { + let card: TeslaStyleSolarPowerCard; + let haCard: HTMLElement | null; + let teslaCard: HTMLElement | null | undefined; + let hass: any; + let config: LovelaceCardConfig; + + /** Tests are extended in energy_capable. * */ + + beforeEach(async () => { + config = { + type: 'custom:tesla-style-solar-power-card', + name: 'Powerhouse', + house_consumption_entity: 'sensor.house_consumption', + battery_consumption_entity: 'sensor.battery_consumption', + battery_to_house_entity: 'sensor.battery_consumption', + }; + hass = { + states: { + 'sensor.house_consumption': { + attributes: { + unit_of_measurement: 'W', + friendly_name: 'House consumption', + }, + entity_id: 'sensor.house_consumption', + state: '1300', + }, + 'sensor.battery_consumption': { + attributes: { + unit_of_measurement: 'W', + }, + entity_id: 'battery_consumption', + state: '1300', + }, + 'sensor.battery_to_house': { + attributes: { + unit_of_measurement: 'W', + }, + entity_id: 'sensor.battery_to_house', + state: '1300', + }, + }, + }; + await setViewport({ width: 1200, height: 1000 }); + card = await setCard(hass, config); + // let iframe = document.createElement('iframe'); + // document.body.appendChild(iframe); + // let div = document.createElement('div'); + // document.body. + // console.log("document width " + document.body.clientWidth); + // document.body.appendChild(card); + if (card.shadowRoot === null) assert.fail('No Card Shadowroot'); + haCard = card.shadowRoot.querySelector('ha-card'); + if (haCard === null || haCard === undefined) assert.fail('No ha-card'); + teslaCard = ( + haCard.querySelector('#tesla-style-solar-power-card') + ); + if (teslaCard === null || teslaCard === undefined) + assert.fail('No tesla-style-card'); + }); + + it('has house_consumption_entity, text and icon', async () => { + const houseEntity = teslaCard?.querySelector('.house_consumption_entity'); + if (houseEntity === null || houseEntity === undefined) + assert.fail('No house_consumption_entity element found'); + expect(houseEntity?.querySelector('.acc_text')?.innerHTML).contains( + '1.3 kW' + ); + expect( + houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString() + ).to.equal('mdi:home'); + }); + + it('has battery_consumption_entity, text and icon', async () => { + const batteryEntity = teslaCard?.querySelector( + '.battery_consumption_entity' + ); + if (batteryEntity === null || batteryEntity === undefined) + assert.fail('No battery_consumption_entity element found'); + expect(batteryEntity?.querySelector('.acc_text')?.innerHTML).contains( + '1.3 kW' + ); + expect( + batteryEntity + ?.querySelector('.acc_icon') + ?.getAttribute('icon') + ?.toString() + ).to.equal('mdi:battery-medium'); + }); + + it('has battery to house consumption line and circle', async () => { + // await setCardConsumingFromGrid(); + const batteryToHouseLine = teslaCard?.querySelector( + '#battery_to_house_entity_line' + ); + if (batteryToHouseLine === null || batteryToHouseLine === undefined) { + assert.fail('No battery_to_house_line element found'); + } + const batteryToHouseCircle = teslaCard?.querySelector( + '#battery_to_house_entity_circle' + ); + if (batteryToHouseCircle === null || batteryToHouseCircle === undefined) { + assert.fail('No batter_to_house_entity_circle element found'); + } + + if (haCard === null || haCard === undefined) assert.fail('No ha-card'); + expect(batteryToHouseLine?.getAttribute('hidden')).to.equal(null); + }); + + it('has no pv, grid or appliance icons', async () => { + const pvEntity = teslaCard?.querySelector('.pv_consumption_entity'); + if (pvEntity !== null) + assert.fail('No pv_consumption_entity element found'); + + const gridEntity = teslaCard?.querySelector('.grid_consumption_entity'); + if (gridEntity !== null) + assert.fail('No battery_consumption_entity element found'); + + const appliance1Entity = teslaCard?.querySelector( + '.appliance1_consumption_entity' + ); + if (appliance1Entity !== null) + assert.fail('No appliance1_consumption_entity element found'); + + const appliance2Entity = teslaCard?.querySelector( + '.appliance2_consumption_entity' + ); + if (appliance2Entity !== null) + assert.fail('No appliance2_consumption_entity element found'); + }); + + it('has battery icon', async () => { + card.requestUpdate(); + const batteryEntity = teslaCard?.querySelector( + '.battery_consumption_entity' + ); + if (batteryEntity === null || batteryEntity === undefined) + assert.fail('No battery_consumption_entity element found'); + expect( + batteryEntity + ?.querySelector('.acc_icon') + ?.getAttribute('icon') + ?.toString() + ).to.equal('mdi:battery'); + }); +});