Showing preview only (214K chars total). Download the full file or copy to clipboard to get everything.
Repository: reptilex/tesla-style-solar-power-card
Branch: master
Commit: 4881d8e2e1f1
Files: 40
Total size: 202.2 KB
Directory structure:
gitextract_1qkmhazr/
├── .editorconfig
├── .github/
│ └── workflows/
│ └── validate.yaml
├── .gitignore
├── LICENSE-2.0.txt
├── README.md
├── hacs.json
├── package.json
├── rollup.config.js
├── src/
│ ├── TeslaStyleSolarPowerCard.ts
│ ├── components/
│ │ └── editor.ts
│ ├── localize/
│ │ ├── languages/
│ │ │ ├── de.json
│ │ │ └── en.json
│ │ └── localize.ts
│ ├── models/
│ │ ├── BubbleData.ts
│ │ ├── SensorElement.ts
│ │ └── TeslaStyleSolarPowerCardConfig.ts
│ ├── services/
│ │ ├── HtmlResizeForPowerCard.ts
│ │ └── htmlWriterForPowerCard.ts
│ ├── translations/
│ │ ├── languages/
│ │ │ ├── de.json
│ │ │ └── en.json
│ │ └── localize.ts
│ └── types.ts
├── tesla-style-solar-power-card.js
├── tesla-style-solar-power-card.ts
├── test/
│ ├── SensorElement.test.ts
│ ├── battery.test.ts
│ ├── batteryCharging.test.ts
│ ├── batteryWithoutExtra.test.ts
│ ├── colouringOfBubblesDependingOnProduction.test.ts
│ ├── defaultConfig.test.ts
│ ├── extraAppliances.test.ts
│ ├── extraAppliancesNotInHouse.test.ts
│ ├── gridFeed.test.ts
│ ├── setters.ts
│ ├── solarProduction.test.ts
│ ├── tesla-style-solar-power-card.test.ts
│ └── threshold.test.ts
├── tsconfig.json
├── web-dev-server.config.mjs
└── web-test-runner.config.mjs
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.json]
indent_size = 2
[*.{html,js,md}]
block_comment_start = /**
block_comment = *
block_comment_end = */
================================================
FILE: .github/workflows/validate.yaml
================================================
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- name: HACS validation
uses: "hacs/integration/action@main"
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CATEGORY: "plugin"
================================================
FILE: .gitignore
================================================
## editors
/.idea
/.vscode
## system files
.DS_Store
## npm
/node_modules/
/npm-debug.log
## testing
/coverage/
## temp folders
/.tmp/
# build
/_site/
/dist/
/out-tsc/
/build
storybook-static
*.d.ts
LICENSE
================================================
FILE: LICENSE-2.0.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# tesla-style-solar-power-card
> **⚠ WARNING: BREAKING CONFIG CHANGE**
> **You have to define the FLOWS AGAIN!!**
> Without defining each flow no line will show, read the usage part carefully (the bubbles can be clickable but this is optional)
This is a [home-assistant](home-assistant.io) card for solar installations. It provides a tesla style graphic to see the flows of energy ((k)W).
### Table of contents
* [Concept](#concept)
* [HACS-Installation](#hacs-installation)
* [Usage](#usage)
* [Tesla Powerwall Usage](#tesla-powerwall-usage)
* [Contributing](#contributing)

## Concept
I have tried to make it as generic as possible, for now there are 6 bubbles with 4 main icons and 2 extra appliances. The Four main icon values are controlled by the sum of the flows from and to them:
1. Grid
2. Generation (usually solar)
3. House
4. Battery
The two optional appliances can be any consumer in the house, they are attached to the house. These two are controlled directly by their consumption. Meaning no flow sum is done.
* appliance1_consumption_entity (car/heater ...)
* appliance2_consumption_entity (car2/oven ...)
The bubbles/icons can be configured to have an entity when clicked, but the numbers are calculated from the flows. You can show an extra entity text/value on the top part of the bubble.
There are 7 main flows and 2 appliance flows. The main flows are:
* generation_to_grid_entity
* generation_to_battery_entity
* generation_to_house_entity
* grid_to_battery_entity
* grid_to_house_entity
* battery_to_grid_entity
* battery_to_house_entity
You need at least one, the placement of the main bubbles is fixed for now. Some will substract the value from one bubble and will add value to another bubble. For example:
battery_to_house will substract from the battery bubble/icon and add to house bubble/icon.
## Optional clickable entities
The clickable entities can be configured through these entities but are optional:
* grid_entity
* generation_entity (solar/wind ...)
* battery_entity
* house_entity
This card started based on the card from [bessarabov animated consumption card](https://github.com/bessarabov/animated-consumption-card), thanks again for that work. Then was rewritten completely taking [boilerplate card](https://github.com/custom-cards/boilerplate-card) as a starting point but with typescript. I also borrowed a few ideas from [power-wheel-card](https://github.com/gurbyz/power-wheel-card) sadly not yet as many as I would like ;)
## Optional extra entities
On top of the flows and clickable entity every bubble can have an extra value on top. To define those you need to add a sensor to any of theses entities:
* battery_extra_entity
* house_extra_entity
* generation_extra_entity
* grid_extra_entity
I always have the battery current charge as the battery_extra_entity. In this case the battery icon will also change with the charge.
# HACS-Installation
1. [install HACS](https://hacs.xyz/docs/installation/installation) if you don't have it yet
2. When installed go to HACS->Frontend->Explore & add repositories
3. search for "tesla style"
4. click on the tesla-style-solar-power-card
5. Install repository
6. Restart HA
### Manual Installation (hacs will do all this for you)
1. Add the card js file from the repo under your home assistant config in the www folder (create one if you don't have it yet).
2. Add a resource under lovelace (you have to enable advanced Mode in your user profile to see the resource tab([see here for this card](https://github.com/reptilex/tesla-style-solar-power-card/blob/master/add-card-resource.png)).
3. restart home assistant.
4. add a manual card with the lovelace gui and configure as seen below.
# Usage
### Just a grid a house and a line
Currently I have no minimum configuration, but some combinations might not make sense. I would advice to use the bubbles you want and the flows linked to the one's you are using. The left part of these examples is fixed, change the right part with your own sensors. There are no required entities, though your configuration can show strange results if you leave some combinations out. The sensor can be called whatever you want, they are powermeter sensors in Watt or Kilowatt (choose the same for all, it will create kw from it). __ALL SENSORS NEED TO BE POSITIVE VALUES__
A simple combination example:
```yml
type: 'custom:tesla-style-solar-power-card'
grid_consumption_entity: sensor.grid_consumption
house_consumption_entity: sensor.house_consumption
grid_to_house_entity: sensor.grid_consumption
```
This will allow you to have two bubbles that are clickable and the flow from grid to house, which will determine the values beneath the icons.
### Complete example with all details
```yml
type: 'custom:tesla-style-solar-power-card'
name: My Flows
# 7 flows between bubbles
grid_to_house_entity: sensor.grid_consumption
grid_to_battery_entity: sensor.grid_battery_charge
generation_to_grid_entity: sensor.grid_feed_in
generation_to_battery_entity: sensor.battery_charging
generation_to_house_entity: sensor.solar_consumption
battery_to_house_entity: sensor.battery_consumption
battery_to_grid_entity: sensor.battery_to_grid
# extra values to show as text above icons
battery_extra_entity: sensor.battery_charge
house_extra_entity: sensor.current_temperature
generation_extra_entity: sensor.percent_cloud_coverage
grid_extra_entity: sensor.monthly_feed_in
# optional appliances with consumption and extra values
appliance1_consumption_entity: sensor.car_consumption
appliance1_extra_entity: sensor.car_battery_state_of_charge
appliance2_consumption_entity: sensor.heating_consumption
appliance2_extra_entity: sensor.heating_operation
# optional 4 main bubble icons for clickable entities
grid_entity: sensor.grid_consumption
house_entity: sensor.house_consumption
generation_entity: sensor.solar_yield
battery_entity: sensor.battery_consumption
```
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.
There a few configuration variables that change the behaviour:
Heading:
```yml
name: 'My Tesla Power Card!'
```
One to force W (Watt) instead of kW, set it to 1 to use it:
```yml
show_w_not_kw: 1
```
One to set a different speed for the moving dots, normal speed factor is 0.04 so stay near that number at first, 0.2 is really fast:
```yml
speed_factor: 0.03
```
One for the threshold from which W is converted to kW (the example below will change W into kilowatt from 5000 W onwards):
```yml
threshold_in_k: 5
```
threshold_in_k is not compatible with show_w_not_kw, the latter will overrule the threshold_in_k
One to hide the lines not active to use it, please make sure everything is working before you hide the lines:
```yml
hide_inactive_lines: 1
```
One to add gaps for the power lines the way the energy panel from ha does it:
```yml
show_gap: true
```
One to colour the house bubble depending on the highest flow:
```yml
change_house_bubble_color_with_flow: 1
```
One to not show moving circles but an energy flow diagramm (thicker lines when flow is higher):
```yml
energy_flow_diagramm: 1
```
There is a factor to make the lines thicker depending on your flow normaly it's 2:
```yml
energy_flow_diagramm_line_factor: 2
```
You can subtract the appliance values from the house value without affecting the line flow:
```yml
house_without_appliances_values: 1
```
Then there are 6 icon configuration variables:
```yml
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'
```
### templates for missing sensors or for negative sensors
Remember you can create
template sensors if you are missing one like solar yield out of solar_consumption and grid_feed_in or if you are missing another one like home_consumption. Some inverters have positive and negative values, here all sensors need to be positive values, so create template sensors like:
```yml
battery_consumption:
value_template: '{% set batter_cons = sensor.powerwall_battery_now | int %}
{% if batter_cons > 0 %}
{{ batter_cons | int }}
{% else %}
0
{% endif %}'
device_class: power
unit_of_measurement: W
```
# Tesla-Powerwall-Usage
In order to use this card with the [Tesla Powerwall integration](https://www.home-assistant.io/integrations/powerwall/) you will need to create some additional sensors first. This card expects an entity with a positive numeric value per line shown on the screen. However the Tesla Powerwall integration creates sensors which go negative or positive depending on whether energy is being consumed from or feed into that particular meter.
Fortunately this can be easily fixed with the addition of a few template sensors, the ones you would need to add are shown below. Note that these sensors assume the default names for each entity created by the Tesla Powerwall integration, if you've changed the names of your entities then you'll need to adjust the config accordingly:
```yaml
# Templates for Actual Powerflow transfer charts (APF - Actual PowerFlow)
#
# For the math to add up a new Real House Load must be calculated and used, witch includes
# the inverter consumption and excludes rounding errors and corrects inaccurate power readings.
#
# It never made sense that inbound power sometimes does not equal outbound power. This fixes it!
#
# Developed by AviadorLP modified for powerwall by purcell-lab
# Correctly sets battery2grid & grid2battery flows
#
template:
- sensor:
# grid sensor must be negative when importing and positive when exporting
- name: APF Grid Entity
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ (0 - states('sensor.powerwall_site_now')|float(0)*1000)|int(0) }}"
# sensor must always be 0 or positive (i think they always are)
- name: APF House Entity
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ (states('sensor.powerwall_load_now')|float(0)*1000)|int(0) }}"
# sensor must always be 0 or positive (i think they always are)
- name: APF Generation Entity
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ (states('sensor.powerwall_solar_now')|float(0)*1000)|int(0) }}"
# battery sensor must be positive when charging and negative when discharging
- name: APF Battery Entity
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ (0 - states('sensor.powerwall_battery_now')|float(0)*1000)|int(0) }}"
# Required to reduce code later on
- name: APF Grid Import
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_grid_entity')|int(default=0) < 0 %}
{{ states('sensor.apf_grid_entity')|int(default=0)|abs }}
{% else %}
0
{% endif %}
# Inverter consumption and power losses due to Inverter transfers and power conversions (AC/DC)
# excludes rounding errors made worst by the fact that some inverters round all sensors readings to INT
# Occasionally this might be negative probably due to cumulative errors in not so accurate power readings.
- name: APF Inverter Power Consumption
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ states('sensor.apf_generation_entity')|int(default=0) - states('sensor.apf_battery_entity')|int(default=0) - states('sensor.apf_house_entity')|int(default=0) - states('sensor.apf_grid_entity')|int(default=0) }}"
# Real House Load Includes Inverter consumption and transfer conversions and losses and rounding errors.
# It never made sense that inbound power sometimes does not equal outbound power. This fixes it!
- name: APF Real House Load
device_class: power
state_class: measurement
unit_of_measurement: W
state: "{{ states('sensor.apf_house_entity')|int(default=0) + states('sensor.apf_inverter_power_consumption')|int(default=0) }}"
icon: mdi:home-lightning-bolt
- name: APF Grid2House
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_grid_import')|int(default=0) > states('sensor.apf_real_house_load')|int(default=0) %}
{{ states('sensor.apf_real_house_load')|int(default=0) }}
{% else %}
{{ states('sensor.apf_grid_import')|int(default=0) }}
{% endif %}
- name: APF Grid2Batt
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_grid_import')|int(default=0) > states('sensor.apf_real_house_load')|int(default=0) %}
{{ states('sensor.apf_grid_import')|int(default=0) - states('sensor.apf_real_house_load')|int(default=0) }}
{% else %}
0
{% endif %}
- name: APF Batt2House
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_battery_entity')|int(default=0) < 0 %}
{% if states('sensor.apf_battery_entity')|int(default=0)|abs > states('sensor.apf_real_house_load')|int(default=0) %}
{{ states('sensor.apf_real_house_load')|int(default=0) }}
{% else %}
{{ states('sensor.apf_battery_entity')|int(default=0)|abs }}
{% endif %}
{% else %}
0
{% endif %}
# This might be called house to grid, and can happen in rare circumstances,
# like when the inverter is not able to do a precise adjustment of power fast enough
# or when you want to force a discharge of the battery or something...
# But it only happens with battery or other power generator users.
- name: APF Batt2Grid
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_battery_entity')|int(default=0) < 0 %}
{% if states('sensor.apf_battery_entity')|int(default=0)|abs > states('sensor.apf_real_house_load')|int(default=0) %}
{{ states('sensor.apf_battery_entity')|int(default=0)|abs - states('sensor.apf_real_house_load')|int(default=0) }}
{% else %}
0
{% endif %}
{% else %}
0
{% endif %}
- name: APF Solar2Grid
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_grid_entity')|int(default=0) > states('sensor.apf_batt2grid')|int(default=0) %}
{{ states('sensor.apf_grid_entity')|int(default=0) - states('sensor.apf_batt2grid')|int(default=0) }}
{% else %}
0
{% endif %}
- name: APF Solar2House
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_generation_entity')|int(default=0) > 0 and states('sensor.apf_real_house_load')|int(default=0) > states('sensor.apf_batt2house')|int(default=0) + states('sensor.apf_grid_import')|int(default=0) %}
{% if states('sensor.apf_generation_entity')|int(default=0) > states('sensor.apf_real_house_load')|int(default=0) - states('sensor.apf_batt2house')|int(default=0) - states('sensor.apf_grid2house')|int(default=0) %}
{{ states('sensor.apf_real_house_load')|int(default=0) - states('sensor.apf_batt2house')|int(default=0) - states('sensor.apf_grid2house')|int(default=0) }}
{% else %}
{{ states('sensor.apf_generation_entity')|int(default=0) }}
{% endif %}
{% else %}
0
{% endif %}
- name: APF Solar2Batt
device_class: power
state_class: measurement
unit_of_measurement: W
state: >
{% if states('sensor.apf_generation_entity')|int(default=0) > 0 and states('sensor.apf_battery_entity')|int(default=0) > 0 %}
{% if states('sensor.apf_battery_entity')|int(default=0) > states('sensor.apf_grid2batt')|int(default=0) %}
{% if states('sensor.apf_generation_entity')|int(default=0) - states('sensor.apf_solar2house')|int(default=0) > states('sensor.apf_battery_entity')|int(default=0) - states('sensor.apf_grid2batt')|int(default=0) %}
{{ states('sensor.apf_battery_entity')|int(default=0) - states('sensor.apf_grid2batt')|int(default=0) }}
{% else %}
{{ states('sensor.apf_generation_entity')|int(default=0) - states('sensor.apf_solar2house')|int(default=0) - states('sensor.apf_solar2grid')|int(default=0) }}
{% endif %}
{% else %}
0
{% endif %}
{% else %}
0
{% endif %}
```
After you've included these sensors then you can configure the card like this:
```yaml
type: 'custom:tesla-style-solar-power-card'
grid_entity: sensor.apf_grid_entity
house_entity: sensor.apf_real_house_load
generation_entity: sensor.apf_generation_entity
battery_entity: sensor.apf_battery_entity
grid_to_house_entity: sensor.apf_grid2house
grid_to_battery_entity: sensor.apf_grid2batt
generation_to_grid_entity: sensor.apf_solar2grid
generation_to_battery_entity: sensor.apf_solar2batt
generation_to_house_entity: sensor.apf_solar2house
battery_to_house_entity: sensor.apf_batt2house
battery_to_grid_entity: sensor.apf_batt2grid
battery_extra_entity: sensor.powerwall_charge
```
## Releases
v0.9
v0.92
vbeta1.1.
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
Apache License V 2.0
================================================
FILE: hacs.json
================================================
{
"name": "Tesla style solar power card",
"content_in_root": true,
"filename": "tesla-style-solar-power-card.js",
"render_readme": true
}
================================================
FILE: package.json
================================================
{
"name": "tesla-style-solar-power-card",
"version": "0.0.0",
"description": "Webcomponent tesla-style-solar-power-card following open-wc recommendations",
"author": "tesla-style-solar-power-card",
"license": "Apache-2.0",
"main": "dist/index.js",
"module": "dist/index.js",
"scripts": {
"start": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wds\"",
"build": "rimraf dist && tsc && rollup -c rollup.config.js",
"prepublish": "tsc",
"lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore",
"format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore",
"test": "tsc && wtr --coverage",
"test:watch": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch\""
},
"dependencies": {
"custom-card-helpers": "^1.7.0",
"home-assistant-js-websocket": "^5.7.0",
"lit": "^2.2.1"
},
"devDependencies": {
"@babel/eslint-plugin": "^7.12.13",
"@open-wc/building-rollup": "^2.0.1",
"@open-wc/eslint-config": "^4.2.0",
"@open-wc/testing": "^3.1.2",
"@rollup/plugin-node-resolve": "^13.1.3",
"@types/convert-source-map": "^1.5.1",
"@types/istanbul-reports": "^3.0.0",
"@types/mocha": "^9.1.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"@web/dev-server": "^0.1.5",
"@web/dev-server-legacy": "^0.1.7",
"@web/test-runner": "^0.13.27",
"@web/test-runner-commands": "^0.6.1",
"concurrently": "^7.1.0",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^7.2.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.7",
"prettier": "^2.6.2",
"rimraf": "^3.0.2",
"rollup": "^2.45.1",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.2",
"sinon": "^13.0.1",
"tslib": "^2.1.0",
"typescript": "^4.1.3"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"@open-wc/eslint-config",
"eslint-config-prettier"
],
"plugins": [
"@typescript-eslint"
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error"
],
"import/no-unresolved": "off",
"import/extensions": [
"error",
"always",
{
"ignorePackages": true
}
]
}
},
"prettier": {
"singleQuote": true,
"arrowParens": "avoid"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.ts": [
"eslint --fix",
"prettier --write --print-width 140"
]
}
}
================================================
FILE: rollup.config.js
================================================
import typescript from 'rollup-plugin-typescript2'
import resolve from '@rollup/plugin-node-resolve'
import {terser} from 'rollup-plugin-terser';
import pkg from './package.json';
export default {
input: 'tesla-style-solar-power-card.ts',
output: [
{
file: pkg.main,
format: 'cjs',
},
{
file: pkg.module,
format: 'es',
},
{
file: 'tesla-style-solar-power-card.js',
format: 'iife',
name: 'version',
plugins: [terser()]
}
],
plugins: [
resolve(),
typescript(),
],
}
================================================
FILE: src/TeslaStyleSolarPowerCard.ts
================================================
/* eslint-disable no-restricted-globals, prefer-template, no-param-reassign, class-methods-use-this, lit-a11y/click-events-have-key-events, no-bitwise, import/extensions */
import { LitElement, html, TemplateResult, CSSResult, css } from 'lit';
import { property } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardConfig /* , LovelaceCardEditor */ } from 'custom-card-helpers';
import { TeslaStyleSolarPowerCardConfig } from './models/TeslaStyleSolarPowerCardConfig';
/* import './components/editor'; */
import { SensorElement } from './models/SensorElement';
import { BubbleData } from './models/BubbleData';
import { HtmlWriterForPowerCard } from './services/HtmlWriterForPowerCard';
import { HtmlResizeForPowerCard } from './services/HtmlResizeForPowerCard';
// import { localize } from './localize/localize';
// This puts your card into the UI card picker dialog
(window as any).customCards = (window as any).customCards || [];
(window as any).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',
});
export class TeslaStyleSolarPowerCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private config!: TeslaStyleSolarPowerCardConfig;
@property({ attribute: false }) public solarCardElements: Map<string, SensorElement> = new Map();
@property() private oldWidth = 100;
public pxRate = 4;
private teslaCardElement?: HTMLElement;
private htmlWriter: HtmlWriterForPowerCard = new HtmlWriterForPowerCard(this, this.hass);
@property({ type: String }) title = 'Hey there';
@property({ type: Number }) counter = 5;
__increment() {
this.counter += 1;
}
private error: string = '';
public setConfig(config: LovelaceCardConfig): void {
if (!config) {
// throw new Error(localize('common.invalid_configuration'));
}
if (config.test_gui) {
// getLovelace().setEditMode(true);
}
this.config = {
...config,
};
if (this.config.grid_icon == null) this.config.grid_icon = 'mdi:transmission-tower';
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) this.config.appliance2_icon = 'mdi:air-filter';
if (this.config.speed_factor == null) this.config.speed_factor = 0.04;
this.createSolarCardElements();
if (!this.config.energy_flow_diagramm) {
const obj = this;
setInterval(this.animateCircles, 15, obj);
}
}
private createSolarCardElements(): void {
Object.keys(this.config).forEach(key => {
if (this.config[key] != null && key.indexOf('_entity') > 5) {
// only filled entity config elements
const sensorName = this.config[key].toString();
this.solarCardElements.set(key, new SensorElement(sensorName, key));
}
});
}
public getCardSize() {
return 5;
}
/*
public static async getConfigElement(): Promise<LovelaceCardEditor> {
return document.createElement('tesla-style-solar-power-card-editor');
}
*/
public static getStubConfig(): Record<string, any> {
return {};
}
/* ** LitElement process functions ** */
async firstUpdated(): Promise<void> {
// Give the browser a chance to paint
await new Promise(r => setTimeout(r, 0));
const realWidth = this.getBoundingClientRect().width
this.oldWidth = HtmlResizeForPowerCard.changeStylesDependingOnWidth(this, this.solarCardElements, realWidth, this.oldWidth);
}
public connectedCallback(): void {
super.connectedCallback();
this.redraw = this.redraw.bind(this);
window.addEventListener('resize', this.redraw);
}
public shouldUpdate(changedProperties: any): boolean {
let obj: any;
obj = this;
if (!this.config.energy_flow_diagramm) {
requestAnimationFrame(timestamp => {
obj.updateAllCircles(timestamp);
});
}
obj = this;
// Update only when our values in hass changed
let update = true;
Array.from(changedProperties.keys()).some((propName: any) => {
const oldValue = changedProperties.get(propName);
if (propName === 'hass' && oldValue) {
update = update && this.sensorChangeDetected(oldValue);
}
return !update;
});
return update;
}
private sensorChangeDetected(oldValue: any): boolean {
let change = false;
this.solarCardElements.forEach((_solarSensor, key) => {
if (
this.hass.states[this.config[key]] !== undefined &&
this.hass.states[this.config[key]].state !== oldValue.states[this.config[key]].state
) {
change = true;
}
});
return change;
}
public async performUpdate(): Promise<void> {
this.error = '';
this.solarCardElements.forEach(solarSensor => {
try {
solarSensor.setValueAndUnitOfMeasurement(
this.hass.states[solarSensor.entity].state,
this.hass.states[solarSensor.entity].attributes.unit_of_measurement
);
solarSensor.setSpeed(this.config.speed_factor);
} catch (err) {
this.error += " Configured '" + solarSensor.entity + "' entity was not found. ";
}
});
if (this.config.energy_flow_diagramm) {
this.setEnergyFlowDiagramm();
}
if (this.config.change_house_bubble_color_with_flow) {
this.colourHouseBubbleDependingOnHighestInput();
}
super.performUpdate();
}
/* **** render functions ****** */
protected render(): TemplateResult | void {
if (this.error !== '') return this._showError();
let newWidth = this.getBoundingClientRect().width;
if(newWidth < 200) newWidth = 250;
this.pxRate = newWidth / 100;
let gap: number;
if (this.config.show_gap !== undefined && this.config.show_gap) {
gap = 2 * this.pxRate;
} else {
gap = 0;
}
const half = 22 * this.pxRate;
//
return html`
<ha-card .header=${this.config.name} tabindex="0">
<div id="tesla-style-solar-power-card">
${this.writeGenerationIconBubble()}
<div class="acc_center">
<div class="acc_center_container">
${this.writeGridIconBubble()}
<div
class="acc_line power_lines"
style="
height:${42 * this.pxRate + 'px'};
width:${42 * this.pxRate + 'px'};
top:${0 * this.pxRate + 'px'};
left:${28 * this.pxRate + 'px'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="${'0 0 ' + 42 * this.pxRate + ' ' + 42 * this.pxRate}"
preserveAspectRatio="xMinYMax slice"
style="height:${42 * this.pxRate + 'px'};width:${42 * this.pxRate + 'px'}"
>
${this.htmlWriter.writeCircleAndLine(
'generation_to_house_entity',
'M' +
(half - this.pxRate + gap) +
',0' +
'C' +
(half - this.pxRate + gap) +
',' +
(half - gap) +
' ' +
(half - this.pxRate + gap) +
',' +
(half - gap) +
' ' +
half * 2 +
',' +
(half - gap)
)}
${this.htmlWriter.writeCircleAndLine(
'grid_to_house_entity',
'M0,' +
half +
' ' +
'C' +
(half - this.pxRate) +
',' +
half +
' ' +
(half - this.pxRate) +
',' +
half +
' ' +
(half - this.pxRate) * 2 +
',' +
half
)}
${this.htmlWriter.writeCircleAndLine(
'generation_to_grid_entity',
'M' +
(half - this.pxRate - gap) +
',0 ' +
'C' +
(half - this.pxRate - gap) +
',' +
(half - gap) +
' ' +
(half - this.pxRate - gap) +
',' +
(half - gap) +
' 0,' +
(half - gap)
)}
${this.htmlWriter.writeCircleAndLine(
'grid_to_battery_entity',
'M0,' +
(half + gap) +
' ' +
'C' +
(half - this.pxRate - gap) +
',' +
(half + gap) +
' ' +
(half - this.pxRate - gap) +
',' +
(half + gap) +
' ' +
(half - this.pxRate - gap) +
',' +
half * 2
)}
${this.htmlWriter.writeCircleAndLine(
'battery_to_grid_entity',
'M' +
(half - this.pxRate - gap) +
',' +
half * 2 +
' ' +
'C' +
(half - this.pxRate - gap) +
',' +
(half + gap) +
' ' +
(half - this.pxRate - gap) +
',' +
(half + gap) +
' ' +
'0,' +
(half + gap)
)}
${this.htmlWriter.writeCircleAndLine(
'generation_to_battery_entity',
'M' +
(half - this.pxRate) +
',0 ' +
'C' +
(half - this.pxRate) +
',0 ' +
(half - this.pxRate) +
',' +
half * 2 +
' ' +
(half - this.pxRate) +
',' +
half * 2
)}
${this.htmlWriter.writeCircleAndLine(
'battery_to_house_entity',
'M' +
(half - this.pxRate + gap) +
',' +
half * 2 +
' ' +
'C' +
(half - this.pxRate + gap) +
',' +
(half + gap) +
' ' +
(half - this.pxRate + gap) +
',' +
(half + gap) +
' ' +
half * 2 +
',' +
(half + gap)
)}
</svg>
</div>
${this.writeHouseIconBubble()} ${this.writeApplianceIconBubble(1)}
${this.htmlWriter.writeAppliancePowerLineAndCircle(1, 'M5,' + 12 * this.pxRate + ' C5,' + 12 * this.pxRate + ' 5,0 5,0')}
${this.writeApplianceIconBubble(2)}
${this.htmlWriter.writeAppliancePowerLineAndCircle(2, 'M5,0 C5,0 5,' + 11 * this.pxRate + ' 5,' + 11 * this.pxRate)}
</div>
</div>
<div class="acc_bottom">${this.writeBatteryIconBubble()}</div>
</div>
</ha-card>
`;
}
private writeGenerationIconBubble(): TemplateResult {
const generationEntities = ['generation_to_grid_entity', 'generation_to_house_entity', 'generation_to_battery_entity'];
const bubbleData:BubbleData = this.calculateIconBubbleData(
generationEntities,
'generation_entity',
'generation_extra_entity');
bubbleData.cssSelector = 'acc_top';
bubbleData.icon = this.config.generation_icon;
return this.htmlWriter.writeBatteryBubbleDiv(bubbleData);
}
private writeGridIconBubble(): TemplateResult {
const gridEntities = ['-generation_to_grid_entity', 'grid_to_house_entity', '-battery_to_grid_entity', 'grid_to_battery_entity'];
const bubbleData:BubbleData = this.calculateIconBubbleData(
gridEntities,
'grid_entity',
'grid_extra_entity');
bubbleData.cssSelector = 'acc_left';
bubbleData.icon = this.config.grid_icon;
return this.htmlWriter.writeBatteryBubbleDiv(bubbleData);
}
private writeHouseIconBubble(): TemplateResult {
let houseEntities:Array<string>;
if(this.config.house_without_appliances_values){
houseEntities = ['generation_to_house_entity', 'grid_to_house_entity', 'battery_to_house_entity', '-appliance1_consumption_entity','-appliance2_consumption_entity'];
} else {
houseEntities = ['generation_to_house_entity', 'grid_to_house_entity', 'battery_to_house_entity'];
}
const bubbleData:BubbleData = this.calculateIconBubbleData(
houseEntities,
'house_entity',
'house_extra_entity');
bubbleData.cssSelector = 'acc_right';
bubbleData.icon = this.config.house_icon;
return this.htmlWriter.writeBatteryBubbleDiv(bubbleData);
}
private writeBatteryIconBubble(): TemplateResult {
const batteryEntities = [
'generation_to_battery_entity',
'grid_to_battery_entity',
'-battery_to_house_entity',
'-battery_to_grid_entity',
];
const bubbleData:BubbleData = this.calculateIconBubbleData(
batteryEntities,
'battery_entity',
'battery_extra_entity');
bubbleData.cssSelector = 'acc_bottom';
bubbleData.icon = this.config.battery_icon;
return this.htmlWriter.writeBatteryBubbleDiv(bubbleData);
}
private writeApplianceIconBubble(applianceNumber: number): TemplateResult {
const applianceEntities = ['appliance' + applianceNumber + '_consumption_entity'];
const bubbleData:BubbleData = this.calculateIconBubbleData(
applianceEntities,
'appliance' + applianceNumber + '_consumption_entity',
'appliance' + applianceNumber + '_extra_entity');
bubbleData.cssSelector = 'acc_appliance' + applianceNumber;
bubbleData.icon = this.config['appliance' + applianceNumber + '_icon'];
return this.htmlWriter.writeBatteryBubbleDiv(bubbleData);
}
private calculateIconBubbleData(
entitiesForMainValue: Array<string>,
bubbleClickEntitySlot: string | null = null,
extraEntitySlot: string | null = null,
): BubbleData {
let isSubstractionEntity = false;
const bubbleData = new BubbleData;
bubbleData.clickEntitySlot = bubbleClickEntitySlot;
entitiesForMainValue.forEach((entityHolder: string) => {
if (entityHolder.substring(0, 1) === '-') {
entityHolder = entityHolder.substring(1);
isSubstractionEntity = true;
}
const divSolarElement = this.solarCardElements.get(entityHolder);
if (divSolarElement !== null && divSolarElement?.value !== undefined) {
bubbleData.noEntitiesWithValueFound = false;
bubbleData.mainValue = isSubstractionEntity ? (bubbleData.mainValue - divSolarElement?.value) : (bubbleData.mainValue + divSolarElement?.value);
bubbleData.mainValue = ((bubbleData.mainValue * 100) | 0) / 100;
bubbleData.mainUnitOfMeasurement = divSolarElement?.unitOfMeasurement;
}
isSubstractionEntity = false;
});
if (extraEntitySlot !== null) {
const extraEntity = this.solarCardElements.get(extraEntitySlot);
bubbleData.extraValue = extraEntity?.value;
bubbleData.extraUnitOfMeasurement = extraEntity?.unitOfMeasurement;
}
if (bubbleClickEntitySlot !== null) {
bubbleData.clickEntityHassState = this.hass.states[this.config[bubbleClickEntitySlot]];
}
if (this.showKW(bubbleData.mainValue)) {
bubbleData.mainValue = this.roundValue(bubbleData.mainValue / 1000);
bubbleData.mainUnitOfMeasurement = 'kW';
}
return bubbleData;
}
private showKW(value: number) {
if (this.config.show_w_not_kw) {
return false;
}
if (this.config.threshold_in_k !== undefined && Math.abs(value) < this.config.threshold_in_k * 1000) {
return false;
}
return true;
}
private roundValue(value: number): number {
let roundedValue: number;
if (value > 0.1) {
roundedValue = (Math.round((value + Number.EPSILON) * 10) | 0) / 10;
} else {
roundedValue = (Math.round((value + Number.EPSILON) * 100) | 0) / 100;
}
return roundedValue;
}
private animateCircles(obj: any) {
requestAnimationFrame(timestamp => {
obj.updateAllCircles(timestamp);
});
}
public updateAllCircles(timestamp: number): void {
// console.log('updating all circles')
this.solarCardElements.forEach((_solarSensor, key) => {
const element = this.solarCardElements.get(key);
if (element !== undefined) this.updateOneCircle(timestamp, element);
});
}
private updateOneCircle(timestamp: number, entity: SensorElement) {
if (this.shadowRoot == null) return;
const teslaCardElement = <HTMLElement>this.shadowRoot.querySelector('#tesla-style-solar-power-card');
if (teslaCardElement == null) return;
entity.line = <SVGPathElement>teslaCardElement.querySelector('#' + entity.entitySlot + '_line');
if (entity.line === null) return;
const lineLength = entity.line.getTotalLength();
if (isNaN(lineLength)) return;
entity.circle = <SVGPathElement>teslaCardElement.querySelector('#' + entity.entitySlot + '_circle');
if (entity.speed === 0) {
entity.circle.setAttribute('visibility', 'hidden');
if (this.config.hide_inactive_lines) entity.line.setAttribute('visibility', 'hidden');
return;
}
entity.circle.setAttribute('visibility', 'visible');
if (this.config.hide_inactive_lines) {
entity.line.setAttribute('visibility', 'visible');
}
if (entity.prevTimestamp === 0) {
entity.prevTimestamp = timestamp;
entity.currentDelta = 0;
}
entity.currentDelta += Math.abs(entity.speed) * (timestamp - entity.prevTimestamp);
let percentageDelta = entity.currentDelta / lineLength;
if (entity.speed > 0) {
if (percentageDelta >= 1 || isNaN(percentageDelta)) {
entity.currentDelta = 0;
percentageDelta = 0.01;
}
} else {
percentageDelta = 1 - percentageDelta;
if (percentageDelta <= 0 || isNaN(percentageDelta)) {
entity.currentDelta = 0;
percentageDelta = 1;
}
}
const point = entity.line.getPointAtLength(lineLength * percentageDelta);
entity.circle.setAttributeNS(null, 'cx', point.x.toString());
entity.circle.setAttributeNS(null, 'cy', point.y.toString());
entity.prevTimestamp = timestamp;
}
private colourHouseBubbleDependingOnHighestInput() {
if (this.shadowRoot == null) return;
const teslaCardElement = <HTMLElement>this.shadowRoot.querySelector('#tesla-style-solar-power-card');
if (teslaCardElement == null) return;
const houseEntities = ['generation_to_house_entity', 'grid_to_house_entity', 'battery_to_house_entity'];
let highestEntity: SensorElement | null = null;
let highestEntityHolder = '';
houseEntities.forEach(entityHolder => {
const divSolarElement = this.solarCardElements.get(entityHolder);
if (divSolarElement !== null && divSolarElement?.value !== undefined) {
if (highestEntity == null || divSolarElement?.value > highestEntity.value) {
highestEntityHolder = entityHolder;
highestEntity = divSolarElement;
}
}
});
switch (highestEntityHolder) {
case 'generation_to_house_entity':
this.colourBubble('.house_entity', teslaCardElement, 'warning');
this.colourBubble('.appliance1_consumption_entity', teslaCardElement, 'warning');
this.colourBubble('.appliance2_consumption_entity', teslaCardElement, 'warning');
this.colourLineAndCircle('#appliance1_consumption_entity', teslaCardElement, 'warning');
this.colourLineAndCircle('#appliance2_consumption_entity', teslaCardElement, 'warning');
break;
case 'battery_to_house_entity':
this.colourBubble('.house_entity', teslaCardElement, 'success');
this.colourBubble('.appliance1_consumption_entity', teslaCardElement, 'success');
this.colourBubble('.appliance2_consumption_entity', teslaCardElement, 'success');
this.colourLineAndCircle('#appliance1_consumption_entity', teslaCardElement, 'success');
this.colourLineAndCircle('#appliance2_consumption_entity', teslaCardElement, 'success');
break;
case 'grid_to_house_entity':
this.colourBubble('.house_entity', teslaCardElement, 'info');
this.colourBubble('.appliance1_consumption_entity', teslaCardElement, 'info');
this.colourBubble('.appliance2_consumption_entity', teslaCardElement, 'info');
this.colourLineAndCircle('#appliance1_consumption_entity', teslaCardElement, 'info');
this.colourLineAndCircle('#appliance2_consumption_entity', teslaCardElement, 'info');
break;
default:
}
}
private colourBubble(elementName: string, teslaCardElement: HTMLElement, colour: string) {
const element = <HTMLElement>teslaCardElement.querySelector(elementName);
if (element === null) return;
element.style.color = 'var(--' + colour + '-color)';
element.style.border = '1px solid var(--' + colour + '-color)';
}
private colourLineAndCircle(elementName: string, teslaCardElement: HTMLElement, colour: string) {
const elementLine = <HTMLElement>teslaCardElement.querySelector(elementName + '_line');
const elementCircle = <HTMLElement>teslaCardElement.querySelector(elementName + '_circle');
if (elementLine === null) return;
elementLine.style.stroke = 'var(--' + colour + '-color)';
elementCircle.style.fill = 'var(--' + colour + '-color)';
}
private setEnergyFlowDiagramm() {
if (this.shadowRoot == null) return;
const teslaCardElement = <HTMLElement>this.shadowRoot.querySelector('#tesla-style-solar-power-card');
if (teslaCardElement == null) return;
this.solarCardElements.forEach((_solarSensor, key) => {
const element = this.solarCardElements.get(key);
let width = 1;
if (teslaCardElement == null) return;
const entityLine = <SVGPathElement>teslaCardElement.querySelector('#' + key + '_line');
if (entityLine != null && element !== undefined) {
const entityCircle = <SVGPathElement>teslaCardElement.querySelector('#' + key + '_circle');
entityCircle.style.visibility = 'hidden';
if (this.config.energy_flow_diagramm_lines_factor === undefined) this.config.energy_flow_diagramm_lines_factor = 2;
if (element?.unitOfMeasurement.toUpperCase() === 'W') {
width = (Math.floor(element?.value / 100) / 10) * this.config.energy_flow_diagramm_lines_factor;
} else {
width = (Math.floor(element?.value * 10) / 10) * this.config.energy_flow_diagramm_lines_factor;
}
if (width <= 0.1 && width !== 0) width = 0.1;
entityLine.style.strokeWidth = width + 'px';
}
});
}
private redraw(ev: UIEvent) {
if (this.hass && this.config && ev.type === 'resize') {
const realWidth = this.getBoundingClientRect().width
this.oldWidth = HtmlResizeForPowerCard.changeStylesDependingOnWidth(this, this.solarCardElements, realWidth, this.oldWidth);
}
}
/* ******* actions ******** */
private _showWarning(warning: string): TemplateResult {
return html` <hui-warning>${warning}</hui-warning> `;
}
private _showError(): TemplateResult {
// const errorCard = <LovelaceCard>document.createElement('hui-error-card');
// eslint-disable-next-line no-console
console.log(this.error);
return html`
<hui-warning
><div>
ERROR:<br />
${this.error}
</div></hui-warning
>
`;
}
/* ******* style ******** */
static get styles(): CSSResult {
return css`
#tesla-style-solar-power-card{
margin:auto;
display:table;
padding: 10px;
position: relative;
}
.acc_container {
height: 40px;
width: 40px;
border: 1px solid black;
border-radius: 100px;
padding: 22px;
color: var(--primary-text-color);
border-color: var(--primary-text-color);
position:relative;
cursor:pointer;
}
.acc_icon {
--mdc-icon-size: 40px;
}
.acc_text,
.acc_text_extra {
text-align: center;
white-space: nowrap;
}
.acc_text_extra {
overflow: hidden;
position: absolute;
}
.acc_td {
vertical-align: top;
}
.acc_center .acc_td{
position:relative;
}
.acc_top .acc_container,
.acc_bottom .acc_container{
margin:auto;
}
.acc_center{
display:flex;
}
.acc_center_container{
display:inline-block;
margin: 0px auto;
margin-bottom:-5px;
}
.acc_right ,
.acc_left ,
.acc_line{
display:inline-block;
margin-right:-4px
}
.acc_left {
vertical-align: top;
}
.acc_right {
margin-right:0px;
}
#battery_to_house_entity_line,
#generation_to_house_entity_line,
#grid_to_house_entity_line,
#generation_to_battery_entity_line,
#grid_feed_in_entity_line,
#generation_to_grid_entity_line,
#battery_to_grid_entity_line,
#grid_to_battery_entity_line,
#appliance1_consumption_entity_line,
#appliance2_consumption_entity_line{
stroke:var(--info-color);
fill:none;
stroke-width:1;
}
.generation_entity {
border: 1px solid var(--warning-color);
}
.generation_entity .acc_icon,
.generation_entity{
color: var(--warning-color);
}
.house_entity{
border: 1px solid var(--info-color);
}
.appliance1_consumption_entity,
.appliance2_consumption_entity {
border: 1px solid var(--info-color);
}
.house_entity,
.appliance1_consumption_entity,
.appliance2_consumption_entity{
color: var(--info-color);
}
#generation_to_house_entity_line,
#generation_to_grid_entity_line,
#generation_to_battery_entity_line{
stroke:var(--warning-color);
}
#grid_to_battery_entity_circle,
#grid_to_house_entity_circle,
#appliance1_consumption_entity_circle,
#appliance2_consumption_entity_circle{
fill:var(--info-color);
}
#generation_to_house_entity_circle,
#generation_to_grid_entity_circle,
#generation_to_battery_entity_circle{
fill:var(--warning-color);
}
#battery_to_house_entity_line,
#battery_to_grid_entity_line{
stroke:var(--success-color);
}
#battery_to_house_entity_circle,
#battery_to_grid_entity_circle{
fill:var(--success-color);
}
.battery_extra_entity,
.battery_entity{
border: 1px solid var(--success-color);
color: var(--success-color);
}
.battery_extra_text{
position:absolute;
top:8px;
}
br.clear {
clear:both;
}
.power_lines svg{
transform: translateZ(0);
display:inline-block;
}
.acc_center .acc_td.acc_appliance1,
.acc_center .acc_td.acc_appliance2 {
position: absolute;
right: 10px;
`;
}
}
================================================
FILE: src/components/editor.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any, no-param-reassign, camelcase, lit/no-useless-template-literals, lit-a11y/click-events-have-key-events */
import { LitElement, html, TemplateResult, CSSResult, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardEditor, ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
import '../types.js';
import { TeslaStyleSolarPowerCardConfig } from '../models/TeslaStyleSolarPowerCardConfig.js';
const options = {
entities: {
icon: 'tune',
name: 'Entities',
secondary: 'Entities for card to make sense, none are required but you should have a few.',
show: false,
},
actions: {
icon: 'gesture-tap-hold',
name: 'Actions',
secondary: 'Perform actions based on tapping/clicking',
show: false,
options: {
tap: {
icon: 'gesture-tap',
name: 'Tap',
secondary: 'Set the action to perform on tap',
show: false,
},
hold: {
icon: 'gesture-tap-hold',
name: 'Hold',
secondary: 'Set the action to perform on hold',
show: false,
},
double_tap: {
icon: 'gesture-double-tap',
name: 'Double Tap',
secondary: 'Set the action to perform on double tap',
show: false,
},
},
},
appearance: {
icon: 'palette',
name: 'Appearance',
secondary: 'Customize the name, icon, etc',
show: false,
},
};
@customElement('tesla-style-solar-power-card-editor')
export class TeslaStyleSolarPowerCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() private _config?: LovelaceCardConfig;
@property() private _toggle?: boolean;
@property() private _helpers?: any;
private _initialized = false;
private _entityMap = new Map();
public setConfig(config: LovelaceCardConfig): void {
this._config = config;
// this._fillLineEntityMap();
this.loadCardHelpers();
}
protected shouldUpdate(): boolean {
if (!this._initialized) {
this._initialize();
}
return true;
}
get name(): string {
return this._config?.name || '';
}
// entities
get home_entity(): string {
return this._config?.home_entity || '';
}
get battery_entity(): string {
return this._config?.battery_consumption_entity || '';
}
get grid_entity(): string {
return this._config?.grid_entity || '';
}
get generation_entity(): string {
return this._config?.generation_entity || '';
}
get home_extra_entity(): string {
return this._config?.home_entity || '';
}
get battery_extra_entity(): string {
return this._config?.battery_consumption_entity || '';
}
get grid_extra_entity(): string {
return this._config?.grid_entity || '';
}
get generation_extra_entity(): string {
return this._config?.generation_entity || '';
}
get grid_to_house_entity(): string {
return this._config?.grid_to_house_entity || '';
}
get grid_to_battery_entity(): string {
return this._config?.grid_to_battery_entity || '';
}
get battery_to_grid_in_entity(): string {
return this._config?.battery_to_grid_entity || '';
}
get generation_to_grid_entity(): string {
return this._config?.grid_to_battery_entity || '';
}
get generation_to_house_entity(): string {
return this._config?.generation_entity || '';
}
get generation_to_battery_entity(): string {
return this._config?.generation_yield_entity || '';
}
get appliance1_consumption_entity(): string {
return this._config?.appliance1_consumption_entity || '';
}
get appliance1_extra_entity(): string {
return this._config?.appliance1_state_entity || '';
}
get appliance2_consumption_entity(): string {
return this._config?.appliance2_consumption_entity || '';
}
get appliance2_extra_entity(): string {
return this._config?.appliance2_state_entity || '';
}
get show_w_not_kw(): boolean {
return this._config?.show_w_not_kw || false;
}
get show_warning(): boolean {
return this._config?.show_warning || false;
}
get show_error(): boolean {
return this._config?.show_error || false;
}
get hide_inactive_lines(): boolean {
return this._config?.hide_inactive_lines || false;
}
get tap_action(): ActionConfig {
return this._config?.tap_action || { action: 'more-info' };
}
get hold_action(): ActionConfig {
return this._config?.hold_action || { action: 'none' };
}
get double_tap_action(): ActionConfig {
return this._config?.double_tap_action || { action: 'none' };
}
protected render(): TemplateResult | void {
if (!this.hass || !this._helpers) {
return html``;
}
// The climate more-info has ha-switch and paper-dropdown-menu elements that are lazy loaded unless explicitly done here
this._helpers.importMoreInfoControl('climate');
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize('ui.panel.lovelace.editor.card.generic.title')} (${this.hass.localize(
'ui.panel.lovelace.editor.card.config.optional'
)})"
.value=${this.name}
.configValue=${'name'}
@value-changed=${this._valueChanged}
></paper-input>
<div class="option" @click=${this._toggleOption} .option=${'entities'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.entities.icon}`}></ha-icon>
<div class="title">${options.entities.name}</div>
</div>
<div class="secondary">${options.entities.secondary}</div>
</div>
${options.entities.show
? html`<div class="values">
${Array.from(this._entityMap).map(entityArr => {
const entityName: keyof TeslaStyleSolarPowerCardConfig = entityArr[0];
const entityFunction = this[`_${entityName}`];
return html`
<ha-entity-picker
label="${entityName}"
@value-changed=${this._valueChanged}
.hass="${this.hass}"
.value="${entityFunction}"
.configValue=${entityName}
@change="${this._valueChanged}"
allow-custom-entity
>
</ha-entity-picker>
`;
})}
</div>`
: ''}
<div class="option" @click=${this._toggleOption} .option=${'actions'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.actions.icon}`}></ha-icon>
<div class="title">${options.actions.name}</div>
</div>
<div class="secondary">${options.actions.secondary}</div>
</div>
${options.actions.show
? html`
<div class="values">
<div class="option" @click=${this._toggleAction} .option=${'tap'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.actions.options.tap.icon}`}></ha-icon>
<div class="title">${options.actions.options.tap.name}</div>
</div>
<div class="secondary">${options.actions.options.tap.secondary}</div>
</div>
${options.actions.options.tap.show
? html`
<div class="values">
<paper-item>Action Editors Coming Soon</paper-item>
</div>
`
: ''}
<div class="option" @click=${this._toggleAction} .option=${'hold'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.actions.options.hold.icon}`}></ha-icon>
<div class="title">${options.actions.options.hold.name}</div>
</div>
<div class="secondary">${options.actions.options.hold.secondary}</div>
</div>
${options.actions.options.hold.show
? html`
<div class="values">
<paper-item>Action Editors Coming Soon</paper-item>
</div>
`
: ''}
<div class="option" @click=${this._toggleAction} .option=${'double_tap'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.actions.options.double_tap.icon}`}></ha-icon>
<div class="title">${options.actions.options.double_tap.name}</div>
</div>
<div class="secondary">${options.actions.options.double_tap.secondary}</div>
</div>
${options.actions.options.double_tap.show
? html`
<div class="values">
<paper-item>Action Editors Coming Soon</paper-item>
</div>
`
: ''}
</div>
`
: ''}
<div class="option" @click=${this._toggleOption} .option=${'appearance'}>
<div class="row">
<ha-icon .icon=${`mdi:${options.appearance.icon}`}></ha-icon>
<div class="title">${options.appearance.name}</div>
</div>
<div class="secondary">${options.appearance.secondary}</div>
</div>
${options.appearance.show
? html`
<div class="values">
<br />
<ha-formfield .label=${`Toggle warning ${this.show_warning ? 'off' : 'on'}`}>
<ha-switch
.checked=${this.show_warning !== false}
.configValue=${'show_warning'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<ha-formfield .label=${`Toggle error ${this.show_error ? 'off' : 'on'}`}>
<ha-switch .checked=${this.show_error !== false} .configValue=${'show_error'} @change=${this._valueChanged}></ha-switch>
</ha-formfield>
<ha-formfield .label=${`Toggle W instead of kW ${this.show_w_not_kw ? 'off' : 'on'}`}>
<ha-switch
.checked=${this.show_w_not_kw !== false}
.configValue=${'show_w_not_kw'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<ha-formfield .label=${`Toggle hiding inactive power lines ${this.hide_inactive_lines ? 'off' : 'on'}`}>
<ha-switch
.checked=${this.hide_inactive_lines !== false}
.configValue=${'hide_inactive_lines'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
</div>
`
: ''}
</div>
`;
}
private _initialize(): void {
if (this.hass === undefined) return;
if (this._config === undefined) return;
if (this._helpers === undefined) return;
this._initialized = true;
}
private async loadCardHelpers(): Promise<void> {
this._helpers = await (window as any).loadCardHelpers();
}
private _toggleAction(ev: any): void {
this._toggleThing(ev, options.actions.options);
}
private _toggleOption(ev: any): void {
this._toggleThing(ev, options);
}
private _toggleThing(ev: any, optionList: any): void {
const show = !optionList[ev.target.option].show;
for (const [key] of Object.entries(optionList)) {
optionList[key].show = false;
}
optionList[ev.target.option].show = show;
this._toggle = !this._toggle;
}
private _valueChanged(ev: any): void {
if (!this._config || !this.hass) {
return;
}
const { target } = ev;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === '') {
delete this._config[target.configValue];
} else {
this._config = {
...this._config,
[target.configValue]: target.checked !== undefined ? target.checked : target.value,
};
}
}
// fireEvent(this, 'config-changed', { config: this._config }); // this is breaking the card built when terser is activated
}
private _fillLineEntityMap() {
if (this._config !== undefined) {
this._entityMap.set('home_entity', this._config.home_entity);
this._entityMap.set('grid_entity', this._config.grid_entity);
this._entityMap.set('generation_entity', this._config.generation_entity);
this._entityMap.set('battery_entity', this._config.battery_entity);
this._entityMap.set('grid_to_house_entity', this._config.grid_to_battery_entity);
this._entityMap.set('grid_to_battery_entity', this._config.grid_to_battery_entity);
this._entityMap.set('generation_to_grid_entity', this._config.grid_feed_in_entity);
this._entityMap.set('generation_to_house_entity', this._config.generation_consumption_entity);
this._entityMap.set('generation_to_battery_entity', this._config.generation_to_battery_entity);
this._entityMap.set('battery_to_grid_entity', this._config.generation_consumption_entity);
this._entityMap.set('battery_to_house_entity', this._config.generation_to_battery_entity);
this._entityMap.set('battery_extra_entity', this._config.battery_extra_entity);
this._entityMap.set('appliance1_consumption_entity', this._config.appliance1_entity);
this._entityMap.set('appliance1_state_entity', this._config.appliance1_state_entity);
this._entityMap.set('appliance2_consumption_entity', this._config.appliance2_entity);
this._entityMap.set('appliance2_state_entity', this._config.appliance2_state_entity);
}
}
private _fillIconEntityMap() {
if (this._config !== undefined) {
this._entityMap.set('grid_entity', this._config.grid_entity);
this._entityMap.set('home_entity', this._config.home_entity);
this._entityMap.set('generation_entity', this._config.generation_entity);
this._entityMap.set('battery_entity', this._config.battery_entity);
this._entityMap.set('appliance1_entity', this._config.appliance1_entity);
this._entityMap.set('appliance2_entity', this._config.appliance2_entity);
}
}
static get styles(): CSSResult {
return css`
.option {
padding: 4px 0px;
cursor: pointer;
}
.row {
display: flex;
margin-bottom: -14px;
pointer-events: none;
}
.title {
padding-left: 16px;
margin-top: -6px;
pointer-events: none;
}
.secondary {
padding-left: 40px;
color: var(--secondary-text-color);
pointer-events: none;
}
.values {
padding-left: 16px;
background: var(--secondary-background-color);
display: grid;
}
ha-formfield {
padding-bottom: 8px;
}
`;
}
}
================================================
FILE: src/localize/languages/de.json
================================================
{
"common": {
"version": "Version",
"invalid_configuration": "Invalid configuration",
"show_warning": "Show Warning",
"show_error": "Show Error"
}
}
================================================
FILE: src/localize/languages/en.json
================================================
================================================
FILE: src/localize/localize.ts
================================================
import * as en from './languages/en.json';
import * as nb from './languages/de.json';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const languages: any = {
en,
nb,
};
export function localize(string: string, search = '', replace = ''): string {
const lang = (localStorage.getItem('selectedLanguage') || 'en')
.replace(/['"]+/g, '')
.replace('-', '_');
let translated: string;
try {
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
} catch (e) {
translated = string.split('.').reduce((o, i) => o[i], languages.en);
}
if (translated === undefined)
translated = string.split('.').reduce((o, i) => o[i], languages.en);
if (search !== '' && replace !== '') {
translated = translated.replace(search, replace);
}
return translated;
}
================================================
FILE: src/models/BubbleData.ts
================================================
import { HassEntity } from 'home-assistant-js-websocket';
export class BubbleData {
public mainValue: number = 0;
public mainUnitOfMeasurement: string | undefined;
public clickEntitySlot: string | null = null;
public clickEntityHassState:HassEntity | null = null;
public icon:string | undefined;
public cssSelector: string | undefined;
public extraValue: string | undefined;
public extraUnitOfMeasurement: string | undefined;
public noEntitiesWithValueFound = true;
}
================================================
FILE: src/models/SensorElement.ts
================================================
/* eslint-disable class-methods-use-this, no-bitwise */
export class SensorElement {
public speed = 0;
public startPosition = 0;
public currentPosition = 0;
public currentDelta = 0;
public maxPosition = 30;
public value: any;
public unitOfMeasurement = '';
public accText = '';
public accTextclassName = 'accText';
public entity = '';
public circle?: SVGPathElement;
public line?: SVGPathElement;
public color = 'stroke:var(--info-color)';
public circleColor = 'var(--primary-color)';
public prevTimestamp = 0;
public accTextElement = null;
public entitySlot: string;
private static readonly SPEEDFACTOR = 0.04;
constructor(entity: string, enitySlot: string) {
this.entity = entity;
this.entitySlot = enitySlot;
this.value = 0;
}
public setValueAndUnitOfMeasurement(entityState: string | undefined, unitOfMeasurement: string | undefined): void {
if (entityState === undefined) {
this.value = 0;
return;
}
if (unitOfMeasurement === undefined) {
this.value = entityState;
return;
}
const valueFromState = parseFloat(entityState);
switch (unitOfMeasurement) {
case 'W':
case 'w':
case 'kW':
this.value = valueFromState;
if (unitOfMeasurement === 'kW') {
this.value *= 1000;
}
this.unitOfMeasurement = 'W';
this.value = Math.round(this.value);
break;
case '%':
this.value = valueFromState;
this.unitOfMeasurement = unitOfMeasurement;
break;
default:
this.value = entityState;
this.unitOfMeasurement = unitOfMeasurement;
}
}
public setSpeed(factor: number | undefined): void {
this.speed = 0;
if (Math.abs(this.value) === 0) return;
let speedFactor: number;
if (factor === undefined || factor > 1 || factor <= 0) {
speedFactor = SensorElement.SPEEDFACTOR;
} else {
speedFactor = factor;
}
this.speed = (speedFactor * this.value) / 1000;
}
}
================================================
FILE: src/models/TeslaStyleSolarPowerCardConfig.ts
================================================
/* eslint-disable camelcase */
import { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
export interface TeslaStyleSolarPowerCardConfig extends LovelaceCardConfig {
type: string;
name?: string;
show_header_toggle?: boolean;
show_warning?: boolean;
show_error?: boolean;
show_gap?: boolean;
test_gui?: boolean;
show_w_not_kw?: any;
hide_inactive_lines?: boolean;
threshold_in_k?: number;
speed_factor?: number;
energy_flow_diagramm?: boolean;
energy_flow_diagramm_lines_factor?: number;
change_house_bubble_color_with_flow?: boolean;
house_without_appliances_values?: boolean;
grid_icon?: string;
generation_icon?: string;
house_icon?: string;
battery_icon?: string;
appliance1_icon?: string;
appliance2_icon?: string;
icon_entities?: Map<string, string>;
line_entities?: Map<string, string>;
house_entity?: string;
battery_entity?: string;
generation_entity?: string;
grid_entity?: string;
grid_to_house_entity?: string;
grid_to_battery_entity?: string;
generation_to_grid_entity?: string;
generation_to_battery_entity?: string;
generation_to_house_entity?: string;
battery_to_house_entity?: string;
battery_to_grid_entity?: string;
grid_extra_entity?: string;
generation_extra_entity?: string;
house_extra_entity?: string;
battery_extra_entity?: string;
appliance1_consumption_entity?: string;
appliance1_extra_entity?: string;
appliance2_consumption_entity?: string;
appliance2_extra_entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
================================================
FILE: src/services/HtmlResizeForPowerCard.ts
================================================
/* 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';
export class HtmlResizeForPowerCard {
public static changeStylesDependingOnWidth(
teslaCard: TeslaStyleSolarPowerCard,
solarCardElements: Map<string, SensorElement>,
newWidth: number,
oldWidth: number
): number {
if (document.readyState !== 'complete' || oldWidth === newWidth)
return oldWidth;
if (teslaCard.shadowRoot == null) return oldWidth;
const teslaCardElement = <HTMLElement>(
teslaCard.shadowRoot.querySelector('#tesla-style-solar-power-card')
);
if (teslaCardElement == null) return oldWidth;
if(newWidth < 200) newWidth = 250;
const pxRate = newWidth / 100;
const changeSelectorStyle = function (
selector: string,
styleAttribute: any,
attributeValue: string
) {
const selectorElement = <HTMLElement>(
teslaCardElement.querySelector(selector)
);
if (selectorElement !== null)
selectorElement.style[styleAttribute] = attributeValue;
};
changeSelectorStyle('.acc_left', 'top', 12 * pxRate + 'px');
changeSelectorStyle('.acc_right', 'top', 12 * pxRate + 'px');
// icons
teslaCardElement
.querySelectorAll('.acc_container')
.forEach((_currentValue, currentIndex, iconContainerItem) => {
const iconContainer = <HTMLElement>iconContainerItem[currentIndex];
iconContainer.style.height = 9 * pxRate + 'px';
iconContainer.style.width = 9 * pxRate + 'px';
iconContainer.style.padding = 5 * pxRate + 'px';
});
teslaCardElement
.querySelectorAll('ha-icon')
.forEach((_currentValue, currentIndex, icons) => {
const icon = <HTMLElement>(
icons[currentIndex].shadowRoot?.querySelector('ha-svg-icon')
);
if (icon != null) {
icon.style.height = 9 * pxRate + 'px';
icon.style.width = 9 * pxRate + 'px';
}
});
teslaCardElement
.querySelectorAll<HTMLElement>('.acc_text')
.forEach(icontext => {
icontext.style['font-size'] = 3 * pxRate + 'px';
icontext.style['margin-top'] = -0.5 * pxRate + 'px';
icontext.style.width = 10 * pxRate + 'px';
});
teslaCardElement
.querySelectorAll<HTMLElement>('.acc_text_extra')
.forEach(icontextExtra => {
icontextExtra.style['font-size'] = 3 * pxRate + 'px';
icontextExtra.style.top = 1 * pxRate + 'px';
icontextExtra.style.width = 10 * pxRate + 'px';
});
// power lines
changeSelectorStyle('.power_lines', 'height', 42 * pxRate + 'px');
changeSelectorStyle('.power_lines', 'width', 42 * pxRate + 'px');
changeSelectorStyle('.power_lines', 'top', 0 * pxRate + 'px');
changeSelectorStyle('.power_lines', 'left', 28 * pxRate + 'px');
changeSelectorStyle('.power_lines svg', 'width', 42 * pxRate + 'px');
changeSelectorStyle('.power_lines svg', 'height', 42 * pxRate + 'px');
changeSelectorStyle(
'.power_lines svg',
'viewBox',
'0 0 ' + 42 * pxRate + ' ' + 42 * pxRate
);
let selectorElement = <HTMLElement>(
teslaCardElement.querySelector('.power_lines svg')
);
if (selectorElement !== null)
selectorElement.setAttribute(
'viewBox',
'0 0 ' + 42 * pxRate + ' ' + 42 * pxRate
);
const half = 22 * pxRate;
changeSelectorStyle(
'#generation_to_house_entity_line',
'd',
'M' +
(half-pxRate) +
',0 C' +
(half-pxRate) +
',' +
half +
' ' +
(half-pxRate) +
',' +
half +
' ' +
half * 2 +
',' +
half
);
changeSelectorStyle(
'#grid_feed_in_entity_line',
'd',
'M' +
(half-pxRate) +
',0 C' +
(half-pxRate) +
',' +
half +
' ' +
(half-pxRate) +
',' +
half +
' 0,' +
half
);
changeSelectorStyle(
'#grid_to_house_entity_line',
'd',
'M0,' +
half +
' C' +
half +
',' +
half +
' ' +
half +
',' +
half +
' ' +
half * 2 +
',' +
half
);
changeSelectorStyle(
'#grid_to_battery_entity_line',
'd',
'M0,' +
half +
' C' +
half +
',' +
half +
' ' +
half +
',' +
half +
' ' +
half +
',' +
half * 2
);
changeSelectorStyle(
'#battery_to_house_entity_line',
'd',
'M' +
(half-pxRate) +
',' +
half * 2 +
' C' +
(half-pxRate) +
',' +
half +
' ' +
(half-pxRate) +
',' +
half +
' ' +
half * 2 +
',' +
half
);
changeSelectorStyle(
'#generation_to_battery_entity_line',
'd',
'M' +
(half-pxRate) +
',0 C' +
(half-pxRate) +
',0 ' +
(half-pxRate) +
',' +
half * 2 +
' ' +
(half-pxRate) +
',' +
half * 2
);
// appliances
[1, 2].forEach(value => {
changeSelectorStyle(
'.acc_appliance' + value + '_line svg',
'viewBox',
'0 0 ' + ((12*pxRate)-((value-1)*5)) + ' ' + ((12*pxRate)-((value-1)*5))
);
changeSelectorStyle(
'.acc_appliance' + value + '_line',
'right',
(9.5 * pxRate) + 10 + 'px'
);
changeSelectorStyle(
'.acc_appliance' + value + '_line',
'width',
'10px'
);
changeSelectorStyle(
'.acc_appliance' + value + '_line',
'height',
(12 * pxRate ) - (( value - 1) * 5) + 'px'
);
changeSelectorStyle(
'.acc_appliance' + value + '_line svg',
'width',
'10px'
);
changeSelectorStyle(
'.acc_appliance' + value + '_line svg',
'height',
(12 * pxRate ) - (( value - 1) * 5) + 'px'
);
selectorElement = <HTMLElement>(
teslaCardElement.querySelector('.acc_appliance' + value + '_line_svg')
);
if (selectorElement !== null)
selectorElement.setAttribute(
'viewBox',
'0 0 ' + ((12*pxRate)-((value-1)*5)) + ' ' + ((12*pxRate)-((value-1)*5))
);
const topElement = <HTMLElement>(
teslaCardElement.querySelector('.generation_entity')
);
if (topElement === null && value === 1 && selectorElement !== null) {
changeSelectorStyle(
'.acc_center_container',
'margin-top',
19 * pxRate + 'px'
);
}
const bottomElement = <HTMLElement>(
teslaCardElement.querySelector('.battery_entity')
);
if (bottomElement === null && value === 2 && selectorElement !== null) {
changeSelectorStyle(
'.acc_center_container',
'margin-bottom',
19 * pxRate + 'px'
);
}
});
const gridElement = <HTMLElement>(
teslaCardElement.querySelector('.grid_entity')
);
if (gridElement === null) {
changeSelectorStyle('.generation_entity', 'margin', '0px');
changeSelectorStyle('.battery_entity', 'margin', '0px');
changeSelectorStyle('.power_lines', 'width', 30 * pxRate + 'px');
selectorElement = <HTMLElement>(
teslaCardElement.querySelector('.power_lines svg')
);
if (selectorElement !== null)
selectorElement.setAttribute(
'viewBox',
12 * pxRate + ' 0 ' + 42 * pxRate + ' ' + 42 * pxRate
);
}
changeSelectorStyle('.acc_appliance1', 'top', 10 + 'px');
changeSelectorStyle('.acc_appliance1_line', 'top', 19 * pxRate + 12 + 'px');
changeSelectorStyle('.acc_appliance2', 'bottom', 10 + 'px');
changeSelectorStyle('.acc_appliance2_line', 'bottom', 19 * pxRate + 12 +'px');
return newWidth;
}
}
================================================
FILE: src/services/htmlWriterForPowerCard.ts
================================================
/* eslint-disable no-param-reassign, import/extensions, prefer-template, class-methods-use-this, lit-a11y/click-events-have-key-events, lines-between-class-members */
import { html, TemplateResult } from 'lit';
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { SensorElement } from '../models/SensorElement';
import { TeslaStyleSolarPowerCard } from '../TeslaStyleSolarPowerCard';
import { BubbleData } from '../models/BubbleData';
export class HtmlWriterForPowerCard {
private teslaCard: TeslaStyleSolarPowerCard;
private solarCardElements: Map<string, SensorElement>;
private pxRate: number;
private hass: HomeAssistant;
public constructor(teslaCard: TeslaStyleSolarPowerCard, hass: HomeAssistant) {
this.teslaCard = teslaCard;
this.solarCardElements = teslaCard.solarCardElements;
this.pxRate = teslaCard.pxRate;
this.hass = hass;
}
public writeBubbleDiv(bubbleData: BubbleData
): TemplateResult {
if(bubbleData.noEntitiesWithValueFound) return html``;
return html` <div class="acc_td ${bubbleData.cssSelector}">
<div
class="acc_container ${bubbleData.clickEntitySlot}"
style="${'width:' + 9 * this.pxRate + 'px; height: ' + 9 * this.pxRate + 'px; padding:' + 5 * this.pxRate + 'px;'}"
@click="${() => this._handleClick(bubbleData.clickEntityHassState)}"
>
${bubbleData.extraValue !== null
? html` <div
class="acc_text_extra"
style="font-size:${3 * this.pxRate + 'px'};
top: ${1 * this.pxRate + 'px'};
width: ${10 * this.pxRate + 'px'};"
>${bubbleData.extraValue} ${bubbleData.extraUnitOfMeasurement}
</div>`
: html``}
<ha-icon class="acc_icon" icon="${bubbleData.icon}"></ha-icon>
<div class="acc_text" style="font-size:${3 * this.pxRate + 'px'}; margin-top:${-0.5 * this.pxRate + 'px'}; width: ${10 * this.pxRate + 'px'}">
${bubbleData.mainValue} ${bubbleData.mainUnitOfMeasurement}
</div>
</div>
</div>`;
}
public writeBatteryBubbleDiv(bubbleData:BubbleData): TemplateResult {
if (bubbleData.extraValue !== undefined) {
if (bubbleData.icon === 'mdi:battery-medium' || bubbleData.icon === 'mdi:battery'){
bubbleData.icon = this.getBatteryIcon(parseFloat(bubbleData.extraValue), bubbleData.mainValue);
}
}
return this.writeBubbleDiv(bubbleData);
}
private getBatteryIcon(batteryValue: number, batteryChargeDischargeValue: number) {
let TempSocValue = batteryValue;
if (batteryValue <= 5) TempSocValue = 0;
const batteryStateRoundedValue = Math.ceil(TempSocValue / 10) * 10;
let batteryStateIconString = '-' + batteryStateRoundedValue.toString();
// show charging icon beside battery state
let batteryCharging: string = '-charging';
if (batteryChargeDischargeValue <= 0) {
batteryCharging = '';
}
if (batteryStateRoundedValue === 100) batteryStateIconString = ''; // full
if (batteryStateRoundedValue <= 5) batteryStateIconString = '-outline'; // empty
return 'mdi:battery' + batteryCharging + batteryStateIconString;
}
public writeAppliancePowerLineAndCircle(applianceNumber: number, pathDAttribute: string) {
const divEntity = this.solarCardElements.get('appliance' + applianceNumber + '_consumption_entity');
if (divEntity == null) return html``;
const height = 12;
let verticalPosition: string;
if (applianceNumber === 1) {
verticalPosition = 'top:' + 22.5 * this.pxRate + 'px;';
} else {
verticalPosition = 'bottom:' + 15 * this.pxRate + 'px;';
}
return html` <div
class="acc_line acc_appliance${applianceNumber}_line"
style="
height:${(height * this.pxRate)-((applianceNumber-1)*5)+'px'}
width:10px};
right:${(9.5 * this.pxRate) + 10 + 'px'};
${verticalPosition}
position:absolute"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox='${'0 0 '+ ((12*this.pxRate)-((applianceNumber-1)*5)) + ' ' +((12*this.pxRate)-((applianceNumber-1)*5))}'
preserveAspectRatio="xMinYMax slice"
style="height:${(height * this.pxRate)-((applianceNumber-1)*5)+'px'};width:10px}"
class="acc_appliance${applianceNumber}_line_svg"
>
${this.writeCircleAndLine('appliance' + applianceNumber + '_consumption_entity', pathDAttribute)}
</svg>
</div>`;
}
public writeCircleAndLine(sensorName: string, pathDAttribute: string) {
const entity = this.solarCardElements.get(sensorName);
if (entity == null) return html``;
return html`<svg>
<circle r="4" cx="${entity.startPosition.toString()}" cy="4" fill="${entity.color}" id="${sensorName + '_circle'}"></circle>
<path d="${pathDAttribute}" id="${sensorName + '_line'}"></path>
</svg>`;
}
private _handleClick(stateObj: HassEntity | null) {
if (stateObj == null) return;
const event = <any>new Event('hass-more-info', {
bubbles: true,
cancelable: true,
composed: true,
});
event.detail = { entityId: stateObj.entity_id };
if (this.teslaCard.shadowRoot == null) return;
this.teslaCard.shadowRoot.dispatchEvent(event);
}
}
================================================
FILE: src/translations/languages/de.json
================================================
{
"common": {
"version": "Version",
"invalid_configuration": "Invalid configuration",
"show_warning": "Show Warning",
"show_error": "Show Error"
}
}
================================================
FILE: src/translations/languages/en.json
================================================
================================================
FILE: src/translations/localize.ts
================================================
import * as en from './languages/en.json';
import * as nb from './languages/de.json';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const languages: any = {
en,
nb,
};
export function localize(string: string, search = '', replace = ''): string {
const lang = (localStorage.getItem('selectedLanguage') || 'en')
.replace(/['"]+/g, '')
.replace('-', '_');
let translated: string;
try {
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
} catch (e) {
translated = string.split('.').reduce((o, i) => o[i], languages.en);
}
if (translated === undefined)
translated = string.split('.').reduce((o, i) => o[i], languages.en);
if (search !== '' && replace !== '') {
translated = translated.replace(search, replace);
}
return translated;
}
================================================
FILE: src/types.ts
================================================
import { LovelaceCard, LovelaceCardEditor } from 'custom-card-helpers';
declare global {
interface HTMLElementTagNameMap {
'tesla-style-solar-power-card-editor': LovelaceCardEditor;
'hui-error-card': LovelaceCard;
}
}
================================================
FILE: tesla-style-solar-power-card.js
================================================
!function(){"use strict";
/*! *****************************************************************************
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 t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,n);else for(var l=t.length-1;l>=0;l--)(s=t[l])&&(r=(o<3?s(r):o>3?s(e,i,r):s(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/}const e=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),n=new Map;class s{constructor(t,e){if(this._$cssResult$=!0,e!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){let t=n.get(this.cssText);return e&&void 0===t&&(n.set(this.cssText,t=new CSSStyleSheet),t.replaceSync(this.cssText)),t}toString(){return this.cssText}}const o=(t,...e)=>{const n=1===t.length?t[0]:e.reduce(((e,i,n)=>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.")})(i)+t[n+1]),t[0]);return new s(n,i)},r=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new s("string"==typeof t?t:t+"",i))(e)})(t):t
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/;var l;const a=window.trustedTypes,c=a?a.emptyScript:"",h=window.reactiveElementPolyfillSupport,u={toAttribute(t,e){switch(e){case Boolean:t=t?c:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){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}},d=(t,e)=>e!==t&&(e==e||t==t),p={attribute:!0,type:String,converter:u,reflect:!1,hasChanged:d};class _ extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o()}static addInitializer(t){var e;null!==(e=this.l)&&void 0!==e||(this.l=[]),this.l.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const n=this._$Eh(i,e);void 0!==n&&(this._$Eu.set(n,i),t.push(n))})),t}static createProperty(t,e=p){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const s=this[t];this[e]=n,this.requestUpdate(t,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||p}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this._$Eu=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(r(t))}else void 0!==t&&e.push(r(t));return e}static _$Eh(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}o(){var t;this._$Ep=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Em(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$Eg)&&void 0!==e?e:this._$Eg=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$Eg)||void 0===e||e.splice(this._$Eg.indexOf(t)>>>0,1)}_$Em(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Et.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const i=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,i)=>{e?t.adoptedStyleSheets=i.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):i.forEach((e=>{const i=document.createElement("style"),n=window.litNonce;void 0!==n&&i.setAttribute("nonce",n),i.textContent=e.cssText,t.appendChild(i)}))})(i,this.constructor.elementStyles),i}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ES(t,e,i=p){var n,s;const o=this.constructor._$Eh(t,i);if(void 0!==o&&!0===i.reflect){const r=(null!==(s=null===(n=i.converter)||void 0===n?void 0:n.toAttribute)&&void 0!==s?s:u.toAttribute)(e,i.type);this._$Ei=t,null==r?this.removeAttribute(o):this.setAttribute(o,r),this._$Ei=null}}_$AK(t,e){var i,n,s;const o=this.constructor,r=o._$Eu.get(t);if(void 0!==r&&this._$Ei!==r){const t=o.getPropertyOptions(r),l=t.converter,a=null!==(s=null!==(n=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==n?n:"function"==typeof l?l:null)&&void 0!==s?s:u.fromAttribute;this._$Ei=r,this[r]=a(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||d)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$Ep=this._$E_())}async _$E_(){this.isUpdatePending=!0;try{await this._$Ep}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,e)=>this[e]=t)),this._$Et=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$Eg)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return!0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$ES(e,this[e],t))),this._$EC=void 0),this._$EU()}updated(t){}firstUpdated(t){}}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var y;_.finalized=!0,_.elementProperties=new Map,_.elementStyles=[],_.shadowRootOptions={mode:"open"},null==h||h({ReactiveElement:_}),(null!==(l=globalThis.reactiveElementVersions)&&void 0!==l?l:globalThis.reactiveElementVersions=[]).push("1.3.1");const g=globalThis.trustedTypes,v=g?g.createPolicy("lit-html",{createHTML:t=>t}):void 0,m=`lit$${(Math.random()+"").slice(9)}$`,f="?"+m,b=`<${f}>`,$=document,w=(t="")=>$.createComment(t),x=t=>null===t||"object"!=typeof t&&"function"!=typeof t,A=Array.isArray,C=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,E=/-->/g,S=/>/g,R=/>|[ \n\r](?:([^\s"'>=/]+)([ \n\r]*=[ \n\r]*(?:[^ \n\r"'`<>=]|("|')|))|$)/g,B=/'/g,M=/"/g,k=/^(?:script|style|textarea|title)$/i,O=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),P=Symbol.for("lit-noChange"),U=Symbol.for("lit-nothing"),T=new WeakMap,H=$.createTreeWalker($,129,null,!1),N=(t,e)=>{const i=t.length-1,n=[];let s,o=2===e?"<svg>":"",r=C;for(let e=0;e<i;e++){const i=t[e];let l,a,c=-1,h=0;for(;h<i.length&&(r.lastIndex=h,a=r.exec(i),null!==a);)h=r.lastIndex,r===C?"!--"===a[1]?r=E:void 0!==a[1]?r=S:void 0!==a[2]?(k.test(a[2])&&(s=RegExp("</"+a[2],"g")),r=R):void 0!==a[3]&&(r=R):r===R?">"===a[0]?(r=null!=s?s:C,c=-1):void 0===a[1]?c=-2:(c=r.lastIndex-a[2].length,l=a[1],r=void 0===a[3]?R:'"'===a[3]?M:B):r===M||r===B?r=R:r===E||r===S?r=C:(r=R,s=void 0);const u=r===R&&t[e+1].startsWith("/>")?" ":"";o+=r===C?i+b:c>=0?(n.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+m+u):i+m+(-2===c?(n.push(void 0),e):u)}const l=o+(t[i]||"<?>")+(2===e?"</svg>":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==v?v.createHTML(l):l,n]};class D{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let s=0,o=0;const r=t.length-1,l=this.parts,[a,c]=N(t,e);if(this.el=D.createElement(a,i),H.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(n=H.nextNode())&&l.length<r;){if(1===n.nodeType){if(n.hasAttributes()){const t=[];for(const e of n.getAttributeNames())if(e.endsWith("$lit$")||e.startsWith(m)){const i=c[o++];if(t.push(e),void 0!==i){const t=n.getAttribute(i.toLowerCase()+"$lit$").split(m),e=/([.?@])?(.*)/.exec(i);l.push({type:1,index:s,name:e[2],strings:t,ctor:"."===e[1]?q:"?"===e[1]?j:"@"===e[1]?F:V})}else l.push({type:6,index:s})}for(const e of t)n.removeAttribute(e)}if(k.test(n.tagName)){const t=n.textContent.split(m),e=t.length-1;if(e>0){n.textContent=g?g.emptyScript:"";for(let i=0;i<e;i++)n.append(t[i],w()),H.nextNode(),l.push({type:2,index:++s});n.append(t[e],w())}}}else if(8===n.nodeType)if(n.data===f)l.push({type:2,index:s});else{let t=-1;for(;-1!==(t=n.data.indexOf(m,t+1));)l.push({type:7,index:s}),t+=m.length-1}s++}}static createElement(t,e){const i=$.createElement("template");return i.innerHTML=t,i}}function W(t,e,i=t,n){var s,o,r,l;if(e===P)return e;let a=void 0!==n?null===(s=i._$Cl)||void 0===s?void 0:s[n]:i._$Cu;const c=x(e)?void 0:e._$litDirective$;return(null==a?void 0:a.constructor)!==c&&(null===(o=null==a?void 0:a._$AO)||void 0===o||o.call(a,!1),void 0===c?a=void 0:(a=new c(t),a._$AT(t,i,n)),void 0!==n?(null!==(r=(l=i)._$Cl)&&void 0!==r?r:l._$Cl=[])[n]=a:i._$Cu=a),void 0!==a&&(e=W(t,a._$AS(t,e.values),a,n)),e}class L{constructor(t,e){this.v=[],this._$AN=void 0,this._$AD=t,this._$AM=e}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}p(t){var e;const{el:{content:i},parts:n}=this._$AD,s=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:$).importNode(i,!0);H.currentNode=s;let o=H.nextNode(),r=0,l=0,a=n[0];for(;void 0!==a;){if(r===a.index){let e;2===a.type?e=new I(o,o.nextSibling,this,t):1===a.type?e=new a.ctor(o,a.name,a.strings,this,t):6===a.type&&(e=new K(o,this,t)),this.v.push(e),a=n[++l]}r!==(null==a?void 0:a.index)&&(o=H.nextNode(),r++)}return s}m(t){let e=0;for(const i of this.v)void 0!==i&&(void 0!==i.strings?(i._$AI(t,i,e),e+=i.strings.length-2):i._$AI(t[e])),e++}}class I{constructor(t,e,i,n){var s;this.type=2,this._$AH=U,this._$AN=void 0,this._$AA=t,this._$AB=e,this._$AM=i,this.options=n,this._$Cg=null===(s=null==n?void 0:n.isConnected)||void 0===s||s}get _$AU(){var t,e;return null!==(e=null===(t=this._$AM)||void 0===t?void 0:t._$AU)&&void 0!==e?e:this._$Cg}get parentNode(){let t=this._$AA.parentNode;const e=this._$AM;return void 0!==e&&11===t.nodeType&&(t=e.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,e=this){t=W(this,t,e),x(t)?t===U||null==t||""===t?(this._$AH!==U&&this._$AR(),this._$AH=U):t!==this._$AH&&t!==P&&this.$(t):void 0!==t._$litType$?this.T(t):void 0!==t.nodeType?this.k(t):(t=>{var e;return A(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])})(t)?this.S(t):this.$(t)}A(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}k(t){this._$AH!==t&&(this._$AR(),this._$AH=this.A(t))}$(t){this._$AH!==U&&x(this._$AH)?this._$AA.nextSibling.data=t:this.k($.createTextNode(t)),this._$AH=t}T(t){var e;const{values:i,_$litType$:n}=t,s="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=D.createElement(n.h,this.options)),n);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===s)this._$AH.m(i);else{const t=new L(s,this),e=t.p(this.options);t.m(i),this.k(e),this._$AH=t}}_$AC(t){let e=T.get(t.strings);return void 0===e&&T.set(t.strings,e=new D(t)),e}S(t){A(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,n=0;for(const s of t)n===e.length?e.push(i=new I(this.A(w()),this.A(w()),this,this.options)):i=e[n],i._$AI(s),n++;n<e.length&&(this._$AR(i&&i._$AB.nextSibling,n),e.length=n)}_$AR(t=this._$AA.nextSibling,e){var i;for(null===(i=this._$AP)||void 0===i||i.call(this,!1,!0,e);t&&t!==this._$AB;){const e=t.nextSibling;t.remove(),t=e}}setConnected(t){var e;void 0===this._$AM&&(this._$Cg=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}class V{constructor(t,e,i,n,s){this.type=1,this._$AH=U,this._$AN=void 0,this.element=t,this.name=e,this._$AM=n,this.options=s,i.length>2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=U}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,n){const s=this.strings;let o=!1;if(void 0===s)t=W(this,t,e,0),o=!x(t)||t!==this._$AH&&t!==P,o&&(this._$AH=t);else{const n=t;let r,l;for(t=s[0],r=0;r<s.length-1;r++)l=W(this,n[i+r],e,r),l===P&&(l=this._$AH[r]),o||(o=!x(l)||l!==this._$AH[r]),l===U?t=U:t!==U&&(t+=(null!=l?l:"")+s[r+1]),this._$AH[r]=l}o&&!n&&this.C(t)}C(t){t===U?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,null!=t?t:"")}}class q extends V{constructor(){super(...arguments),this.type=3}C(t){this.element[this.name]=t===U?void 0:t}}const z=g?g.emptyScript:"";class j extends V{constructor(){super(...arguments),this.type=4}C(t){t&&t!==U?this.element.setAttribute(this.name,z):this.element.removeAttribute(this.name)}}class F extends V{constructor(t,e,i,n,s){super(t,e,i,n,s),this.type=5}_$AI(t,e=this){var i;if((t=null!==(i=W(this,t,e,0))&&void 0!==i?i:U)===P)return;const n=this._$AH,s=t===U&&n!==U||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,o=t!==U&&(n===U||s);s&&this.element.removeEventListener(this.name,this,n),o&&this.element.addEventListener(this.name,this,t),this._$AH=t}handleEvent(t){var e,i;"function"==typeof this._$AH?this._$AH.call(null!==(i=null===(e=this.options)||void 0===e?void 0:e.host)&&void 0!==i?i:this.element,t):this._$AH.handleEvent(t)}}class K{constructor(t,e,i){this.element=t,this.type=6,this._$AN=void 0,this._$AM=e,this.options=i}get _$AU(){return this._$AM._$AU}_$AI(t){W(this,t)}}const G=window.litHtmlPolyfillSupport;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var Z,J;null==G||G(D,I),(null!==(y=globalThis.litHtmlVersions)&&void 0!==y?y:globalThis.litHtmlVersions=[]).push("2.2.1");class Y extends _{constructor(){super(...arguments),this.renderOptions={host:this},this._$Dt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Dt=((t,e,i)=>{var n,s;const o=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:e;let r=o._$litPart$;if(void 0===r){const t=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:null;o._$litPart$=r=new I(e.insertBefore(w(),t),t,void 0,null!=i?i:{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!1)}render(){return P}}Y.finalized=!0,Y._$litElement$=!0,null===(Z=globalThis.litElementHydrateSupport)||void 0===Z||Z.call(globalThis,{LitElement:Y});const Q=globalThis.litElementPolyfillSupport;null==Q||Q({LitElement:Y}),(null!==(J=globalThis.litElementVersions)&&void 0!==J?J:globalThis.litElementVersions=[]).push("3.2.0");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const X=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function tt(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):X(t,e)
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/}var et;null===(et=window.HTMLSlotElement)||void 0===et||et.prototype.assignedElements;class it{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){if(void 0===t)return void(this.value=0);if(void 0===e)return void(this.value=t);const i=parseFloat(t);switch(e){case"W":case"w":case"kW":this.value=i,"kW"===e&&(this.value*=1e3),this.unitOfMeasurement="W",this.value=Math.round(this.value);break;case"%":this.value=i,this.unitOfMeasurement=e;break;default:this.value=t,this.unitOfMeasurement=e}}setSpeed(t){if(this.speed=0,0===Math.abs(this.value))return;let e;e=void 0===t||t>1||t<=0?it.SPEEDFACTOR:t,this.speed=e*this.value/1e3}}it.SPEEDFACTOR=.04;class nt{constructor(){this.mainValue=0,this.clickEntitySlot=null,this.clickEntityHassState=null,this.noEntitiesWithValueFound=!0}}class st{constructor(t,e){this.teslaCard=t,this.solarCardElements=t.solarCardElements,this.pxRate=t.pxRate,this.hass=e}writeBubbleDiv(t){return t.noEntitiesWithValueFound?O``:O` <div class="acc_td ${t.cssSelector}">
<div
class="acc_container ${t.clickEntitySlot}"
style="${"width:"+9*this.pxRate+"px; height: "+9*this.pxRate+"px; padding:"+5*this.pxRate+"px;"}"
@click="${()=>this._handleClick(t.clickEntityHassState)}"
>
${null!==t.extraValue?O` <div
class="acc_text_extra"
style="font-size:${3*this.pxRate+"px"};
top: ${1*this.pxRate+"px"};
width: ${10*this.pxRate+"px"};"
>${t.extraValue} ${t.extraUnitOfMeasurement}
</div>`:O``}
<ha-icon class="acc_icon" icon="${t.icon}"></ha-icon>
<div class="acc_text" style="font-size:${3*this.pxRate+"px"}; margin-top:${-.5*this.pxRate+"px"}; width: ${10*this.pxRate+"px"}">
${t.mainValue} ${t.mainUnitOfMeasurement}
</div>
</div>
</div>`}writeBatteryBubbleDiv(t){return void 0!==t.extraValue&&("mdi:battery-medium"!==t.icon&&"mdi:battery"!==t.icon||(t.icon=this.getBatteryIcon(parseFloat(t.extraValue),t.mainValue))),this.writeBubbleDiv(t)}getBatteryIcon(t,e){let i=t;t<=5&&(i=0);const n=10*Math.ceil(i/10);let s="-"+n.toString(),o="-charging";return e<=0&&(o=""),100===n&&(s=""),n<=5&&(s="-outline"),"mdi:battery"+o+s}writeAppliancePowerLineAndCircle(t,e){if(null==this.solarCardElements.get("appliance"+t+"_consumption_entity"))return O``;let i;return i=1===t?"top:"+22.5*this.pxRate+"px;":"bottom:"+15*this.pxRate+"px;",O` <div
class="acc_line acc_appliance${t}_line"
style="
height:${12*this.pxRate-5*(t-1)+"px"}
width:10px};
right:${9.5*this.pxRate+10+"px"};
${i}
position:absolute"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox='${"0 0 "+(12*this.pxRate-5*(t-1))+" "+(12*this.pxRate-5*(t-1))}'
preserveAspectRatio="xMinYMax slice"
style="height:${12*this.pxRate-5*(t-1)+"px"};width:10px}"
class="acc_appliance${t}_line_svg"
>
${this.writeCircleAndLine("appliance"+t+"_consumption_entity",e)}
</svg>
</div>`}writeCircleAndLine(t,e){const i=this.solarCardElements.get(t);return null==i?O``:O`<svg>
<circle r="4" cx="${i.startPosition.toString()}" cy="4" fill="${i.color}" id="${t+"_circle"}"></circle>
<path d="${e}" id="${t+"_line"}"></path>
</svg>`}_handleClick(t){if(null==t)return;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 ot{static changeStylesDependingOnWidth(t,e,i,n){if("complete"!==document.readyState||n===i)return n;if(null==t.shadowRoot)return n;const s=t.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==s)return n;i<200&&(i=250);const o=i/100,r=function(t,e,i){const n=s.querySelector(t);null!==n&&(n.style[e]=i)};r(".acc_left","top",12*o+"px"),r(".acc_right","top",12*o+"px"),s.querySelectorAll(".acc_container").forEach(((t,e,i)=>{const n=i[e];n.style.height=9*o+"px",n.style.width=9*o+"px",n.style.padding=5*o+"px"})),s.querySelectorAll("ha-icon").forEach(((t,e,i)=>{var n;const s=null===(n=i[e].shadowRoot)||void 0===n?void 0:n.querySelector("ha-svg-icon");null!=s&&(s.style.height=9*o+"px",s.style.width=9*o+"px")})),s.querySelectorAll(".acc_text").forEach((t=>{t.style["font-size"]=3*o+"px",t.style["margin-top"]=-.5*o+"px",t.style.width=10*o+"px"})),s.querySelectorAll(".acc_text_extra").forEach((t=>{t.style["font-size"]=3*o+"px",t.style.top=1*o+"px",t.style.width=10*o+"px"})),r(".power_lines","height",42*o+"px"),r(".power_lines","width",42*o+"px"),r(".power_lines","top",0*o+"px"),r(".power_lines","left",28*o+"px"),r(".power_lines svg","width",42*o+"px"),r(".power_lines svg","height",42*o+"px"),r(".power_lines svg","viewBox","0 0 "+42*o+" "+42*o);let l=s.querySelector(".power_lines svg");null!==l&&l.setAttribute("viewBox","0 0 "+42*o+" "+42*o);const a=22*o;r("#generation_to_house_entity_line","d","M"+(a-o)+",0 C"+(a-o)+","+a+" "+(a-o)+","+a+" "+2*a+","+a),r("#grid_feed_in_entity_line","d","M"+(a-o)+",0 C"+(a-o)+","+a+" "+(a-o)+","+a+" 0,"+a),r("#grid_to_house_entity_line","d","M0,"+a+" C"+a+","+a+" "+a+","+a+" "+2*a+","+a),r("#grid_to_battery_entity_line","d","M0,"+a+" C"+a+","+a+" "+a+","+a+" "+a+","+2*a),r("#battery_to_house_entity_line","d","M"+(a-o)+","+2*a+" C"+(a-o)+","+a+" "+(a-o)+","+a+" "+2*a+","+a),r("#generation_to_battery_entity_line","d","M"+(a-o)+",0 C"+(a-o)+",0 "+(a-o)+","+2*a+" "+(a-o)+","+2*a),[1,2].forEach((t=>{r(".acc_appliance"+t+"_line svg","viewBox","0 0 "+(12*o-5*(t-1))+" "+(12*o-5*(t-1))),r(".acc_appliance"+t+"_line","right",9.5*o+10+"px"),r(".acc_appliance"+t+"_line","width","10px"),r(".acc_appliance"+t+"_line","height",12*o-5*(t-1)+"px"),r(".acc_appliance"+t+"_line svg","width","10px"),r(".acc_appliance"+t+"_line svg","height",12*o-5*(t-1)+"px"),l=s.querySelector(".acc_appliance"+t+"_line_svg"),null!==l&&l.setAttribute("viewBox","0 0 "+(12*o-5*(t-1))+" "+(12*o-5*(t-1)));null===s.querySelector(".generation_entity")&&1===t&&null!==l&&r(".acc_center_container","margin-top",19*o+"px");null===s.querySelector(".battery_entity")&&2===t&&null!==l&&r(".acc_center_container","margin-bottom",19*o+"px")}));return null===s.querySelector(".grid_entity")&&(r(".generation_entity","margin","0px"),r(".battery_entity","margin","0px"),r(".power_lines","width",30*o+"px"),l=s.querySelector(".power_lines svg"),null!==l&&l.setAttribute("viewBox",12*o+" 0 "+42*o+" "+42*o)),r(".acc_appliance1","top","10px"),r(".acc_appliance1_line","top",19*o+12+"px"),r(".acc_appliance2","bottom","10px"),r(".acc_appliance2_line","bottom",19*o+12+"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 rt extends Y{constructor(){super(...arguments),this.solarCardElements=new Map,this.oldWidth=100,this.pxRate=4,this.htmlWriter=new st(this,this.hass),this.title="Hey there",this.counter=5,this.error=""}__increment(){this.counter+=1}setConfig(t){if(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"),null==this.config.speed_factor&&(this.config.speed_factor=.04),this.createSolarCardElements(),!this.config.energy_flow_diagramm){const t=this;setInterval(this.animateCircles,15,t)}}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 it(e,t))}}))}getCardSize(){return 5}static getStubConfig(){return{}}async firstUpdated(){await new Promise((t=>setTimeout(t,0)));const t=this.getBoundingClientRect().width;this.oldWidth=ot.changeStylesDependingOnWidth(this,this.solarCardElements,t,this.oldWidth)}connectedCallback(){super.connectedCallback(),this.redraw=this.redraw.bind(this),window.addEventListener("resize",this.redraw)}shouldUpdate(t){let e;e=this,this.config.energy_flow_diagramm||requestAnimationFrame((t=>{e.updateAllCircles(t)})),e=this;let i=!0;return Array.from(t.keys()).some((e=>{const n=t.get(e);return"hass"===e&&n&&(i=i&&this.sensorChangeDetected(n)),!i})),i}sensorChangeDetected(t){let e=!1;return this.solarCardElements.forEach(((i,n)=>{void 0!==this.hass.states[this.config[n]]&&this.hass.states[this.config[n]].state!==t.states[this.config[n]].state&&(e=!0)})),e}async performUpdate(){this.error="",this.solarCardElements.forEach((t=>{try{t.setValueAndUnitOfMeasurement(this.hass.states[t.entity].state,this.hass.states[t.entity].attributes.unit_of_measurement),t.setSpeed(this.config.speed_factor)}catch(e){this.error+=" Configured '"+t.entity+"' entity was not found. "}})),this.config.energy_flow_diagramm&&this.setEnergyFlowDiagramm(),this.config.change_house_bubble_color_with_flow&&this.colourHouseBubbleDependingOnHighestInput(),super.performUpdate()}render(){if(""!==this.error)return this._showError();let t,e=this.getBoundingClientRect().width;e<200&&(e=250),this.pxRate=e/100,t=void 0!==this.config.show_gap&&this.config.show_gap?2*this.pxRate:0;const i=22*this.pxRate;return O`
<ha-card .header=${this.config.name} tabindex="0">
<div id="tesla-style-solar-power-card">
${this.writeGenerationIconBubble()}
<div class="acc_center">
<div class="acc_center_container">
${this.writeGridIconBubble()}
<div
class="acc_line power_lines"
style="
height:${42*this.pxRate+"px"};
width:${42*this.pxRate+"px"};
top:${0*this.pxRate+"px"};
left:${28*this.pxRate+"px"}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="${"0 0 "+42*this.pxRate+" "+42*this.pxRate}"
preserveAspectRatio="xMinYMax slice"
style="height:${42*this.pxRate+"px"};width:${42*this.pxRate+"px"}"
>
${this.htmlWriter.writeCircleAndLine("generation_to_house_entity","M"+(i-this.pxRate+t)+",0C"+(i-this.pxRate+t)+","+(i-t)+" "+(i-this.pxRate+t)+","+(i-t)+" "+2*i+","+(i-t))}
${this.htmlWriter.writeCircleAndLine("grid_to_house_entity","M0,"+i+" C"+(i-this.pxRate)+","+i+" "+(i-this.pxRate)+","+i+" "+2*(i-this.pxRate)+","+i)}
${this.htmlWriter.writeCircleAndLine("generation_to_grid_entity","M"+(i-this.pxRate-t)+",0 C"+(i-this.pxRate-t)+","+(i-t)+" "+(i-this.pxRate-t)+","+(i-t)+" 0,"+(i-t))}
${this.htmlWriter.writeCircleAndLine("grid_to_battery_entity","M0,"+(i+t)+" C"+(i-this.pxRate-t)+","+(i+t)+" "+(i-this.pxRate-t)+","+(i+t)+" "+(i-this.pxRate-t)+","+2*i)}
${this.htmlWriter.writeCircleAndLine("battery_to_grid_entity","M"+(i-this.pxRate-t)+","+2*i+" C"+(i-this.pxRate-t)+","+(i+t)+" "+(i-this.pxRate-t)+","+(i+t)+" 0,"+(i+t))}
${this.htmlWriter.writeCircleAndLine("generation_to_battery_entity","M"+(i-this.pxRate)+",0 C"+(i-this.pxRate)+",0 "+(i-this.pxRate)+","+2*i+" "+(i-this.pxRate)+","+2*i)}
${this.htmlWriter.writeCircleAndLine("battery_to_house_entity","M"+(i-this.pxRate+t)+","+2*i+" C"+(i-this.pxRate+t)+","+(i+t)+" "+(i-this.pxRate+t)+","+(i+t)+" "+2*i+","+(i+t))}
</svg>
</div>
${this.writeHouseIconBubble()} ${this.writeApplianceIconBubble(1)}
${this.htmlWriter.writeAppliancePowerLineAndCircle(1,"M5,"+12*this.pxRate+" C5,"+12*this.pxRate+" 5,0 5,0")}
${this.writeApplianceIconBubble(2)}
${this.htmlWriter.writeAppliancePowerLineAndCircle(2,"M5,0 C5,0 5,"+11*this.pxRate+" 5,"+11*this.pxRate)}
</div>
</div>
<div class="acc_bottom">${this.writeBatteryIconBubble()}</div>
</div>
</ha-card>
`}writeGenerationIconBubble(){const t=this.calculateIconBubbleData(["generation_to_grid_entity","generation_to_house_entity","generation_to_battery_entity"],"generation_entity","generation_extra_entity");return t.cssSelector="acc_top",t.icon=this.config.generation_icon,this.htmlWriter.writeBatteryBubbleDiv(t)}writeGridIconBubble(){const t=this.calculateIconBubbleData(["-generation_to_grid_entity","grid_to_house_entity","-battery_to_grid_entity","grid_to_battery_entity"],"grid_entity","grid_extra_entity");return t.cssSelector="acc_left",t.icon=this.config.grid_icon,this.htmlWriter.writeBatteryBubbleDiv(t)}writeHouseIconBubble(){let t;t=this.config.house_without_appliances_values?["generation_to_house_entity","grid_to_house_entity","battery_to_house_entity","-appliance1_consumption_entity","-appliance2_consumption_entity"]:["generation_to_house_entity","grid_to_house_entity","battery_to_house_entity"];const e=this.calculateIconBubbleData(t,"house_entity","house_extra_entity");return e.cssSelector="acc_right",e.icon=this.config.house_icon,this.htmlWriter.writeBatteryBubbleDiv(e)}writeBatteryIconBubble(){const t=this.calculateIconBubbleData(["generation_to_battery_entity","grid_to_battery_entity","-battery_to_house_entity","-battery_to_grid_entity"],"battery_entity","battery_extra_entity");return t.cssSelector="acc_bottom",t.icon=this.config.battery_icon,this.htmlWriter.writeBatteryBubbleDiv(t)}writeApplianceIconBubble(t){const e=["appliance"+t+"_consumption_entity"],i=this.calculateIconBubbleData(e,"appliance"+t+"_consumption_entity","appliance"+t+"_extra_entity");return i.cssSelector="acc_appliance"+t,i.icon=this.config["appliance"+t+"_icon"],this.htmlWriter.writeBatteryBubbleDiv(i)}calculateIconBubbleData(t,e=null,i=null){let n=!1;const s=new nt;if(s.clickEntitySlot=e,t.forEach((t=>{"-"===t.substring(0,1)&&(t=t.substring(1),n=!0);const e=this.solarCardElements.get(t);null!==e&&void 0!==(null==e?void 0:e.value)&&(s.noEntitiesWithValueFound=!1,s.mainValue=n?s.mainValue-(null==e?void 0:e.value):s.mainValue+(null==e?void 0:e.value),s.mainValue=(100*s.mainValue|0)/100,s.mainUnitOfMeasurement=null==e?void 0:e.unitOfMeasurement),n=!1})),null!==i){const t=this.solarCardElements.get(i);s.extraValue=null==t?void 0:t.value,s.extraUnitOfMeasurement=null==t?void 0:t.unitOfMeasurement}return null!==e&&(s.clickEntityHassState=this.hass.states[this.config[e]]),this.showKW(s.mainValue)&&(s.mainValue=this.roundValue(s.mainValue/1e3),s.mainUnitOfMeasurement="kW"),s}showKW(t){return!this.config.show_w_not_kw&&!(void 0!==this.config.threshold_in_k&&Math.abs(t)<1e3*this.config.threshold_in_k)}roundValue(t){let e;return e=t>.1?(0|Math.round(10*(t+Number.EPSILON)))/10:(0|Math.round(100*(t+Number.EPSILON)))/100,e}animateCircles(t){requestAnimationFrame((e=>{t.updateAllCircles(e)}))}updateAllCircles(t){this.solarCardElements.forEach(((e,i)=>{const n=this.solarCardElements.get(i);void 0!==n&&this.updateOneCircle(t,n)}))}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 n=e.line.getTotalLength();if(isNaN(n))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 s=e.currentDelta/n;e.speed>0?(s>=1||isNaN(s))&&(e.currentDelta=0,s=.01):(s=1-s,(s<=0||isNaN(s))&&(e.currentDelta=0,s=1));const o=e.line.getPointAtLength(n*s);e.circle.setAttributeNS(null,"cx",o.x.toString()),e.circle.setAttributeNS(null,"cy",o.y.toString()),e.prevTimestamp=t}colourHouseBubbleDependingOnHighestInput(){if(null==this.shadowRoot)return;const t=this.shadowRoot.querySelector("#tesla-style-solar-power-card");if(null==t)return;let e=null,i="";switch(["generation_to_house_entity","grid_to_house_entity","battery_to_house_entity"].forEach((t=>{const n=this.solarCardElements.get(t);null!==n&&void 0!==(null==n?void 0:n.value)&&(null==e||(null==n?void 0:n.value)>e.value)&&(i=t,e=n)})),i){case"generation_to_house_entity":this.colourBubble(".house_entity",t,"warning"),this.colourBubble(".appliance1_consumption_entity",t,"warning"),this.colourBubble(".appliance2_consumption_entity",t,"warning"),this.colourLineAndCircle("#appliance1_consumption_entity",t,"warning"),this.colourLineAndCircle("#appliance2_consumption_entity",t,"warning");break;case"battery_to_house_entity":this.colourBubble(".house_entity",t,"success"),this.colourBubble(".appliance1_consumption_entity",t,"success"),this.colourBubble(".appliance2_consumption_entity",t,"success"),this.colourLineAndCircle("#appliance1_consumption_entity",t,"success"),this.colourLineAndCircle("#appliance2_consumption_entity",t,"success");break;case"grid_to_house_entity":this.colourBubble(".house_entity",t,"info"),this.colourBubble(".appliance1_consumption_entity",t,"info"),this.colourBubble(".appliance2_consumption_entity",t,"info"),this.colourLineAndCircle("#appliance1_consumption_entity",t,"info"),this.colourLineAndCircle("#appliance2_consumption_entity",t,"info")}}colourBubble(t,e,i){const n=e.querySelector(t);null!==n&&(n.style.color="var(--"+i+"-color)",n.style.border="1px solid var(--"+i+"-color)")}colourLineAndCircle(t,e,i){const n=e.querySelector(t+"_line"),s=e.querySelector(t+"_circle");null!==n&&(n.style.stroke="var(--"+i+"-color)",s.style.fill="var(--"+i+"-color)")}setEnergyFlowDiagramm(){if(null==this.shadowRoot)return;const t=this.shadowRoot.querySelector("#tesla-style-solar-power-card");null!=t&&this.solarCardElements.forEach(((e,i)=>{const n=this.solarCardElements.get(i);let s=1;if(null==t)return;const o=t.querySelector("#"+i+"_line");if(null!=o&&void 0!==n){t.querySelector("#"+i+"_circle").style.visibility="hidden",void 0===this.config.energy_flow_diagramm_lines_factor&&(this.config.energy_flow_diagramm_lines_factor=2),s="W"===(null==n?void 0:n.unitOfMeasurement.toUpperCase())?Math.floor((null==n?void 0:n.value)/100)/10*this.config.energy_flow_diagramm_lines_factor:Math.floor(10*(null==n?void 0:n.value))/10*this.config.energy_flow_diagramm_lines_factor,s<=.1&&0!==s&&(s=.1),o.style.strokeWidth=s+"px"}}))}redraw(t){if(this.hass&&this.config&&"resize"===t.type){const t=this.getBoundingClientRect().width;this.oldWidth=ot.changeStylesDependingOnWidth(this,this.solarCardElements,t,this.oldWidth)}}_showWarning(t){return O` <hui-warning>${t}</hui-warning> `}_showError(){return console.log(this.error),O`
<hui-warning
><div>
ERROR:<br />
${this.error}
</div></hui-warning
>
`}static get styles(){return o`
#tesla-style-solar-power-card{
margin:auto;
display:table;
padding: 10px;
position: relative;
}
.acc_container {
height: 40px;
width: 40px;
border: 1px solid black;
border-radius: 100px;
padding: 22px;
color: var(--primary-text-color);
border-color: var(--primary-text-color);
position:relative;
cursor:pointer;
}
.acc_icon {
--mdc-icon-size: 40px;
}
.acc_text,
.acc_text_extra {
text-align: center;
white-space: nowrap;
}
.acc_text_extra {
overflow: hidden;
position: absolute;
}
.acc_td {
vertical-align: top;
}
.acc_center .acc_td{
position:relative;
}
.acc_top .acc_container,
.acc_bottom .acc_container{
margin:auto;
}
.acc_center{
display:flex;
}
.acc_center_container{
display:inline-block;
margin: 0px auto;
margin-bottom:-5px;
}
.acc_right ,
.acc_left ,
.acc_line{
display:inline-block;
margin-right:-4px
}
.acc_left {
vertical-align: top;
}
.acc_right {
margin-right:0px;
}
#battery_to_house_entity_line,
#generation_to_house_entity_line,
#grid_to_house_entity_line,
#generation_to_battery_entity_line,
#grid_feed_in_entity_line,
#generation_to_grid_entity_line,
#battery_to_grid_entity_line,
#grid_to_battery_entity_line,
#appliance1_consumption_entity_line,
#appliance2_consumption_entity_line{
stroke:var(--info-color);
fill:none;
stroke-width:1;
}
.generation_entity {
border: 1px solid var(--warning-color);
}
.generation_entity .acc_icon,
.generation_entity{
color: var(--warning-color);
}
.house_entity{
border: 1px solid var(--info-color);
}
.appliance1_consumption_entity,
.appliance2_consumption_entity {
border: 1px solid var(--info-color);
}
.house_entity,
.appliance1_consumption_entity,
.appliance2_consumption_entity{
color: var(--info-color);
}
#generation_to_house_entity_line,
#generation_to_grid_entity_line,
#generation_to_battery_entity_line{
stroke:var(--warning-color);
}
#grid_to_battery_entity_circle,
#grid_to_house_entity_circle,
#appliance1_consumption_entity_circle,
#appliance2_consumption_entity_circle{
fill:var(--info-color);
}
#generation_to_house_entity_circle,
#generation_to_grid_entity_circle,
#generation_to_battery_entity_circle{
fill:var(--warning-color);
}
#battery_to_house_entity_line,
#battery_to_grid_entity_line{
stroke:var(--success-color);
}
#battery_to_house_entity_circle,
#battery_to_grid_entity_circle{
fill:var(--success-color);
}
.battery_extra_entity,
.battery_entity{
border: 1px solid var(--success-color);
color: var(--success-color);
}
.battery_extra_text{
position:absolute;
top:8px;
}
br.clear {
clear:both;
}
.power_lines svg{
transform: translateZ(0);
display:inline-block;
}
.acc_center .acc_td.acc_appliance1,
.acc_center .acc_td.acc_appliance2 {
position: absolute;
right: 10px;
`}}t([tt({attribute:!1})],rt.prototype,"hass",void 0),t([tt()],rt.prototype,"config",void 0),t([tt({attribute:!1})],rt.prototype,"solarCardElements",void 0),t([tt()],rt.prototype,"oldWidth",void 0),t([tt({type:String})],rt.prototype,"title",void 0),t([tt({type:Number})],rt.prototype,"counter",void 0),window.customElements.define("tesla-style-solar-power-card",rt)}();
================================================
FILE: tesla-style-solar-power-card.ts
================================================
import { TeslaStyleSolarPowerCard } from './src/TeslaStyleSolarPowerCard.js';
window.customElements.define('tesla-style-solar-power-card', TeslaStyleSolarPowerCard);
================================================
FILE: test/SensorElement.test.ts
================================================
import { expect } from '@open-wc/testing';
import { SensorElement } from '../src/models/SensorElement.js';
// import { TeslaStyleSolarPowerCard } from '../src/TeslaStyleSolarPowerCard.js';
import '../tesla-style-solar-power-card.js';
describe('SensorElements test', () => {
it('should setSpeed with W', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.unitOfMeasurement = 'W';
selement.setSpeed(0.04);
expect(selement.speed).to.equal(0.00004);
});
it('should setSpeed with kW', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.unitOfMeasurement = 'KW';
selement.setSpeed(0.04);
expect(selement.speed).to.equal(0.00004);
});
it('should setSpeed with kW with factor 0.05', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.unitOfMeasurement = 'KW';
selement.setSpeed(0.05);
expect(selement.speed).to.equal(0.00005);
});
it('should setValueAndUnitOfMeasurement from kW rounded to 1 decimals', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.setValueAndUnitOfMeasurement('1.1111', 'kW');
expect(selement.value).to.equal(1111);
});
it('should setValueAndUnitOfMeasurement from 0.0111 kW to W', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.setValueAndUnitOfMeasurement('0.0111', 'kW');
expect(selement.value).to.equal(11);
});
it('should setValueAndUnitOfMeasurement from W rounded to two decimals get kW', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.setValueAndUnitOfMeasurement('1100.1', 'W');
expect(selement.value).to.equal(1100);
});
it('should setValueAndUnitOfMeasurement from W rounded to 1 decimals but get W', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.setValueAndUnitOfMeasurement('1111.111', 'W');
expect(selement.value).to.equal(1111);
});
it('should setValueAndUnitOfMeasurement from percentage', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.setValueAndUnitOfMeasurement('1', '%');
expect(selement.value).to.equal(1);
});
it('should setValueAndUnitOfMeasurement from undefined', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.setValueAndUnitOfMeasurement(undefined, '%');
expect(selement.value).to.equal(0);
});
it('should setValueAndUnitOfMeasurement from normal string without unit', () => {
const selement = new SensorElement('test_entity', 'solar_consumption');
selement.value = 1;
selement.setValueAndUnitOfMeasurement('on', undefined);
expect(selement.value).to.equal('on');
});
});
================================================
FILE: test/battery.test.ts
================================================
import { expect, elementUpdated, 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_entity: 'sensor.house_consumption',
battery_extra_entity: 'sensor.battery_charge',
battery_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_charge': {
attributes: {
unit_of_measurement: '%',
},
entity_id: 'sensor.battery_charge',
state: '100',
},
'sensor.battery_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'sensor.battery_to_house',
state: '1300',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
const setBatteryState = async (state: string) => {
hass.states['sensor.battery_charge'].state = state;
card.setAttribute('hass', JSON.stringify(hass));
await elementUpdated(card);
await card.setConfig(config);
};
it('has house_entity, text and icon', async () => {
const houseEntity = teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined)
assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'1.3 kW'
);
expect(
houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:home');
});
it('has battery_entity, text and icon', async () => {
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(batteryEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'1.3 kW'
);
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'100 %'
);
});
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_entity');
if (gridEntity !== null) assert.fail('No battery_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 at 90%', async () => {
await setBatteryState('90');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-90');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'90 %'
);
});
it('has battery at 83%', async () => {
await setBatteryState('83');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-90');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'83 %'
);
});
it('has battery at 73%', async () => {
await setBatteryState('73');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-80');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'73 %'
);
});
it('has battery at 65%', async () => {
await setBatteryState('65');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-70');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'65 %'
);
});
it('has battery at 15%', async () => {
await setBatteryState('15');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-20');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'15 %'
);
});
it('has battery at 6%', async () => {
await setBatteryState('6');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-10');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'6 %'
);
});
it('has battery at 5%', async () => {
await setBatteryState('5');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-outline');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'5 %'
);
});
it('has battery at 5%', async () => {
await setBatteryState('5');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-outline');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'5 %'
);
});
});
================================================
FILE: test/batteryCharging.test.ts
================================================
import { expect, elementUpdated, 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 with defaultConfig', () => {
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',
generation_to_battery_entity: 'sensor.battery_charging',
grid_to_battery_entity: 'sensor.grid_to_battery',
battery_extra_entity: 'sensor.battery_charge',
battery_entity: 'sensor.battery_consumption',
};
hass = {
states: {
'sensor.grid_to_battery': {
attributes: {
unit_of_measurement: 'W',
friendly_name: 'Grid to battery',
},
entity_id: 'sensor.grid_to_battery',
state: '1000',
},
'sensor.battery_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'battery_consumption',
state: '0',
},
'sensor.battery_charging': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'battery_charging',
state: '1000',
},
'sensor.battery_charge': {
attributes: {
unit_of_measurement: '%',
},
entity_id: 'sensor.battery_charge',
state: '99',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
const setBatteryState = async (state: string) => {
hass.states['sensor.battery_charge'].state = state;
card.setAttribute('hass', JSON.stringify(hass));
await elementUpdated(card);
await card.setConfig(config);
};
it('has battery_entity, text and icon', async () => {
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''),
'No sum of battery charging flows in acc_text of battery_entity'
).contains('2 kW');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'99 %'
);
});
it('has grid to battery charging line and circle', async () => {
const gridToBatteryLine = teslaCard?.querySelector(
'#grid_to_battery_entity_line'
);
if (gridToBatteryLine === null || gridToBatteryLine === undefined) {
assert.fail('No grid_to_battery_entity_line element found');
}
const gridToBatteryCircle = teslaCard?.querySelector(
'#grid_to_battery_entity_circle'
);
if (gridToBatteryCircle === null || gridToBatteryCircle === undefined) {
assert.fail('No grid_to_battery_entity_circle element found');
}
expect(gridToBatteryLine?.getAttribute('hidden')).to.equal(null);
});
it('has solar to battery charging line and circle', async () => {
const SolarToBatteryLine = teslaCard?.querySelector(
'#generation_to_battery_entity_line'
);
if (SolarToBatteryLine === null || SolarToBatteryLine === undefined) {
assert.fail('No generation_to_battery_entity_line element found');
}
const SolarToBatteryCircle = teslaCard?.querySelector(
'#generation_to_battery_entity_circle'
);
if (SolarToBatteryCircle === null || SolarToBatteryCircle === undefined) {
assert.fail('No generation_to_battery_entity_circle element found');
}
expect(SolarToBatteryLine?.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_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 at 100%', async () => {
await setBatteryState('100');
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'100 %'
);
});
it('has battery at 83%', async () => {
await setBatteryState('83');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-90');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'83 %'
);
});
it('has battery at 73%', async () => {
await setBatteryState('73');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-80');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'73 %'
);
});
it('has battery at 65%', async () => {
await setBatteryState('65');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-70');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'65 %'
);
});
it('has battery at 15%', async () => {
await setBatteryState('15');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-20');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'15 %'
);
});
it('has battery at 6%', async () => {
await setBatteryState('6');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-10');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'6 %'
);
});
it('has battery at 5%', async () => {
await setBatteryState('5');
card.requestUpdate();
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-charging-outline');
expect(batteryEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'5 %'
);
});
});
================================================
FILE: test/batteryWithoutExtra.test.ts
================================================
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_entity: 'sensor.house_consumption',
// battery_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 = <TeslaStyleSolarPowerCard>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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
it('has house_entity, text and icon', async () => {
const houseEntity = teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined)
assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'1.3 kW'
);
expect(
houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:home');
});
it('has battery_entity, text and icon', async () => {
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(batteryEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).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('.generation_entity');
if (pvEntity !== null) assert.fail('No generation_entity element found');
const gridEntity = teslaCard?.querySelector('.grid_consumption_entity');
if (gridEntity !== null) assert.fail('No battery_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_entity');
if (batteryEntity === null || batteryEntity === undefined)
assert.fail('No battery_entity element found');
expect(
batteryEntity
?.querySelector('.acc_icon')
?.getAttribute('icon')
?.toString()
).to.equal('mdi:battery-medium');
});
});
================================================
FILE: test/colouringOfBubblesDependingOnProduction.test.ts
================================================
import { expect, elementUpdated, 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('Colouring of bubble depending on production test', () => {
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',
generation_to_house_entity: 'sensor.generation_to_house',
battery_to_house_entity: 'sensor.battery_to_house',
grid_to_house_entity: 'sensor.grid_to_house',
change_house_bubble_color_with_flow: 1,
};
hass = {
states: {
'sensor.generation_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'generation_to_house',
state: '1100.1221',
},
'sensor.battery_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'battery_to_house',
state: '2051.1221',
},
'sensor.grid_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'grid_to_house',
state: '1050.1221',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
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 = <HTMLElement>haCard.querySelector('#tesla-style-solar-power-card');
if (teslaCard === null || teslaCard === undefined) assert.fail('No tesla-style-card');
});
it('house_icon shoud have success colour', async () => {
const houseEntity = <HTMLElement>teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined) assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''), 'sum of consumptions in mixed house consumption is wrong').contains(
'4.2 kW'
);
expect(houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()).to.equal('mdi:home');
expect(houseEntity.style.color).to.equal('var(--success-color)');
});
const setGenerationToHouseFlow = async (state: string) => {
hass.states['sensor.generation_to_house'].state = state;
card.setAttribute('hass', JSON.stringify(hass));
await elementUpdated(card);
await card.setConfig(config);
};
it('house_icon shoud have warning colour', async () => {
await setGenerationToHouseFlow('2500');
const houseEntity = <HTMLElement>teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined) assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''), 'sum of consumptions in mixed house consumption is wrong').contains(
'5.6 kW'
);
expect(houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()).to.equal('mdi:home');
expect(houseEntity.style.color).to.equal('var(--warning-color)');
});
const setGridToHouseFlow = async (state: string) => {
hass.states['sensor.grid_to_house'].state = state;
card.setAttribute('hass', JSON.stringify(hass));
await elementUpdated(card);
await card.setConfig(config);
};
it('house_icon shoud have no colour', async () => {
await setGridToHouseFlow('3500');
const houseEntity = <HTMLElement>teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined) assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''), 'sum of consumptions in mixed house consumption is wrong').contains(
'6.7 kW'
);
expect(houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()).to.equal('mdi:home');
expect(houseEntity.style.color).to.equal('var(--info-color)');
});
});
================================================
FILE: test/defaultConfig.test.ts
================================================
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 with defaultConfig', () => {
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',
grid_to_house_entity: 'sensor.grid_to_house',
grid_entity: 'sensor.grid_consumption',
};
hass = {
states: {
'sensor.grid_to_house': {
attributes: {
unit_of_measurement: 'w',
},
entity_id: 'sensor.grid_to_house',
state: '500.000000001',
},
'sensor.grid_consumption': {
attributes: {
unit_of_measurement: 'w',
},
entity_id: 'sensor.grid_consumption',
state: '500.000000001',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
it('has card size 5', () => {
expect(card.getCardSize()).to.equal(5);
});
/* it('has a title "Powerhouse"', async () => {
if(haCard === null || haCard === undefined) assert.fail("No ha-card");
if(haCard.shadowRoot === null) assert.fail(haCard.outerHTML);
console.log(haCard);
expect(haCard.shadowRoot.querySelector('h1')?.innerText).is.equal('Powerhouse');
}); */
it('has no warnings or errors', async () => {
if (card.shadowRoot === null) assert.fail('No Card Shadowroot');
console.log(card.innerHTML);
expect(card.shadowRoot.querySelectorAll('.message').length).to.equal(0);
});
it('has grid_entity and icon', async () => {
const gridEntity = teslaCard?.querySelector('.grid_entity');
if (gridEntity === null || gridEntity === undefined)
assert.fail('No grid_entity element found');
expect(gridEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'0.5 kW'
);
expect(
gridEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:transmission-tower');
});
it('has house_entity, text and icon', async () => {
const houseEntity = teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined)
assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains(
'0.5 kW'
);
expect(
houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:home');
});
it('has grid to house consumption line and circle', async () => {
// await setCardConsumingFromGrid();
const gridConsumptionLine = teslaCard?.querySelector(
'#grid_to_house_entity_line'
);
if (gridConsumptionLine === null || gridConsumptionLine === undefined) {
assert.fail('No grid_to_house_entity_line element found');
}
const gridConsumptionCircle = teslaCard?.querySelector(
'#grid_to_house_entity_circle'
);
if (gridConsumptionCircle === null || gridConsumptionCircle === undefined) {
assert.fail('No grid_to_house_entity_circle element found');
}
if (haCard === null || haCard === undefined) assert.fail('No ha-card');
expect(gridConsumptionLine?.getAttribute('hidden')).to.equal(null);
});
it('has no pv, battery or appliance icons', async () => {
const pvEntity = teslaCard?.querySelector('.generation_entity');
if (pvEntity !== null) assert.fail('No generation_entity element found');
const batteryEntity = teslaCard?.querySelector('.battery_entity');
if (batteryEntity !== null) assert.fail('No battery_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');
});
});
================================================
FILE: test/extraAppliances.test.ts
================================================
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 with extra appliances', () => {
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_entity: 'sensor.house_consumption',
grid_to_house_entity: 'sensor.grid_to_house',
appliance1_consumption_entity: 'sensor.car_consumption',
appliance1_extra_entity: 'sensor.car_soc',
appliance2_consumption_entity: 'sensor.heating_consumption',
appliance2_extra_entity: 'sensor.heating_current_function',
};
hass = {
states: {
'sensor.heating_consumption': {
attributes: {
unit_of_measurement: 'W',
friendly_name: 'Heating consumption',
},
entity_id: 'heating_consumption',
state: '1000',
},
'sensor.heating_current_function': {
attributes: {
unit_of_measurement: null,
friendly_name: 'Heating function',
},
entity_id: 'heating_current_function',
state: 'Warm water',
},
'sensor.car_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'car_consumption',
state: '2000',
},
'sensor.car_soc': {
attributes: {
unit_of_measurement: '%',
},
entity_id: 'car_soc',
state: '90',
},
'sensor.house_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'house_consumption',
state: '4000',
},
'sensor.grid_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'grid_to_house',
state: '4000',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
it('has house_entity, text and icon', async () => {
const gridEntity = teslaCard?.querySelector('.house_entity');
if (gridEntity === null || gridEntity === undefined)
assert.fail('No house_entity element found');
expect(gridEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains('4 kW');
expect(
gridEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:home');
});
it('has appliance1_consumption_entity, text and icon', async () => {
const gridEntity = teslaCard?.querySelector(
'.appliance1_consumption_entity'
);
if (gridEntity === null || gridEntity === undefined)
assert.fail('No appliance1_consumption_entity element found');
expect(
gridEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''),
'No sum of appliance1 charging flows in acc_text of grid_entity'
).contains('2 kW');
expect(
gridEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:car-sports');
expect(
gridEntity?.querySelector('.acc_text_extra')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''),
'Appliance 1 extra text is wrong'
).contains('90 %');
});
it('has appliance2_consumption_entity, text and icon', async () => {
const gridEntity = teslaCard?.querySelector(
'.appliance2_consumption_entity'
);
if (gridEntity === null || gridEntity === undefined)
assert.fail('No appliance2_consumption_entity element found');
expect(
gridEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''),
'No sum of appliance2 charging flows in acc_text of grid_entity'
).contains('1 kW');
expect(
gridEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:air-filter');
expect(
gridEntity?.querySelector('.acc_text_extra')?.innerHTML,
'Appliance 2 extra text is wrong'
).contains('Warm water');
});
it('has appliance1 line and circle', async () => {
const appliance = teslaCard?.querySelector(
'#appliance1_consumption_entity_line'
);
if (appliance === null || appliance === undefined) {
assert.fail('No appliance1_consumption_entity_line element found');
}
const applianceCircle = teslaCard?.querySelector(
'#appliance1_consumption_entity_circle'
);
if (applianceCircle === null || applianceCircle === undefined) {
assert.fail('No appliance1_consumption_entity_circle element found');
}
expect(appliance?.getAttribute('hidden')).to.equal(null);
});
it('has appliance2 line and circle', async () => {
const applianceLine = teslaCard?.querySelector(
'#appliance2_consumption_entity_line'
);
if (applianceLine === null || applianceLine === undefined) {
assert.fail('No appliance2_consumption_entity_line element found');
}
const applianceCircle = teslaCard?.querySelector(
'#appliance1_consumption_entity_circle'
);
if (applianceCircle === null || applianceCircle === undefined) {
assert.fail('No appliance2_consumption_entity_circle element found');
}
expect(applianceLine?.getAttribute('hidden')).to.equal(null);
});
it('has no pv, grid or battery icons', async () => {
// assert.fail(haCard?.innerHTML);
const pvEntity = teslaCard?.querySelector('.generation_yield_entity');
if (pvEntity !== null)
assert.fail('pv_consumption_entity element found, should not be there');
const batteryEntity = teslaCard?.querySelector(
'.battery_consumption_entity'
);
if (batteryEntity !== null)
assert.fail(
'battery_consumption_entity element found, should not be there'
);
const gridEntity = teslaCard?.querySelector('.grid_consumption_entity');
if (gridEntity !== null)
assert.fail('grid_consumption_entity element found, should not be there');
});
});
================================================
FILE: test/extraAppliancesNotInHouse.test.ts
================================================
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 with extra appliances', () => {
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_entity: 'sensor.house_consumption',
grid_to_house_entity: 'sensor.grid_to_house',
appliance1_consumption_entity: 'sensor.car_consumption',
appliance1_extra_entity: 'sensor.car_soc',
appliance2_consumption_entity: 'sensor.heating_consumption',
appliance2_extra_entity: 'sensor.heating_current_function',
house_without_appliances_values: 'sensor.heating_current_function',
};
hass = {
states: {
'sensor.heating_consumption': {
attributes: {
unit_of_measurement: 'W',
friendly_name: 'Heating consumption',
},
entity_id: 'heating_consumption',
state: '1000',
},
'sensor.heating_current_function': {
attributes: {
unit_of_measurement: null,
friendly_name: 'Heating function',
},
entity_id: 'heating_current_function',
state: 'Warm water',
},
'sensor.car_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'car_consumption',
state: '2000',
},
'sensor.car_soc': {
attributes: {
unit_of_measurement: '%',
},
entity_id: 'car_soc',
state: '90',
},
'sensor.house_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'house_consumption',
state: '4000',
},
'sensor.grid_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'grid_to_house',
state: '4000',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
it('has house_entity, text and icon', async () => {
const houseEntity = teslaCard?.querySelector('.house_entity');
if (houseEntity === null || houseEntity === undefined)
assert.fail('No house_entity element found');
expect(houseEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, '')).contains('1 kW');
expect(
houseEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:home');
});
});
================================================
FILE: test/gridFeed.test.ts
================================================
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 with defaultConfig', () => {
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',
generation_to_grid_entity: 'sensor.generation_to_grid',
battery_to_grid_entity: 'sensor.battery_to_grid',
grid_entity: 'sensor.grid_consumption',
};
hass = {
states: {
'sensor.battery_to_grid': {
attributes: {
unit_of_measurement: 'W',
friendly_name: 'Batter to battery',
},
entity_id: 'sensor.battery_to_grid',
state: '1000',
},
'sensor.grid_consumption': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'grid_consumption',
state: '0',
},
'sensor.generation_to_grid': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'generation_to_grid',
state: '1000',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
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 = <HTMLElement>(
haCard.querySelector('#tesla-style-solar-power-card')
);
if (teslaCard === null || teslaCard === undefined)
assert.fail('No tesla-style-card');
});
it('has grid_entity, text and icon', async () => {
const gridEntity = teslaCard?.querySelector('.grid_entity');
if (gridEntity === null || gridEntity === undefined)
assert.fail('No grid_entity element found');
expect(
gridEntity?.querySelector('.acc_text')?.innerHTML.replace(/<!--[^(-->)]+-->/g, ''),
'No sum of grid charging flows in acc_text of grid_entity'
).contains('2 kW');
expect(
gridEntity?.querySelector('.acc_icon')?.getAttribute('icon')?.toString()
).to.equal('mdi:transmission-tower');
});
it('has battery to grid feed in line and circle', async () => {
const batteryToGridLine = teslaCard?.querySelector(
'#battery_to_grid_entity_line'
);
if (batteryToGridLine === null || batteryToGridLine === undefined) {
assert.fail('No battery_to_grid_entity_line element found');
}
const batteryToGridCircle = teslaCard?.querySelector(
'#battery_to_grid_entity_circle'
);
if (batteryToGridCircle === null || batteryToGridCircle === undefined) {
assert.fail('No battery_to_grid_entity_circle element found');
}
expect(batteryToGridLine?.getAttribute('hidden')).to.equal(null);
});
it('has solar to grid line and circle', async () => {
const solarToGridLine = teslaCard?.querySelector(
'#generation_to_grid_entity_line'
);
if (solarToGridLine === null || solarToGridLine === undefined) {
assert.fail('No generation_to_battery_entity_line element found');
}
const solarToGridCircle = teslaCard?.querySelector(
'#generation_to_grid_entity_circle'
);
if (solarToGridCircle === null || solarToGridCircle === undefined) {
assert.fail('No generation_to_battery_entity_circle element found');
}
expect(solarToGridLine?.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 batteryEntity = teslaCard?.querySelector(
'.battery_consumption_entity'
);
if (batteryEntity !== null)
assert.fail('No battery_consumption_entity element found');
const houseEntity = teslaCard?.querySelector('.house_consumption_entity');
if (houseEntity !== null)
assert.fail('No house_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');
});
});
================================================
FILE: test/setters.ts
================================================
import {elementUpdated, fixture, html} from "@open-wc/testing";
import { TeslaStyleSolarPowerCard } from "../src/TeslaStyleSolarPowerCard.js";
const setCard = async (hass:any, config:any) => {
const card = await fixture<TeslaStyleSolarPowerCard>(
html`
<tesla-style-solar-power-card .hass=${hass} .config=${{}}></tesla-style-solar-power-card>
`
);
await card.setConfig(config);
// Call firstUpdated() again because fixture already triggered it the first time.
await card.firstUpdated();
// TODO: Why is this needed for one test case only: 'has debug warning'?
await card.setConfig(config);
return card;
};
const setCardView = async (card:any, view:any) => {
card.setAttribute('view', view);
};
const setCardAllInactive = async (card:any, hass:any, config:any) => {
hass.states['sensor.solar_power'].state = "0";
if (hass.states['sensor.grid_power']) {
hass.states['sensor.grid_power'].state = "0";
}
if (hass.states['sensor.grid_power_consumption']) {
hass.states['sensor.grid_power_consumption'].state = "0";
}
if (hass.states['sensor.grid_power_production']) {
hass.states['sensor.grid_power_production'].state = "0";
}
if (hass.states['sensor.battery_power']) {
hass.states['sensor.battery_power'].state = "0";
}
card.setAttribute('hass', JSON.stringify(hass));
await elementUpdated(card);
await card.setConfig(config);
};
export {setCard, setCardView, setCardAllInactive};
================================================
FILE: test/solarProduction.test.ts
================================================
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 with solarConfig', () => {
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',
generation_to_grid_entity: 'sensor.generation_to_grid',
generation_to_battery_entity: 'sensor.generation_to_battery',
generation_to_house_entity: 'sensor.generation_to_house',
generation_entity: 'sensor.generation_entity',
};
hass = {
states: {
'sensor.generation_to_battery': {
attributes: {
unit_of_measurement: 'W',
friendly_name: 'Batter to battery',
},
entity_id: 'sensor.generation_to_battery',
state: '3100.211',
},
'sensor.generation_entity': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'generation_entity',
state: '8100',
},
'sensor.generation_to_grid': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'generation_to_grid',
state: '4000.0000011221',
},
'sensor.generation_to_house': {
attributes: {
unit_of_measurement: 'W',
},
entity_id: 'generation_to_house',
state: '1000.1221',
},
},
};
await setViewport({ width: 1200, height: 1000 });
card = <TeslaStyleSolarPowerCard>await setCard(hass, config);
if (car
gitextract_1qkmhazr/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── validate.yaml ├── .gitignore ├── LICENSE-2.0.txt ├── README.md ├── hacs.json ├── package.json ├── rollup.config.js ├── src/ │ ├── TeslaStyleSolarPowerCard.ts │ ├── components/ │ │ └── editor.ts │ ├── localize/ │ │ ├── languages/ │ │ │ ├── de.json │ │ │ └── en.json │ │ └── localize.ts │ ├── models/ │ │ ├── BubbleData.ts │ │ ├── SensorElement.ts │ │ └── TeslaStyleSolarPowerCardConfig.ts │ ├── services/ │ │ ├── HtmlResizeForPowerCard.ts │ │ └── htmlWriterForPowerCard.ts │ ├── translations/ │ │ ├── languages/ │ │ │ ├── de.json │ │ │ └── en.json │ │ └── localize.ts │ └── types.ts ├── tesla-style-solar-power-card.js ├── tesla-style-solar-power-card.ts ├── test/ │ ├── SensorElement.test.ts │ ├── battery.test.ts │ ├── batteryCharging.test.ts │ ├── batteryWithoutExtra.test.ts │ ├── colouringOfBubblesDependingOnProduction.test.ts │ ├── defaultConfig.test.ts │ ├── extraAppliances.test.ts │ ├── extraAppliancesNotInHouse.test.ts │ ├── gridFeed.test.ts │ ├── setters.ts │ ├── solarProduction.test.ts │ ├── tesla-style-solar-power-card.test.ts │ └── threshold.test.ts ├── tsconfig.json ├── web-dev-server.config.mjs └── web-test-runner.config.mjs
SYMBOL INDEX (234 symbols across 11 files)
FILE: src/TeslaStyleSolarPowerCard.ts
class TeslaStyleSolarPowerCard (line 22) | class TeslaStyleSolarPowerCard extends LitElement {
method __increment (line 41) | __increment() {
method setConfig (line 47) | public setConfig(config: LovelaceCardConfig): void {
method createSolarCardElements (line 75) | private createSolarCardElements(): void {
method getCardSize (line 85) | public getCardSize() {
method getStubConfig (line 95) | public static getStubConfig(): Record<string, any> {
method firstUpdated (line 100) | async firstUpdated(): Promise<void> {
method connectedCallback (line 107) | public connectedCallback(): void {
method shouldUpdate (line 113) | public shouldUpdate(changedProperties: any): boolean {
method sensorChangeDetected (line 135) | private sensorChangeDetected(oldValue: any): boolean {
method performUpdate (line 148) | public async performUpdate(): Promise<void> {
method render (line 171) | protected render(): TemplateResult | void {
method writeGenerationIconBubble (line 350) | private writeGenerationIconBubble(): TemplateResult {
method writeGridIconBubble (line 364) | private writeGridIconBubble(): TemplateResult {
method writeHouseIconBubble (line 378) | private writeHouseIconBubble(): TemplateResult {
method writeBatteryIconBubble (line 400) | private writeBatteryIconBubble(): TemplateResult {
method writeApplianceIconBubble (line 418) | private writeApplianceIconBubble(applianceNumber: number): TemplateRes...
method calculateIconBubbleData (line 433) | private calculateIconBubbleData(
method showKW (line 476) | private showKW(value: number) {
method roundValue (line 487) | private roundValue(value: number): number {
method animateCircles (line 498) | private animateCircles(obj: any) {
method updateAllCircles (line 504) | public updateAllCircles(timestamp: number): void {
method updateOneCircle (line 512) | private updateOneCircle(timestamp: number, entity: SensorElement) {
method colourHouseBubbleDependingOnHighestInput (line 557) | private colourHouseBubbleDependingOnHighestInput() {
method colourBubble (line 602) | private colourBubble(elementName: string, teslaCardElement: HTMLElemen...
method colourLineAndCircle (line 611) | private colourLineAndCircle(elementName: string, teslaCardElement: HTM...
method setEnergyFlowDiagramm (line 620) | private setEnergyFlowDiagramm() {
method redraw (line 647) | private redraw(ev: UIEvent) {
method _showWarning (line 655) | private _showWarning(warning: string): TemplateResult {
method _showError (line 659) | private _showError(): TemplateResult {
method styles (line 675) | static get styles(): CSSResult {
FILE: src/components/editor.ts
class TeslaStyleSolarPowerCardEditor (line 51) | class TeslaStyleSolarPowerCardEditor extends LitElement implements Lovel...
method setConfig (line 64) | public setConfig(config: LovelaceCardConfig): void {
method shouldUpdate (line 71) | protected shouldUpdate(): boolean {
method name (line 79) | get name(): string {
method home_entity (line 84) | get home_entity(): string {
method battery_entity (line 88) | get battery_entity(): string {
method grid_entity (line 92) | get grid_entity(): string {
method generation_entity (line 96) | get generation_entity(): string {
method home_extra_entity (line 100) | get home_extra_entity(): string {
method battery_extra_entity (line 104) | get battery_extra_entity(): string {
method grid_extra_entity (line 108) | get grid_extra_entity(): string {
method generation_extra_entity (line 112) | get generation_extra_entity(): string {
method grid_to_house_entity (line 116) | get grid_to_house_entity(): string {
method grid_to_battery_entity (line 120) | get grid_to_battery_entity(): string {
method battery_to_grid_in_entity (line 124) | get battery_to_grid_in_entity(): string {
method generation_to_grid_entity (line 128) | get generation_to_grid_entity(): string {
method generation_to_house_entity (line 132) | get generation_to_house_entity(): string {
method generation_to_battery_entity (line 136) | get generation_to_battery_entity(): string {
method appliance1_consumption_entity (line 140) | get appliance1_consumption_entity(): string {
method appliance1_extra_entity (line 144) | get appliance1_extra_entity(): string {
method appliance2_consumption_entity (line 148) | get appliance2_consumption_entity(): string {
method appliance2_extra_entity (line 152) | get appliance2_extra_entity(): string {
method show_w_not_kw (line 156) | get show_w_not_kw(): boolean {
method show_warning (line 160) | get show_warning(): boolean {
method show_error (line 164) | get show_error(): boolean {
method hide_inactive_lines (line 168) | get hide_inactive_lines(): boolean {
method tap_action (line 172) | get tap_action(): ActionConfig {
method hold_action (line 176) | get hold_action(): ActionConfig {
method double_tap_action (line 180) | get double_tap_action(): ActionConfig {
method render (line 184) | protected render(): TemplateResult | void {
method _initialize (line 326) | private _initialize(): void {
method loadCardHelpers (line 333) | private async loadCardHelpers(): Promise<void> {
method _toggleAction (line 337) | private _toggleAction(ev: any): void {
method _toggleOption (line 341) | private _toggleOption(ev: any): void {
method _toggleThing (line 345) | private _toggleThing(ev: any, optionList: any): void {
method _valueChanged (line 354) | private _valueChanged(ev: any): void {
method _fillLineEntityMap (line 375) | private _fillLineEntityMap() {
method _fillIconEntityMap (line 397) | private _fillIconEntityMap() {
method styles (line 408) | static get styles(): CSSResult {
FILE: src/localize/localize.ts
function localize (line 10) | function localize(string: string, search = '', replace = ''): string {
FILE: src/models/BubbleData.ts
class BubbleData (line 3) | class BubbleData {
FILE: src/models/SensorElement.ts
class SensorElement (line 2) | class SensorElement {
method constructor (line 39) | constructor(entity: string, enitySlot: string) {
method setValueAndUnitOfMeasurement (line 45) | public setValueAndUnitOfMeasurement(entityState: string | undefined, u...
method setSpeed (line 78) | public setSpeed(factor: number | undefined): void {
FILE: src/models/TeslaStyleSolarPowerCardConfig.ts
type TeslaStyleSolarPowerCardConfig (line 4) | interface TeslaStyleSolarPowerCardConfig extends LovelaceCardConfig {
FILE: src/services/HtmlResizeForPowerCard.ts
class HtmlResizeForPowerCard (line 5) | class HtmlResizeForPowerCard {
method changeStylesDependingOnWidth (line 6) | public static changeStylesDependingOnWidth(
FILE: src/services/htmlWriterForPowerCard.ts
class HtmlWriterForPowerCard (line 9) | class HtmlWriterForPowerCard {
method constructor (line 18) | public constructor(teslaCard: TeslaStyleSolarPowerCard, hass: HomeAssi...
method writeBubbleDiv (line 25) | public writeBubbleDiv(bubbleData: BubbleData
method writeBatteryBubbleDiv (line 53) | public writeBatteryBubbleDiv(bubbleData:BubbleData): TemplateResult {
method getBatteryIcon (line 62) | private getBatteryIcon(batteryValue: number, batteryChargeDischargeVal...
method writeAppliancePowerLineAndCircle (line 80) | public writeAppliancePowerLineAndCircle(applianceNumber: number, pathD...
method writeCircleAndLine (line 111) | public writeCircleAndLine(sensorName: string, pathDAttribute: string) {
method _handleClick (line 120) | private _handleClick(stateObj: HassEntity | null) {
FILE: src/translations/localize.ts
function localize (line 10) | function localize(string: string, search = '', replace = ''): string {
FILE: src/types.ts
type HTMLElementTagNameMap (line 4) | interface HTMLElementTagNameMap {
FILE: tesla-style-solar-power-card.js
function t (line 15) | function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.g...
class s (line 20) | class s{constructor(t,e){if(this._$cssResult$=!0,e!==i)throw Error("CSSR...
method constructor (line 20) | constructor(t,e){if(this._$cssResult$=!0,e!==i)throw Error("CSSResult ...
method styleSheet (line 20) | get styleSheet(){let t=n.get(this.cssText);return e&&void 0===t&&(n.se...
method toString (line 20) | toString(){return this.cssText}
method toAttribute (line 25) | toAttribute(t,e){switch(e){case Boolean:t=t?c:null;break;case Object:cas...
method fromAttribute (line 25) | fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case ...
class _ (line 25) | class _ extends HTMLElement{constructor(){super(),this._$Et=new Map,this...
method constructor (line 25) | constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.h...
method addInitializer (line 25) | static addInitializer(t){var e;null!==(e=this.l)&&void 0!==e||(this.l=...
method observedAttributes (line 25) | static get observedAttributes(){this.finalize();const t=[];return this...
method createProperty (line 25) | static createProperty(t,e=p){if(e.state&&(e.attribute=!1),this.finaliz...
method getPropertyDescriptor (line 25) | static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n...
method getPropertyOptions (line 25) | static getPropertyOptions(t){return this.elementProperties.get(t)||p}
method finalize (line 25) | static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.fi...
method finalizeStyles (line 25) | static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new S...
method _$Eh (line 25) | static _$Eh(t,e){const i=e.attribute;return!1===i?void 0:"string"==typ...
method o (line 25) | o(){var t;this._$Ep=new Promise((t=>this.enableUpdating=t)),this._$AL=...
method addController (line 25) | addController(t){var e,i;(null!==(e=this._$Eg)&&void 0!==e?e:this._$Eg...
method removeController (line 25) | removeController(t){var e;null===(e=this._$Eg)||void 0===e||e.splice(t...
method _$Em (line 25) | _$Em(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwn...
method createRenderRoot (line 25) | createRenderRoot(){var t;const i=null!==(t=this.shadowRoot)&&void 0!==...
method connectedCallback (line 25) | connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=t...
method enableUpdating (line 25) | enableUpdating(t){}
method disconnectedCallback (line 25) | disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEa...
method attributeChangedCallback (line 25) | attributeChangedCallback(t,e,i){this._$AK(t,i)}
method _$ES (line 25) | _$ES(t,e,i=p){var n,s;const o=this.constructor._$Eh(t,i);if(void 0!==o...
method _$AK (line 25) | _$AK(t,e){var i,n,s;const o=this.constructor,r=o._$Eu.get(t);if(void 0...
method requestUpdate (line 25) | requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.get...
method _$E_ (line 25) | async _$E_(){this.isUpdatePending=!0;try{await this._$Ep}catch(t){Prom...
method scheduleUpdate (line 25) | scheduleUpdate(){return this.performUpdate()}
method performUpdate (line 25) | performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,...
method willUpdate (line 25) | willUpdate(t){}
method _$AE (line 25) | _$AE(t){var e;null===(e=this._$Eg)||void 0===e||e.forEach((t=>{var e;r...
method _$EU (line 25) | _$EU(){this._$AL=new Map,this.isUpdatePending=!1}
method updateComplete (line 25) | get updateComplete(){return this.getUpdateComplete()}
method getUpdateComplete (line 25) | getUpdateComplete(){return this._$Ep}
method shouldUpdate (line 25) | shouldUpdate(t){return!0}
method update (line 25) | update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,e)=>this._$ES(e,t...
method updated (line 25) | updated(t){}
method firstUpdated (line 25) | firstUpdated(t){}
class D (line 31) | class D{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let ...
method constructor (line 31) | constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let s=0,o=...
method createElement (line 31) | static createElement(t,e){const i=$.createElement("template");return i...
function W (line 31) | function W(t,e,i=t,n){var s,o,r,l;if(e===P)return e;let a=void 0!==n?nul...
class L (line 31) | class L{constructor(t,e){this.v=[],this._$AN=void 0,this._$AD=t,this._$A...
method constructor (line 31) | constructor(t,e){this.v=[],this._$AN=void 0,this._$AD=t,this._$AM=e}
method parentNode (line 31) | get parentNode(){return this._$AM.parentNode}
method _$AU (line 31) | get _$AU(){return this._$AM._$AU}
method p (line 31) | p(t){var e;const{el:{content:i},parts:n}=this._$AD,s=(null!==(e=null==...
method m (line 31) | m(t){let e=0;for(const i of this.v)void 0!==i&&(void 0!==i.strings?(i....
class I (line 31) | class I{constructor(t,e,i,n){var s;this.type=2,this._$AH=U,this._$AN=voi...
method constructor (line 31) | constructor(t,e,i,n){var s;this.type=2,this._$AH=U,this._$AN=void 0,th...
method _$AU (line 31) | get _$AU(){var t,e;return null!==(e=null===(t=this._$AM)||void 0===t?v...
method parentNode (line 31) | get parentNode(){let t=this._$AA.parentNode;const e=this._$AM;return v...
method startNode (line 31) | get startNode(){return this._$AA}
method endNode (line 31) | get endNode(){return this._$AB}
method _$AI (line 31) | _$AI(t,e=this){t=W(this,t,e),x(t)?t===U||null==t||""===t?(this._$AH!==...
method A (line 31) | A(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}
method k (line 31) | k(t){this._$AH!==t&&(this._$AR(),this._$AH=this.A(t))}
method $ (line 31) | $(t){this._$AH!==U&&x(this._$AH)?this._$AA.nextSibling.data=t:this.k($...
method T (line 31) | T(t){var e;const{values:i,_$litType$:n}=t,s="number"==typeof n?this._$...
method _$AC (line 31) | _$AC(t){let e=T.get(t.strings);return void 0===e&&T.set(t.strings,e=ne...
method S (line 31) | S(t){A(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,...
method _$AR (line 31) | _$AR(t=this._$AA.nextSibling,e){var i;for(null===(i=this._$AP)||void 0...
method setConnected (line 31) | setConnected(t){var e;void 0===this._$AM&&(this._$Cg=t,null===(e=this....
class V (line 31) | class V{constructor(t,e,i,n,s){this.type=1,this._$AH=U,this._$AN=void 0,...
method constructor (line 31) | constructor(t,e,i,n,s){this.type=1,this._$AH=U,this._$AN=void 0,this.e...
method tagName (line 31) | get tagName(){return this.element.tagName}
method _$AU (line 31) | get _$AU(){return this._$AM._$AU}
method _$AI (line 31) | _$AI(t,e=this,i,n){const s=this.strings;let o=!1;if(void 0===s)t=W(thi...
method C (line 31) | C(t){t===U?this.element.removeAttribute(this.name):this.element.setAtt...
class q (line 31) | class q extends V{constructor(){super(...arguments),this.type=3}C(t){thi...
method constructor (line 31) | constructor(){super(...arguments),this.type=3}
method C (line 31) | C(t){this.element[this.name]=t===U?void 0:t}
class j (line 31) | class j extends V{constructor(){super(...arguments),this.type=4}C(t){t&&...
method constructor (line 31) | constructor(){super(...arguments),this.type=4}
method C (line 31) | C(t){t&&t!==U?this.element.setAttribute(this.name,z):this.element.remo...
class F (line 31) | class F extends V{constructor(t,e,i,n,s){super(t,e,i,n,s),this.type=5}_$...
method constructor (line 31) | constructor(t,e,i,n,s){super(t,e,i,n,s),this.type=5}
method _$AI (line 31) | _$AI(t,e=this){var i;if((t=null!==(i=W(this,t,e,0))&&void 0!==i?i:U)==...
method handleEvent (line 31) | handleEvent(t){var e,i;"function"==typeof this._$AH?this._$AH.call(nul...
class K (line 31) | class K{constructor(t,e,i){this.element=t,this.type=6,this._$AN=void 0,t...
method constructor (line 31) | constructor(t,e,i){this.element=t,this.type=6,this._$AN=void 0,this._$...
method _$AU (line 31) | get _$AU(){return this._$AM._$AU}
method _$AI (line 31) | _$AI(t){W(this,t)}
class Y (line 37) | class Y extends _{constructor(){super(...arguments),this.renderOptions={...
method constructor (line 37) | constructor(){super(...arguments),this.renderOptions={host:this},this....
method createRenderRoot (line 37) | createRenderRoot(){var t,e;const i=super.createRenderRoot();return nul...
method update (line 37) | update(t){const e=this.render();this.hasUpdated||(this.renderOptions.i...
method connectedCallback (line 37) | connectedCallback(){var t;super.connectedCallback(),null===(t=this._$D...
method disconnectedCallback (line 37) | disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=th...
method render (line 37) | render(){return P}
method finisher (line 43) | finisher(i){i.createProperty(e.key,t)}
method initializer (line 43) | initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializ...
method finisher (line 43) | finisher(i){i.createProperty(e.key,t)}
function tt (line 43) | function tt(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createPr...
class it (line 48) | class it{constructor(t,e){this.speed=0,this.startPosition=0,this.current...
method constructor (line 48) | constructor(t,e){this.speed=0,this.startPosition=0,this.currentPositio...
method setValueAndUnitOfMeasurement (line 48) | setValueAndUnitOfMeasurement(t,e){if(void 0===t)return void(this.value...
method setSpeed (line 48) | setSpeed(t){if(this.speed=0,0===Math.abs(this.value))return;let e;e=vo...
class nt (line 48) | class nt{constructor(){this.mainValue=0,this.clickEntitySlot=null,this.c...
method constructor (line 48) | constructor(){this.mainValue=0,this.clickEntitySlot=null,this.clickEnt...
class st (line 48) | class st{constructor(t,e){this.teslaCard=t,this.solarCardElements=t.sola...
method constructor (line 48) | constructor(t,e){this.teslaCard=t,this.solarCardElements=t.solarCardEl...
method writeBubbleDiv (line 48) | writeBubbleDiv(t){return t.noEntitiesWithValueFound?O``:O` <div class=...
method writeBatteryBubbleDiv (line 66) | writeBatteryBubbleDiv(t){return void 0!==t.extraValue&&("mdi:battery-m...
method getBatteryIcon (line 66) | getBatteryIcon(t,e){let i=t;t<=5&&(i=0);const n=10*Math.ceil(i/10);let...
method writeAppliancePowerLineAndCircle (line 66) | writeAppliancePowerLineAndCircle(t,e){if(null==this.solarCardElements....
method writeCircleAndLine (line 84) | writeCircleAndLine(t,e){const i=this.solarCardElements.get(t);return n...
method _handleClick (line 87) | _handleClick(t){if(null==t)return;const e=new Event("hass-more-info",{...
class ot (line 87) | class ot{static changeStylesDependingOnWidth(t,e,i,n){if("complete"!==do...
method changeStylesDependingOnWidth (line 87) | static changeStylesDependingOnWidth(t,e,i,n){if("complete"!==document....
class rt (line 87) | class rt extends Y{constructor(){super(...arguments),this.solarCardEleme...
method constructor (line 87) | constructor(){super(...arguments),this.solarCardElements=new Map,this....
method __increment (line 87) | __increment(){this.counter+=1}
method setConfig (line 87) | setConfig(t){if(t.test_gui,this.config={...t},null==this.config.grid_i...
method createSolarCardElements (line 87) | createSolarCardElements(){Object.keys(this.config).forEach((t=>{if(nul...
method getCardSize (line 87) | getCardSize(){return 5}
method getStubConfig (line 87) | static getStubConfig(){return{}}
method firstUpdated (line 87) | async firstUpdated(){await new Promise((t=>setTimeout(t,0)));const t=t...
method connectedCallback (line 87) | connectedCallback(){super.connectedCallback(),this.redraw=this.redraw....
method shouldUpdate (line 87) | shouldUpdate(t){let e;e=this,this.config.energy_flow_diagramm||request...
method sensorChangeDetected (line 87) | sensorChangeDetected(t){let e=!1;return this.solarCardElements.forEach...
method performUpdate (line 87) | async performUpdate(){this.error="",this.solarCardElements.forEach((t=...
method render (line 87) | render(){if(""!==this.error)return this._showError();let t,e=this.getB...
method writeGenerationIconBubble (line 127) | writeGenerationIconBubble(){const t=this.calculateIconBubbleData(["gen...
method writeGridIconBubble (line 127) | writeGridIconBubble(){const t=this.calculateIconBubbleData(["-generati...
method writeHouseIconBubble (line 127) | writeHouseIconBubble(){let t;t=this.config.house_without_appliances_va...
method writeBatteryIconBubble (line 127) | writeBatteryIconBubble(){const t=this.calculateIconBubbleData(["genera...
method writeApplianceIconBubble (line 127) | writeApplianceIconBubble(t){const e=["appliance"+t+"_consumption_entit...
method calculateIconBubbleData (line 127) | calculateIconBubbleData(t,e=null,i=null){let n=!1;const s=new nt;if(s....
method showKW (line 127) | showKW(t){return!this.config.show_w_not_kw&&!(void 0!==this.config.thr...
method roundValue (line 127) | roundValue(t){let e;return e=t>.1?(0|Math.round(10*(t+Number.EPSILON))...
method animateCircles (line 127) | animateCircles(t){requestAnimationFrame((e=>{t.updateAllCircles(e)}))}
method updateAllCircles (line 127) | updateAllCircles(t){this.solarCardElements.forEach(((e,i)=>{const n=th...
method updateOneCircle (line 127) | updateOneCircle(t,e){if(null==this.shadowRoot)return;const i=this.shad...
method colourHouseBubbleDependingOnHighestInput (line 127) | colourHouseBubbleDependingOnHighestInput(){if(null==this.shadowRoot)re...
method colourBubble (line 127) | colourBubble(t,e,i){const n=e.querySelector(t);null!==n&&(n.style.colo...
method colourLineAndCircle (line 127) | colourLineAndCircle(t,e,i){const n=e.querySelector(t+"_line"),s=e.quer...
method setEnergyFlowDiagramm (line 127) | setEnergyFlowDiagramm(){if(null==this.shadowRoot)return;const t=this.s...
method redraw (line 127) | redraw(t){if(this.hass&&this.config&&"resize"===t.type){const t=this.g...
method _showWarning (line 127) | _showWarning(t){return O` <hui-warning>${t}</hui-warning> `}
method _showError (line 127) | _showError(){return console.log(this.error),O`
method styles (line 134) | static get styles(){return o`
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (216K chars).
[
{
"path": ".editorconfig",
"chars": 526,
"preview": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# edit"
},
{
"path": ".github/workflows/validate.yaml",
"chars": 345,
"preview": "name: Validate\n\non:\n push:\n pull_request:\n schedule:\n - cron: \"0 0 * * *\"\n\njobs:\n validate:\n runs-on: \"ubuntu-"
},
{
"path": ".gitignore",
"chars": 212,
"preview": "## editors\n/.idea\n/.vscode\n\n## system files\n.DS_Store\n\n## npm\n/node_modules/\n/npm-debug.log\n\n## testing\n/coverage/\n\n## t"
},
{
"path": "LICENSE-2.0.txt",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 18067,
"preview": "# tesla-style-solar-power-card\n\n> **⚠ WARNING: BREAKING CONFIG CHANGE**\n\n> **You have to define the FLOWS AGAIN!!** \n> "
},
{
"path": "hacs.json",
"chars": 154,
"preview": "{\n \"name\": \"Tesla style solar power card\",\n \"content_in_root\": true,\n \"filename\": \"tesla-style-solar-power-card"
},
{
"path": "package.json",
"chars": 2746,
"preview": "{\n \"name\": \"tesla-style-solar-power-card\",\n \"version\": \"0.0.0\",\n \"description\": \"Webcomponent tesla-style-solar-power"
},
{
"path": "rollup.config.js",
"chars": 555,
"preview": "\nimport typescript from 'rollup-plugin-typescript2'\nimport resolve from '@rollup/plugin-node-resolve'\nimport {terser} fr"
},
{
"path": "src/TeslaStyleSolarPowerCard.ts",
"chars": 28167,
"preview": "/* eslint-disable no-restricted-globals, prefer-template, no-param-reassign, class-methods-use-this, lit-a11y/click-even"
},
{
"path": "src/components/editor.ts",
"chars": 15137,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any, no-param-reassign, camelcase, lit/no-useless-template-literals, li"
},
{
"path": "src/localize/languages/de.json",
"chars": 183,
"preview": "\n{\n \"common\": {\n \"version\": \"Version\",\n \"invalid_configuration\": \"Invalid configuration\",\n \"show_warni"
},
{
"path": "src/localize/languages/en.json",
"chars": 0,
"preview": ""
},
{
"path": "src/localize/localize.ts",
"chars": 820,
"preview": "import * as en from './languages/en.json';\nimport * as nb from './languages/de.json';\n\n// eslint-disable-next-line @type"
},
{
"path": "src/models/BubbleData.ts",
"chars": 530,
"preview": "import { HassEntity } from 'home-assistant-js-websocket';\n\nexport class BubbleData {\n\n public mainValue: number = 0;\n"
},
{
"path": "src/models/SensorElement.ts",
"chars": 2041,
"preview": "/* eslint-disable class-methods-use-this, no-bitwise */\nexport class SensorElement {\n public speed = 0;\n\n public star"
},
{
"path": "src/models/TeslaStyleSolarPowerCardConfig.ts",
"chars": 1610,
"preview": "/* eslint-disable camelcase */\nimport { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';\n\nexport interface"
},
{
"path": "src/services/HtmlResizeForPowerCard.ts",
"chars": 8212,
"preview": "/* eslint-disable func-names, prefer-template, import/extensions, no-param-reassign, class-methods-use-this, lit-a11y/cl"
},
{
"path": "src/services/htmlWriterForPowerCard.ts",
"chars": 5329,
"preview": "/* eslint-disable no-param-reassign, import/extensions, prefer-template, class-methods-use-this, lit-a11y/click-events-"
},
{
"path": "src/translations/languages/de.json",
"chars": 183,
"preview": "\n{\n \"common\": {\n \"version\": \"Version\",\n \"invalid_configuration\": \"Invalid configuration\",\n \"show_warni"
},
{
"path": "src/translations/languages/en.json",
"chars": 0,
"preview": ""
},
{
"path": "src/translations/localize.ts",
"chars": 820,
"preview": "import * as en from './languages/en.json';\nimport * as nb from './languages/de.json';\n\n// eslint-disable-next-line @type"
},
{
"path": "src/types.ts",
"chars": 231,
"preview": "import { LovelaceCard, LovelaceCardEditor } from 'custom-card-helpers';\n\ndeclare global {\n interface HTMLElementTagName"
},
{
"path": "tesla-style-solar-power-card.js",
"chars": 41217,
"preview": "!function(){\"use strict\";\n/*! *****************************************************************************\n Copyrigh"
},
{
"path": "tesla-style-solar-power-card.ts",
"chars": 167,
"preview": "import { TeslaStyleSolarPowerCard } from './src/TeslaStyleSolarPowerCard.js';\n\nwindow.customElements.define('tesla-style"
},
{
"path": "test/SensorElement.test.ts",
"chars": 3009,
"preview": "import { expect } from '@open-wc/testing';\n\nimport { SensorElement } from '../src/models/SensorElement.js';\n// import { "
},
{
"path": "test/battery.test.ts",
"chars": 10256,
"preview": "import { expect, elementUpdated, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpe"
},
{
"path": "test/batteryCharging.test.ts",
"chars": 9588,
"preview": "import { expect, elementUpdated, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpe"
},
{
"path": "test/batteryWithoutExtra.test.ts",
"chars": 5380,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/colouringOfBubblesDependingOnProduction.test.ts",
"chars": 4526,
"preview": "import { expect, elementUpdated, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpe"
},
{
"path": "test/defaultConfig.test.ts",
"chars": 5241,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/extraAppliances.test.ts",
"chars": 6871,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/extraAppliancesNotInHouse.test.ts",
"chars": 3444,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/gridFeed.test.ts",
"chars": 4947,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/setters.ts",
"chars": 1457,
"preview": "import {elementUpdated, fixture, html} from \"@open-wc/testing\";\nimport { TeslaStyleSolarPowerCard } from \"../src/TeslaSt"
},
{
"path": "test/solarProduction.test.ts",
"chars": 5997,
"preview": "import { expect, assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card-helpers';\nimport { se"
},
{
"path": "test/tesla-style-solar-power-card.test.ts",
"chars": 1305,
"preview": "import { html, fixture, expect } from '@open-wc/testing';\n\nimport { TeslaStyleSolarPowerCard } from '../src/TeslaStyleSo"
},
{
"path": "test/threshold.test.ts",
"chars": 4076,
"preview": "import { expect, /* elementUpdated, */ assert } from '@open-wc/testing';\nimport { LovelaceCardConfig } from 'custom-card"
},
{
"path": "tsconfig.json",
"chars": 543,
"preview": " {\n \"compilerOptions\": {\n \"target\": \"es2018\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"node\",\n \"noEmitOn"
},
{
"path": "web-dev-server.config.mjs",
"chars": 889,
"preview": "// import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';\n\n/** Use Hot Module replacement by adding --hmr to the "
},
{
"path": "web-test-runner.config.mjs",
"chars": 925,
"preview": "// import { playwrightLauncher } from '@web/test-runner-playwright';\n// import { legacyPlugin } from '@web/dev-server-le"
}
]
About this extraction
This page contains the full source code of the reptilex/tesla-style-solar-power-card GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (202.2 KB), approximately 53.7k tokens, and a symbol index with 234 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.