Repository: JonahKr/power-distribution-card
Branch: master
Commit: a0c826b085c1
Files: 53
Total size: 151.4 KB
Directory structure:
gitextract_bav71kz2/
├── .github/
│ └── workflows/
│ ├── build.yaml
│ └── release.yaml
├── .gitignore
├── .prettierrc.js
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── eslint.config.mjs
├── hacs.json
├── package.json
├── rollup.config.mjs
├── src/
│ ├── action-handler.ts
│ ├── card-tags.ts
│ ├── deep-equal.ts
│ ├── editor/
│ │ ├── bar-editor.ts
│ │ ├── editor.ts
│ │ ├── ha-form.ts
│ │ ├── item-editor.ts
│ │ └── items-editor.ts
│ ├── localize/
│ │ ├── languages/
│ │ │ ├── de.json
│ │ │ ├── en.json
│ │ │ └── sk.json
│ │ └── localize.ts
│ ├── power-distribution-card.ts
│ ├── presets.ts
│ ├── styles.ts
│ ├── types.ts
│ └── utils/
│ ├── compute-color.ts
│ ├── create-thing.ts
│ ├── custom-cards.ts
│ ├── debounce.ts
│ ├── get-lovelace.ts
│ ├── ha-component-loader.ts
│ ├── hass-types/
│ │ ├── action.ts
│ │ ├── action_handler.ts
│ │ ├── event.ts
│ │ ├── fire_event.ts
│ │ ├── format-number.ts
│ │ ├── get_main_window.ts
│ │ ├── handle-action.ts
│ │ ├── haptics.ts
│ │ ├── has-action.ts
│ │ ├── homeassistant.ts
│ │ ├── integration.ts
│ │ ├── localize.ts
│ │ ├── lovelace.ts
│ │ ├── navigate.ts
│ │ ├── show-dialog-box.ts
│ │ ├── show-ha-voice-command-dialog.ts
│ │ ├── toast.ts
│ │ └── toggle-entity.ts
│ └── index.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yaml
================================================
name: "Build"
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
name: Test build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v4
with:
version: latest
- name: Build
run: |
pnpm install
pnpm run build
================================================
FILE: .github/workflows/release.yaml
================================================
name: Release
on:
release:
types: [published]
jobs:
release:
name: Prepare release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v4
with:
version: latest
#Building the Js-File
- name: Build the file
run: |
pnpm install
pnpm run build
# Upload build file to the releas as an asset.
- name: Upload zip to release
uses: svenstaro/upload-release-action@v1-release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: /home/runner/work/power-distribution-card/power-distribution-card/dist/power-distribution-card.js
asset_name: power-distribution-card.js
tag: ${{ github.ref }}
overwrite: true
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
================================================
FILE: .prettierrc.js
================================================
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
endOfLine: 'auto',
};
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 JonahKr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# power-distribution-card
[](https://github.com/JonahKr/power-distribution-card/blob/master/package.json)
[](https://github.com/JonahKr/power-distribution-card/actions)
[](https://img.shields.io/github/license/JonahKr/power-distribution-card/blob/master/LICENSE)
[](https://github.com/custom-components/hacs)
<a href="https://www.buymeacoffee.com/JonahKr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" height="20px" width="83px" ></a>
<br/>
<div>
<i>Inspired by</i>
<img height="40px" alt="e3dc-logo" href="https://www.e3dc.com/en/#Intro" src="https://user-images.githubusercontent.com/38377070/95522835-7de46d80-09cd-11eb-9aae-55657aa3caae.png"/>
</div>
<br/>
<h1 align="center">The Lovelace Card for visualizing power distributions.</h1>
<p align="center">
<img src="https://user-images.githubusercontent.com/38377070/103143008-389f2480-470e-11eb-945a-68115febef8a.gif"/>
</p>
<br/>
</div>
<div id="toc">
<h2> Table of Contents </h2>
<ul>
<li>
<h3><a href="#installation">Installation</a></h3>
</li>
<li>
<h3><a href="#configuration">Configuration</a></h3>
<h4><a href="#presets">Presets</a></h4>
<h4><a href="#simple">Simple Configuration</a></h4>
<h4><a href="#yaml">YAML Configuration</a></h4>
<h4><a href="#animation">Animation Options</a></h4>
<h4><a href="#center">Center Panel</a></h4>
<h4><a href="#entity">Advanced Configuration</a></h4>
</li>
<li>
<h3><a href="#faq">FAQs</a></h2>
</li>
</ul>
<br/>
</div>
<hr>
<br/>
<div id="installation">
<h1> Installation</h1>
<h2> Installation via <a href="https://hacs.xyz/">HACS</a> <img src="https://img.shields.io/badge/-Recommended-%2303a9f4"/> </h2>
1. Make sure the [HACS](https://github.com/custom-components/hacs) custom component is installed and working.
2. Search for `power-distribution-card` and add it through HACS
3. Refresh home-assistant.
<h2> Manual installation</h2>
1. Download the latest release of the [power-distribution-card](http://www.github.com/JonahKr/power-distribution-card/releases/latest/download/power-distribution-card.js)
2. Place the file in your `config/www` folder
3. Include the card code in your `ui-lovelace-card.yaml`
```yaml
resources:
- url: /local/power-distribution-card.js
type: module
```
Or alternatively set it up via the UI:
`Configuration -> Lovelace Dashboards -> Resources (TAB)`
For more guidance check out the [docs](https://developers.home-assistant.io/docs/frontend/custom-ui/registering-resources/).
<br/>
</div>
***
<br/>
<div id="configuration">
<h1> Configuration</h1>
<div id="presets">
<h3>Presets</h3>
Every Sensor you want to add has to use one of the Presets. You can add as many of these as you want.
<table style="text-align: center;">
<tr>
<td>
<img height="60px" alt="mdi-battery-outline" src="https://user-images.githubusercontent.com/38377070/95509029-356c8600-09b4-11eb-834d-1c05cdb9758e.png"/>
<td>
<img height="60px" alt="mdi-electirc-car" src="https://user-images.githubusercontent.com/38377070/95509040-369db300-09b4-11eb-8caa-046c5b2999d2.png"/>
</td>
<td>
<img height="60px" alt="mdi-lightbulb" src="https://user-images.githubusercontent.com/38377070/95515835-87b2a480-09be-11eb-92af-45a97c895cda.png"/>
</td>
<td>
<img height="60px" alt="mdi-transmission-tower" src="https://user-images.githubusercontent.com/38377070/95508865-ee7e9080-09b3-11eb-8981-eab0969cecac.png"/>
</td>
<td>
<img height="60px" alt="mdi-home-assistant" src="https://user-images.githubusercontent.com/38377070/95509151-66e55180-09b4-11eb-9228-585dcde1d40e.png"/>
</td>
<td>
<img height="60px" alt="mdi-hydro-power" src="https://user-images.githubusercontent.com/38377070/95515201-85037f80-09bd-11eb-8603-31eb4c70b83b.png"/>
</td>
<td>
<img height="60px" alt="mdi-pool" src="https://user-images.githubusercontent.com/38377070/95515296-abc1b600-09bd-11eb-8b81-1e6fbbddb7c3.png"/>
</td>
<td>
<img height="60px" alt="mdi-lightning-bolt-outline" src="https://user-images.githubusercontent.com/38377070/95509102-50d79100-09b4-11eb-96ea-8a544db60ccb.png"/>
</td>
<td>
<img height="60px" alt="mdi-solar-power" src="https://user-images.githubusercontent.com/38377070/95516097-03145600-09bf-11eb-9027-37593379e1c2.png"/>
</td>
<td>
<img height="60px" alt="mdi-wind-turbine" src="https://user-images.githubusercontent.com/38377070/95516203-2b9c5000-09bf-11eb-9fd0-a3a87447f30e.png"/>
</td>
<td>
<img height="60px" alt="mdi-radiator" src="https://user-images.githubusercontent.com/38377070/151670027-67edf52c-1f7f-47a6-8f2a-1ee1a68b5906.png"/>
</td>
</tr>
<tr>
<td>battery</td>
<td>car_charger</td>
<td>consumer</td>
<td>grid</td>
<td>home</td>
<td>hydro</td>
<td>pool</td>
<td>producer</td>
<td>solar</td>
<td>wind</td>
<td>heating</td>
</tr>
<tr>
<td>
Any Home Battery e.g. <a href="https://www.e3dc.com/en/#Intro">E3dc</a>, <a href="https://www.tesla.com/en_eu/powerwall">Powerwall</a>
</td>
<td>
Any Electric Car Charger
</td>
<td>
A custom home power consumer
</td>
<td>
The interface to the power grid
</td>
<td>
Your Home's power consumption
</td>
<td>
Hydropower setup like <a href="https://www.turbulent.be/">Turbulent</a>
</td>
<td>
pool heater or pump
</td>
<td>
custom home power producer
</td>
<td>
Power coming from Solar
</td>
<td>
Power coming from Wind
</td>
<td>
Radiators
</td>
</tr>
</table>
The presets *consumer* and *producer* enable to add any custom device into your Card with just a bit of tweaking.
</div>
<br/>
<div id="simple">
## Simple Configuration 🛠️ <img src="https://img.shields.io/badge/-Recommended-%2303a9f4"/>
With Version 2.0 a Visual Editor got introduced.
You can find the Card in your Card Selector probably at the bottom.
From there on you can configure your way to your custom Card.
The easiest way to get your Card up and running, is by defining the entities for the presets directly.
<br/>
<p align="center">
<img src="https://github.com/user-attachments/assets/d83e941c-0436-4efc-8060-ba3cbcd457d1" />
</p>
<br/>
```diff
! Please Check for every Sensor: positive sensor values = production, negative values = consumption
! If this is the other way around in your Case, check the `invert_value` setting (Advanced Configuration)!
```
<p align="center">
<img src="https://github.com/user-attachments/assets/e3b25a1f-3438-4b04-8174-9093473ec05b"/>
</p>
### Placeholder
By submitting an empty entity_id and preset, you will generate a plain transparent placeholder item which can be used to further customize your layout.
Alternatively you can use the provided `placeholder` preset.
<p align="center">
<img src="https://user-images.githubusercontent.com/38377070/124113882-3676a380-da6c-11eb-8f3e-db00466fd601.png"/>
</p>
</div>
<br/><br/>
<div id="yaml">
## YAML Only
If you are a real hardcore YAML connoisseur here is a basic example to get things started:
```yaml
type: 'custom:power-distribution-card'
title: Title
animation: flash
entities:
- entity: sensor.e3dc_home
preset: home
- entity: sensor.e3dc_solar
preset: solar
- entity: sensor.e3dc_battery
preset: battery
center:
type: bars
bars:
- preset: autarky
name: autarky
- preset: ratio
name: ratio
```
You can find all options for every entity <a href="#entity">here</a>.
If you want to further modify the center panel youz can find the documentation <a href="#center">here</a>.
</div>
<br/><br/>
<div id="animation">
## Animation
For the animation you have 3 options: `flash`, `slide`, `none`
```yaml
type: 'custom:power-distribution-card'
animation: 'slide'
```
</div>
<br/>
<div id="center">
## Center Panel
For customizing the Center Panel you basically have 3 Options:
### None 🕳️
the *void*
<br/>
### Bars 📊
Bars have the following Settings:
| Setting | type | example | description |
| --------------------- |:-------------:|:-----------------:| :------------|
| `bar_color` | string | red, #C1C1C1 | You can pass any string that CSS will accept as a color. |
| `bar_bg_color` | string | red, #C1C1C1 | The Background Color of the Bar. You can pass any string that CSS will accept as a color. |
| `entity` | string | sensor.ln_autarky | You can specify the entity_id here as well. Required when `preset` is not `autarky` or `ratio`. |
| `invert_value` | bool | false | This will invert the value received from HASS. |
| `lower_bound` | number | 0 | Lower bound for bar fill scaling (default: 0). Values at or below this show an empty bar. |
| `name` | string | Eigenstrom | Feel free to change the displayed name of the element. |
| `preset` | `'autarky'` \| `'ratio'` \| `''` | `autarky` | `autarky`/`ratio` auto-calculate from entity totals. Use `''` (empty string) with an `entity` for a custom bar. |
| `tap_action` | Action Config | [Configuration](https://www.home-assistant.io/lovelace/actions/#configuration-variables) | Single tap action for item. |
| `double_tap_action` | Action Config | [Configuration](https://www.home-assistant.io/lovelace/actions/#configuration-variables) | Double tap action for item. |
| `unit_of_measurement` | string | *W* , *kW* | Default: %; The Unit of the sensor value. **Should be detected automatically!** |
| `upper_bound` | number | 100 | Upper bound for bar fill scaling (default: 100). Values at or above this show a full bar. |
<br/>
<p align="center">
<img src="https://github.com/user-attachments/assets/b56a9980-5248-4c7f-a029-161920bc68d2"/>
</p>
<br/>
### Cards 🃏
<p align="center">
<img width="600px" src="https://user-images.githubusercontent.com/38377070/97620471-e8fbef80-1a21-11eb-90d3-1bcbab57da2c.PNG"/>
</p>
Cards couldn't yet be included in the Visual editor in a nice way. I am working on it though. Feel free to open a Issue with suggestions.
To add a card you can simply replace the `center` part in the Code Editor. Be aware though: While you can switch between `none` and `card` without any issues, switching to Bars will override your settings.
For example you could insert a glance card:
```yaml
center:
type: card
card:
type: glance
entities:
- sensor.any_Sensor
```
</div>
<br/><br/>
<div id="entity">
## Entity Configuration ⚙️
There are alot of settings you can customize your sensors with:
| Setting | type | example | description |
| -------------------------- |:-------------:|:----------------------------:| :------------|
| `attribute` | string | deferredWatts | A Sensor can have multiple attributes. If one of them is your desired value to display, add it here. |
| `arrow_color` | object | {smaller:'red'} | You can Change the Color of the arrow dependant on the value. (Bigger, Equal and Smaller) |
| `calc_excluded` | boolean | true | If the Item should be excluded from ratio/autarky calculations. |
| `color_threshold` | number | 0, -100, 420.69 | The value at which the coloring logic switches on. (default: 0) |
| `consumer` | boolean | true | Marks this entity as a power consumer. Negative values contribute to the consumption total used by autarky/ratio bars. |
| `decimals` | number | 0, 2 | The Number of Decimals shown. (default: 2) |
| `display_abs` | boolean | true | Display values as absolute (non-negative) numbers. Defaults to `true` when a preset is used. |
| `double_tap_action` | Action Config | [Configuration](https://www.home-assistant.io/lovelace/actions/#configuration-variables) | Double tap action for item. |
| `entity` | string | sensor.e3dc_grid | You can specify the entity_id here as well. |
| `hide_arrows` | bool | true | Toggles the visibility of the *arrows*. |
| `icon` | string | mdi:dishwasher | Why not change the displayed Icon to any [MDI](https://pictogrammers.com/library/mdi/) one? |
| `icon_color` | object | {smaller:'red'} | You can Change the Color of the icon dependant on the value. (Bigger, Equal and Smaller) |
| `invert_arrow` | bool | true | This will change the *arrows* direction to the opposite one. |
| `invert_value` | bool | false | This will invert the value received from HASS. This affects calculations as well! |
| `name` | string | dishwasher | Feel free to change the displayed name of the element. |
| `producer` | boolean | true | Marks this entity as a power producer. Positive values contribute to the production total used by autarky/ratio bars. |
| `secondary_info_attribute` | string | min_temp | Requires `secondary_info_entity`. Displays the attribute value instead of the sensor state. |
| `secondary_info_decimals` | number | 1 | Number of decimals for the secondary info value. |
| `secondary_info_entity` | string | sensor.e3dc_grid | entity_id of the secondary info sensor. |
| `secondary_info_replace_name` | bool | true | This will replace the name of the item with the secondary info. |
| `tap_action` | Action Config | [Configuration](https://www.home-assistant.io/lovelace/actions/#configuration-variables) | Single tap action for item. |
| `threshold` | number | 2 | Ignoring all absolute values smaller than threshold. |
| `unit_of_display` | string | *W* , *kW* , *adaptive* | The Unit the value is displayed in (default: W). Adaptive will show kW for values >= 1kW. |
| `unit_of_measurement` | string | *W* , *kW* | The Unit of the sensor value. **Should be detected automatically!** |
<p>
This could look something like:
```yaml
entities:
- decimals: 2
display_abs: true
name: battery
unit_of_display: W
consumer: true
icon: 'mdi:battery-outline'
producer: true
entity: sensor.e3dc_battery
preset: battery
icon_color:
bigger: 'green'
equal: ''
smaller: 'red'
```
</div>
<br/>
<br/>
<div>
## Preset features
The Presets `battery` and `grid` have some additional features which allow some further customization.
For the Battery the icon can display the state of charge and the grid preset can have a small display with power sold and bought from the grid.
<img width="600px" src="https://user-images.githubusercontent.com/38377070/137152436-34753a15-86f9-44c4-ad47-87c35d94bd91.png"/>
If one of those presets is selected there will be additional options in the visual editor.
If you prefer yaml, here are all extra options which can be set per item:
| Setting | type | example | description |
| --------------------------- |:-------------:|:----------------------------:| :------------|
| `battery_percentage_entity` | string | sensor.xyz | Sensor containing the battery charge percentage from 0 to 100 |
| `grid_buy_entity` | string | sensor.xyz | Sensor containing the imported power from the grid |
| `grid_sell_entity` | string | sensor.xyz | Sensor containing the sold power towards the grid |
</div>
</div>
<hr>
<div id="faq">
<h1> FAQs ❓</h1>
### What the heck are these autarky and ratio calculating?
So basically these bar-graphs are nice indicators to show you:
1. the autarky of your home (Home Production like Solar / Home Consumption)
2. the ratio / share of produced electricity used by the home (The Germans call it `Eigenverbrauchsanteil` 😉)
### kW and kWh is not the Same!
I know... In this case usability is more important and the user has to decide if he is ok with that.
<br/>
</div>
<hr>
**If you find a Bug or have some suggestions, let me know <a href="https://github.com/JonahKr/power-distribution-card/issues">here</a>!**
**If you like the card, consider starring it.**
================================================
FILE: eslint.config.mjs
================================================
import { defineConfig } from "eslint/config";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([{
extends: compat.extends(
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended",
),
languageOptions: {
parser: tsParser,
ecmaVersion: 2018,
sourceType: "module",
parserOptions: {
experimentalDecorators: true,
},
},
rules: {
"@typescript-eslint/camelcase": 0,
},
}]);
================================================
FILE: hacs.json
================================================
{
"name": "Power Distribution Card",
"filename": "power-distribution-card.js",
"render_readme": true,
"homeassistant": "2026.3"
}
================================================
FILE: package.json
================================================
{
"name": "power-distribution-card",
"version": "3.0.0",
"license": "MIT",
"author": "JonahKr",
"description": "A Lovelace Card for visualizing power distributions.",
"keywords": [
"power",
"distribution",
"lovelace",
"hacs",
"home assistant",
"e3dc"
],
"module": "power-distribution-card.js",
"repository": {
"type": "git",
"url": "git+https://github.com/JonahKr/power-distribution-card.git"
},
"bugs": {
"url": "https://github.com/JonahKr/power-distribution-card/issues"
},
"homepage": "https://github.com/JonahKr/power-distribution-card#readme",
"scripts": {
"start": "BUILD_DEV=true rollup -c --watch",
"serve": "http-server dist/ --cors -p 5000",
"build": "pnpm run rollup",
"lint": "eslint src/*.ts",
"rollup": "rollup -c"
},
"dependencies": {
"@formatjs/intl-numberformat": "^9.3.1",
"@mdi/js": "^7.4.47",
"lit": "3.3.2"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.29.0",
"@rollup/plugin-babel": "7.0.0",
"@rollup/plugin-commonjs": "29.0.2",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"eslint": "10.1.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.5",
"http-server": "^14.1.1",
"prettier": "3.8.1",
"rollup": "4.60.0",
"typescript": "6.0.2"
}
}
================================================
FILE: rollup.config.mjs
================================================
import typescript from '@rollup/plugin-typescript';
import nodeResolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
const dev = process.env.BUILD_DEV === 'true';
const devSuffix = dev ? '-dev' : '';
const plugins = [
replace({
__DEV_SUFFIX__: JSON.stringify(devSuffix),
preventAssignment: true,
}),
nodeResolve({}),
typescript(),
json(),
babel({
exclude: 'node_modules/**',
babelHelpers: 'bundled',
}),
terser(),
];
export default [
{
input: 'src/power-distribution-card.ts',
output: {
file: `dist/power-distribution-card${devSuffix}.js`,
format: 'es',
},
plugins: [...plugins],
},
];
================================================
FILE: src/action-handler.ts
================================================
import { noChange } from 'lit';
import { AttributePart, directive, Directive, DirectiveParameters } from 'lit/directive.js';
import { deepEqual } from './deep-equal';
import { ACTION_HANDLER_TAG } from './card-tags';
import { fireEvent } from './utils';
export const actions = ['more-info', 'toggle', 'navigate', 'url', 'call-service', 'none'] as const;
interface ActionHandlerMock extends HTMLElement {
holdTime: number;
bind(element: Element, options?: ActionHandlerOptions): void;
}
interface ActionHandlerElement extends HTMLElement {
actionHandler?: {
options: ActionHandlerOptions;
start?: (ev: Event) => void;
end?: (ev: Event) => void;
handleEnter?: (ev: KeyboardEvent) => void;
};
}
export interface ActionHandlerOptions {
hasHold?: boolean;
hasDoubleClick?: boolean;
disabled?: boolean;
}
class ActionHandler extends HTMLElement implements ActionHandlerMock {
public holdTime = 500;
protected timer?: number;
private dblClickTimeout?: number;
public bind(element: ActionHandlerElement, options: ActionHandlerOptions = {}) {
if (element.actionHandler && deepEqual(options, element.actionHandler.options)) {
return;
}
if (element.actionHandler) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
element.removeEventListener('click', element.actionHandler.end!);
}
element.actionHandler = { options };
if (options.disabled) {
return;
}
element.actionHandler.end = (ev: Event): void => {
const target = element; //ev.target as HTMLElement;
// Prevent mouse event if touch event
if (ev.cancelable) {
ev.preventDefault();
}
clearTimeout(this.timer);
this.timer = undefined;
if (options.hasDoubleClick) {
if ((ev.type === 'click' && (ev as MouseEvent).detail < 2) || !this.dblClickTimeout) {
this.dblClickTimeout = window.setTimeout(() => {
this.dblClickTimeout = undefined;
fireEvent(target, 'action', { action: 'tap' });
}, 250);
} else {
clearTimeout(this.dblClickTimeout);
this.dblClickTimeout = undefined;
fireEvent(target, 'action', { action: 'double_tap' });
}
} else {
fireEvent(target, 'action', { action: 'tap' });
}
};
element.addEventListener('click', element.actionHandler.end);
}
}
customElements.define(ACTION_HANDLER_TAG, ActionHandler);
const getActionHandler = (): ActionHandler => {
const body = document.body;
if (body.querySelector(ACTION_HANDLER_TAG)) {
return body.querySelector(ACTION_HANDLER_TAG) as ActionHandler;
}
const actionhandler = document.createElement(ACTION_HANDLER_TAG);
body.appendChild(actionhandler);
return actionhandler as ActionHandler;
};
export const actionHandlerBind = (element: ActionHandlerElement, options?: ActionHandlerOptions): void => {
const actionhandler: ActionHandler = getActionHandler();
if (!actionhandler) {
return;
}
actionhandler.bind(element, options);
};
export const actionHandler = directive(
class extends Directive {
update(part: AttributePart, [options]: DirectiveParameters<this>) {
actionHandlerBind(part.element as ActionHandlerElement, options);
return noChange;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
render(_options?: ActionHandlerOptions) {}
},
);
================================================
FILE: src/card-tags.ts
================================================
// Build-time constant injected by rollup: '-dev' in watch mode, '' in production.
// Never edit this value manually — change the rollup config or npm scripts instead.
declare const __DEV_SUFFIX__: string;
export const CARD_TAG = `power-distribution-card${__DEV_SUFFIX__}`;
export const EDITOR_TAG = `${CARD_TAG}-editor`;
export const ITEM_EDITOR_TAG = `${CARD_TAG}-item-editor`;
export const BAR_EDITOR_TAG = `${CARD_TAG}-bar-editor`;
export const ITEMS_EDITOR_TAG = `${CARD_TAG}-items-editor`;
export const ACTION_HANDLER_TAG = `action-handler${__DEV_SUFFIX__}-power-distribution-card`;
================================================
FILE: src/deep-equal.ts
================================================
// From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const deepEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let i: number | [any, any];
let length: number;
if (Array.isArray(a)) {
length = a.length;
if (length !== b.length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) {
return false;
}
for (i of a.entries()) {
if (!b.has(i[0])) {
return false;
}
}
for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]))) {
return false;
}
}
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) {
return false;
}
for (i of a.entries()) {
if (!b.has(i[0])) {
return false;
}
}
return true;
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
length = a.length;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (length !== b.length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
const keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) {
return false;
}
for (i = length; i-- !== 0; ) {
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
return false;
}
}
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// true if both NaN, false otherwise
// eslint-disable-next-line no-self-compare
return a !== a && b !== b;
};
================================================
FILE: src/editor/bar-editor.ts
================================================
import { LitElement, html, css, nothing, CSSResultGroup } from 'lit';
import { property, state } from 'lit/decorators.js';
import { BAR_EDITOR_TAG } from '../card-tags';
import { BarSettings } from '../types';
import { localize } from '../localize/localize';
import { HaFormSchema } from './ha-form';
import { mdiDelete, mdiPlus } from '@mdi/js';
import { deepEqual } from '../deep-equal';
import { fireCustomEvent, HomeAssistant } from '../utils';
const BAR_PRESETS = ['autarky', 'ratio', ''];
const SCHEMA: HaFormSchema[] = [
{ name: "entity", selector: { entity: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "preset", selector: { select: { options: BAR_PRESETS, mode: 'dropdown' } } },
]
},
{
type: "grid",
name: "",
schema: [
{ name: "lower_bound", selector: { number: {} } },
{ name: "upper_bound", selector: { number: {} } },
]
},
{
type: "grid",
name: "",
schema: [
{ name: "bar_color", selector: { ui_color: {} } },
{ name: "bar_bg_color", selector: { ui_color: {} } },
]
},
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "double_tap_action",
selector: { ui_action: {} },
}
];
export class ItemEditor extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@property({ attribute: false }) config?: BarSettings[];
@state() protected _selectedCard = 0;
private _computeLabel = (schema: HaFormSchema) => {
const nameMap: Record<string, string> = {
bar_color: 'color',
bar_bg_color: 'background_color',
};
const name = nameMap[schema.name] ?? schema.name;
return localize('editor.settings.' + name);
};
protected render() {
if (!this.hass) {
return nothing;
}
const config = this.config ?? [];
const selected = this._selectedCard;
const numBars = config.length;
return html`
<div class="card-config">
<div class="toolbar">
<ha-tab-group @wa-tab-show=${this._selectBar}>
${config.map(
(_card, i) => html`
<ha-tab-group-tab
slot="nav"
.panel=${i}
.active=${i === selected}
>${i + 1}</ha-tab-group-tab>`
)}
</ha-tab-group>
<ha-icon-button
id="add-bar"
.path=${mdiPlus}
@click=${this._addBar}
></ha-icon-button>
</div>
</div>
${numBars > 0 ? html`
<div id="editor">
<div id="bar-options">
<ha-icon-button-arrow-prev
.disabled=${selected === 0}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.move_before"
)}
@click=${this._moveLeft}
.move=${-1}
></ha-icon-button-arrow-prev>
<ha-icon-button-arrow-next
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.move_after"
)}
.disabled=${selected === numBars - 1}
@click=${this._moveRight}
.move=${1}
></ha-icon-button-arrow-next>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}
.path=${mdiDelete}
@click=${this._delete}
></ha-icon-button>
</div>
<ha-form
.hass=${this.hass}
.data=${config[selected]}
.schema=${SCHEMA}
.computeLabel=${this._computeLabel}
@value-changed=${this.valueChanged}
></ha-form>
</div>
` : nothing}
`;
}
protected valueChanged(ev: CustomEvent<{ value: BarSettings }>) {
ev.stopPropagation();
if (!this.config || !this.hass) {
return;
}
// Check if value has changed
if (deepEqual(this.config[this._selectedCard], ev.detail.value)) return;
// Replace value for current index in readonly config
this.config = this.config!.map((item, index) =>
index === this._selectedCard ? ev.detail.value : item);
fireCustomEvent(this, "config-changed", this.config)
}
protected _addBar() {
if (!this.config) {
this.config = [{}];
} else {
this.config = [...this.config, {}];
}
this._selectedCard = this.config.length - 1;
fireCustomEvent(this, "config-changed", this.config);
}
protected _selectBar(ev: CustomEvent<{ name: string }>) {
this._selectedCard = parseInt(ev.detail.name, 10);
}
protected _moveRight() {
if (!this.config || this._selectedCard >= this.config.length - 1) return;
const newConfig = this.config.slice();
const movedElement = newConfig.splice(this._selectedCard, 1)[0];
newConfig.splice(this._selectedCard + 1, 0, movedElement);
this.config = newConfig;
this._selectedCard++;
fireCustomEvent(this, "config-changed", this.config);
}
protected _moveLeft() {
if (!this.config || this._selectedCard === 0) return;
const newConfig = this.config.slice();
const movedElement = newConfig.splice(this._selectedCard, 1)[0];
newConfig.splice(this._selectedCard - 1, 0, movedElement);
this.config = newConfig;
this._selectedCard--;
fireCustomEvent(this, "config-changed", this.config);
}
protected _delete() {
if (!this.config) return;
const newConfig = this.config.slice();
newConfig.splice(this._selectedCard, 1);
this.config = newConfig;
if (this._selectedCard >= newConfig.length && newConfig.length > 0) {
this._selectedCard = newConfig.length - 1;
}
fireCustomEvent(this, "config-changed", this.config);
}
static get styles(): CSSResultGroup {
return [
css`
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-tab-group {
flex-grow: 1;
min-width: 0;
--ha-tab-track-color: var(--card-background-color);
}
#bar-options {
display: flex;
justify-content: flex-end;
width: 100%;
}
#editor {
border: 1px solid var(--divider-color);
padding: 12px;
}
@media (max-width: 450px) {
#editor {
margin: 0 -12px;
}
}
`,
];
}
}
customElements.define(BAR_EDITOR_TAG, ItemEditor);
================================================
FILE: src/editor/editor.ts
================================================
import { LitElement, TemplateResult, html, css, CSSResultGroup, nothing } from 'lit';
import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
import { property, state } from 'lit/decorators.js';
import { EDITOR_TAG, ITEM_EDITOR_TAG, BAR_EDITOR_TAG, ITEMS_EDITOR_TAG } from '../card-tags';
import { mdiPencil } from '@mdi/js';
import { getLovelace } from '../utils';
import {
PDCConfig,
EntitySettings,
CustomValueEvent,
} from '../types';
import { computeLabel, localize } from '../localize/localize';
import './item-editor';
import './items-editor';
import './bar-editor';
import { loadHaComponents } from '../utils/ha-component-loader';
import { HaFormSchema } from './ha-form';
import { fireEvent, HomeAssistant } from '../utils';
/**
* Editor Settings
*/
const animation = ['none', 'flash', 'slide'];
const center = ['none', 'card', 'bars'];
type EditorType = 'main' | 'item' | 'bars' | 'card';
type Editor = {
type: EditorType;
index?: number;
}
const SCHEMA: HaFormSchema[] = [
{ name: 'title', selector: { text: {} } },
{ name: 'animation', selector: { select: { options: animation, mode: 'dropdown' } }, required: true },
];
export class PowerDistributionCardEditor extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config!: PDCConfig;
@state() private _activeEditor: Editor = { type: 'main' };
public setConfig(config: PDCConfig) {
// Migrate old format: center.content -> center.bars or center.card
if (config.center && 'content' in config.center) {
const oldContent = (config.center as any).content;
const { content: _removed, ..._centerWithoutContent } = config.center as any;
let newCenter: PDCConfig['center'];
if (config.center.type === 'bars') {
newCenter = { ..._centerWithoutContent, bars: oldContent };
} else if (config.center.type === 'card') {
newCenter = { ..._centerWithoutContent, card: oldContent };
} else {
newCenter = _centerWithoutContent;
}
this._config = { ...config, center: newCenter };
fireEvent(this, 'config-changed', { config: this._config });
} else {
this._config = config;
}
}
protected firstUpdated() {
loadHaComponents();
}
protected render() {
if (!this.hass || !this._config) return nothing;
if (this._activeEditor.type === 'main') {
return this._renderMainEditor();
}
// All Subpages get an additional header
const content: (TemplateResult | typeof nothing)[] = [
html`
<div class="header">
<div class="back-title">
<ha-icon-button-arrow-prev @click=${this._goBack}>
</ha-icon-button-arrow-prev>
</div>
</div>`,
];
switch (this._activeEditor.type) {
case 'item':
content.push(this._renderItemEditor());
break;
case 'bars':
content.push(this._renderBarEditor());
break;
case 'card':
content.push(this._renderCardEditor());
break;
}
return html`${content}`;
}
protected _enableCenterEditor(ev: any): void {
ev.stopPropagation();
this._activeEditor = { type: ev.currentTarget.value };
}
protected _enableItemEditor(ev: any): void {
ev.stopPropagation();
this._activeEditor = {
type: 'item',
index: ev.detail,
};
}
protected _goBack(): void {
this._activeEditor = { type: 'main' };
}
protected _valueChanged(ev: CustomValueEvent<unknown>) {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const target = ev.target;
const detail = ev.detail;
if (target && detail) {
if (target.configValue) {
let value: any = detail;
// Specific Case
if (target.configValue == 'center.type') {
value = detail.value;
}
// We split the target configValue by '.' to allow for nested config values of depth 1
const configValues = target.configValue.split('.');
this._config = {
...this._config,
[configValues[0]]: configValues.length > 1 ? {
...this._config[configValues[0]],
[configValues[1]]: value,
} : value,
};
} else {
// Assuming a return from ha-form
this._config = detail.value as PDCConfig;
}
fireEvent(this, 'config-changed', { config: this._config });
}
}
protected _renderMainEditor(): TemplateResult {
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
<br />
<div class="entity row">
<ha-select
style="flex-grow: 1"
label="${localize('editor.settings.center')}"
.configValue=${'center.type'}
@selected=${this._valueChanged}
.value=${this._config?.center?.type || 'none'}
.options=${center.map((val) => ({ value: val, label: val }))}
></ha-select>
${this._config?.center?.type != 'none'
? html`<ha-icon-button
class="edit-icon"
.value=${this._config?.center?.type}
.path=${mdiPencil}
@click="${this._enableCenterEditor}"
></ha-icon-button>`
: ''}
</div>
<br />
${staticHtml`<${unsafeStatic(ITEMS_EDITOR_TAG)}
.hass=${this.hass}
.entities=${this._config.entities}
.configValue=${'entities'}
@edit-item=${this._enableItemEditor}
@config-changed=${this._valueChanged}
></${unsafeStatic(ITEMS_EDITOR_TAG)}>`}
</div>
`;
}
protected _renderItemEditor() {
const index = this._activeEditor.index;
if (index == undefined) {
return nothing;
}
return staticHtml`<${unsafeStatic(ITEM_EDITOR_TAG)}
.hass=${this.hass}
.config=${this._config.entities[index]}
@config-changed=${this._itemChanged}
></${unsafeStatic(ITEM_EDITOR_TAG)}>`;
}
/**
* Bar Editor
* -------------------
* This Bar Editor allows the user to easily add and remove new bars.
*/
protected _renderBarEditor() {
return staticHtml`<${unsafeStatic(BAR_EDITOR_TAG)}
.hass=${this.hass}
.config=${this._config.center.bars}
.configValue=${'center.bars'}
@config-changed=${this._valueChanged}
></${unsafeStatic(BAR_EDITOR_TAG)}>`;
}
private _itemChanged(ev: CustomEvent<EntitySettings>) {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const index = this._activeEditor.index;
if (index != undefined) {
const entities = [...this._config.entities];
entities[index] = ev.detail;
fireEvent(this, 'config-changed', { config: { ...this._config, entities: entities } });
}
}
private _renderCardEditor(): TemplateResult {
return html`
<p />
Card configuration is only editable via yaml.
<p />
Check out the
<a target="_blank" rel="noopener noreferrer" href="https://github.com/JonahKr/power-distribution-card#cards-"
>Readme</a
>
to check out the latest and best way to add it.
`;
}
private _cardChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) return;
this._config = {
...this._config,
center: { ...this._config.center, card: ev.detail.config },
};
fireEvent(this, 'config-changed', { config: this._config });
}
/**
* The Second Part comes from here: https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-sortable-style.ts
* @returns Editor CSS
*/
static get styles(): CSSResultGroup[] {
return [
css`
.checkbox {
display: flex;
align-items: center;
padding: 8px 0;
}
.checkbox input {
height: 20px;
width: 20px;
margin-left: 0;
margin-right: 8px;
}
`,
css`
h3 {
margin-bottom: 0.5em;
}
.row {
margin-bottom: 12px;
margin-top: 12px;
display: block;
}
.side-by-side {
display: flex;
}
.side-by-side > * {
flex: 1 1 0%;
padding-right: 4px;
}
.entity,
.add-item {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move;
}
.entity ha-entity-picker,
.add-item ha-entity-picker {
flex-grow: 1;
}
.add-preset {
padding-right: 8px;
max-width: 130px;
}
.remove-icon,
.edit-icon,
.add-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}`,
];
}
}
customElements.define(EDITOR_TAG, PowerDistributionCardEditor);
================================================
FILE: src/editor/ha-form.ts
================================================
import type { LitElement } from "lit";
interface HaDurationData {
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
}
export type HaFormSchema =
| HaFormConstantSchema
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema
| HaFormSelector
| HaFormGridSchema
| HaFormExpandableSchema
| HaFormOptionalActionsSchema;
export interface HaFormBaseSchema {
name: string;
// This value is applied if no data is submitted for this field
default?: HaFormData;
required?: boolean;
disabled?: boolean;
description?: {
suffix?: string;
// This value will be set initially when form is loaded
suggested_value?: HaFormData;
};
context?: Record<string, string>;
}
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
flatten?: boolean;
column_min_width?: string;
schema: readonly HaFormSchema[];
}
export interface HaFormExpandableSchema extends HaFormBaseSchema {
type: "expandable";
flatten?: boolean;
title?: string;
icon?: string;
iconPath?: string;
expanded?: boolean;
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
schema: readonly HaFormSchema[];
}
export interface HaFormOptionalActionsSchema extends HaFormBaseSchema {
type: "optional_actions";
flatten?: boolean;
schema: readonly HaFormSchema[];
}
export interface HaFormSelector extends HaFormBaseSchema {
type?: never;
selector: Selector;
}
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value?: string;
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
valueMin?: number;
valueMax?: number;
}
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options: readonly (readonly [string, string])[];
}
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options:
| Record<string, string>
| readonly string[]
| readonly (readonly [string, string])[];
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
type: "float";
}
export interface HaFormStringSchema extends HaFormBaseSchema {
type: "string";
format?: string;
autocomplete?: string;
autofocus?: boolean;
}
export interface HaFormBooleanSchema extends HaFormBaseSchema {
type: "boolean";
}
export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "positive_time_period_dict";
}
// Type utility to unionize a schema array by flattening any grid schemas
export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number],
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
? SchemaUnion<Schema["schema"]> | Schema
: Schema;
export type HaFormDataContainer = Record<string, HaFormData>;
export type HaFormData =
| HaFormStringData
| HaFormIntegerData
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormMultiSelectData
| HaFormTimeData;
export type HaFormStringData = string;
export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export type HaFormMultiSelectData = string[];
export type HaFormTimeData = HaDurationData;
export interface HaFormElement extends LitElement {
schema: HaFormSchema | readonly HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
}
export type Selector =
| ActionSelector
| AddonSelector
| AreaSelector
| AttributeSelector
| BooleanSelector
| ColorRGBSelector
| ColorTempSelector
| UiColorSelector
| DateSelector
| DateTimeSelector
| DeviceSelector
| DurationSelector
| EntitySelector
| IconSelector
| LocationSelector
| MediaSelector
| NumberSelector
| ObjectSelector
| SelectSelector
| StringSelector
| TargetSelector
| TemplateSelector
| ThemeSelector
| TimeSelector
| UiActionSelector;
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
action: {};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
multiple?: boolean;
};
}
export interface AttributeSelector {
attribute: {
entity_id?: string;
};
}
export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
boolean: {};
}
export interface ColorRGBSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
color_rgb: {};
}
export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_color: {};
}
export interface ColorTempSelector {
color_temp: {
min_mireds?: number;
max_mireds?: number;
};
}
export interface DateSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
date: {};
}
export interface DateTimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
datetime: {};
}
export interface DeviceSelector {
device: {
integration?: string;
manufacturer?: string;
model?: string;
entity?: {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
multiple?: boolean;
};
}
export interface DurationSelector {
duration: {
enable_day?: boolean;
};
}
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | string[];
device_class?: string;
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
};
}
export interface IconSelector {
icon: {
placeholder?: string;
fallbackPath?: string;
};
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string };
}
export interface LocationSelectorValue {
latitude: number;
longitude: number;
radius?: number;
}
export interface MediaSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
media: {};
}
export interface MediaSelectorValue {
entity_id?: string;
media_content_id?: string;
media_content_type?: string;
metadata?: {
title?: string;
thumbnail?: string | null;
media_class?: string;
children_media_class?: string | null;
navigateIds?: { media_content_type: string; media_content_id: string }[];
};
}
export interface NumberSelector {
number: {
min?: number;
max?: number;
step?: number;
mode?: "box" | "slider";
unit_of_measurement?: string;
};
}
export interface ObjectSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
object: {};
}
export interface SelectOption {
value: string;
label: string;
}
export interface SelectSelector {
select: {
multiple?: boolean;
custom_value?: boolean;
mode?: "list" | "dropdown";
options: string[] | SelectOption[];
};
}
export interface StringSelector {
text: {
multiline?: boolean;
type?:
| "number"
| "text"
| "search"
| "tel"
| "url"
| "email"
| "password"
| "date"
| "month"
| "week"
| "time"
| "datetime-local"
| "color";
suffix?: string;
};
}
export interface TargetSelector {
target: {
entity?: {
integration?: EntitySelector["entity"]["integration"];
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
device?: {
integration?: DeviceSelector["device"]["integration"];
manufacturer?: DeviceSelector["device"]["manufacturer"];
model?: DeviceSelector["device"]["model"];
};
};
}
export interface TemplateSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
template: {};
}
export interface ThemeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
theme: {};
}
export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
time: {};
}
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
export interface UiActionSelector {
ui_action: {
actions?: UiAction[];
} | null;
}
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
}
export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service" | "perform-action";
/** @deprecated "service" is kept for backwards compatibility. Replaced by "perform_action". */
service?: string;
perform_action: string;
target?: any;
/** @deprecated "service_data" is kept for backwards compatibility. Replaced by "data". */
service_data?: Record<string, unknown>;
data?: Record<string, unknown>;
}
export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate";
navigation_path: string;
}
export interface UrlActionConfig extends BaseActionConfig {
action: "url";
url_path: string;
}
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
}
export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
export interface CustomActionConfig extends BaseActionConfig {
action: "fire-dom-event";
}
export interface AssistActionConfig extends BaseActionConfig {
action: "assist";
pipeline_id?: string;
start_listening?: boolean;
}
export interface BaseActionConfig {
action: string;
confirmation?: ConfirmationRestrictionConfig;
}
export interface ConfirmationRestrictionConfig {
text?: string;
exemptions?: RestrictionConfig[];
}
export interface RestrictionConfig {
user: string;
}
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
| NavigateActionConfig
| UrlActionConfig
| MoreInfoActionConfig
| AssistActionConfig
| NoActionConfig
| CustomActionConfig;
================================================
FILE: src/editor/item-editor.ts
================================================
import { LitElement, html, css, CSSResult, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { ITEM_EDITOR_TAG } from '../card-tags';
import { EntitySettings } from '../types';
import { localize } from '../localize/localize';
import { PresetList } from '../presets';
import { HaFormSchema } from './ha-form';
import { fireEvent, HomeAssistant } from '../utils';
const BASE_SCHEMA: HaFormSchema[] = [
{
name: "general",
type: "expandable",
flatten: true,
expanded: true,
title: localize('editor.settings.general_settings', true),
icon: "mdi:text",
schema: [
{
name: "entity",
selector: { entity: { domain: "sensor"} }
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "icon", selector: { icon: {} } },
{ name: "attribute", selector: { attribute: {}}, context: { filter_entity: "entity" } },
{ name: "preset", selector: { select: { options: PresetList as any as string[], mode: 'dropdown' } } },
]
},
]
},
{
name: "Value Settings",
type: "expandable",
flatten: true,
title: localize('editor.settings.value', true) + " " +localize('editor.settings.settings', true),
icon: "mdi:numeric",
schema: [
{
type: "grid",
name: "",
schema: [
{ name: "unit_of_display", selector: { text: {} } },
{ name: "decimals", selector: { number: { step: 1} } },
{ name: "invert_value", type: "boolean"},
{ name: "display_abs", type: "boolean"},
{ name: "hide_arrows", type: "boolean"},
{ name: "calc_excluded", type: "boolean"},
{ name: "threshold", selector: { number: { } } },
]
}
]
},
{
name: "Secondary Info",
type: "expandable",
flatten: true,
title: localize('editor.settings.secondary_info', true),
icon: "mdi:attachment-plus",
schema: [
{ name: "secondary_info_entity",
selector: { entity: { domain: "sensor"} } },
{ name: "secondary_info_attribute", selector: { attribute: {}}, context: { filter_entity: "secondary_info_entity" }},
{ name: "secondary_info_decimals", selector: { number: { step: 1 } } },
{ name: "secondary_info_replace_name", type: "boolean"},
]
},
{
name: "Action Settings",
type: "expandable",
flatten: true,
title: localize('editor.settings.action_settings', true),
icon: "mdi:gesture-tap",
schema: [
{
type: "grid",
name: "",
schema: [
{
name: "tap_action",
selector: { ui_action: {} },
},
{
name: "double_tap_action",
selector: { ui_action: {} },
}
]
}
]
},
{
name: "Color Settings",
type: "expandable",
flatten: true,
title: localize('editor.settings.color_settings', true),
icon: "mdi:palette",
schema: [
{ name: "color_threshold", selector: { number: { } } },
{
type: "grid",
name: "",
schema: [
{ name: "icon_color_bigger", selector: { ui_color: {} } },
{ name: "arrow_color_bigger", selector: { ui_color: {} } },
{ name: "icon_color_equal", selector: { ui_color: {} } },
{ name: "arrow_color_equal", selector: { ui_color: {} } },
{ name: "icon_color_smaller", selector: { ui_color: {} } },
{ name: "arrow_color_smaller", selector: { ui_color: {} } },
]
}
]
}
];
const PRESET_LABEL_MAP: Record<string, string> = {
battery_percentage_entity: 'battery_percentage',
grid_buy_entity: 'grid_buy',
grid_sell_entity: 'grid_sell',
secondary_info_decimals: 'decimals',
};
export class ItemEditor extends LitElement {
@property({ attribute: false }) config?: EntitySettings;
@property({ attribute: false }) hass?: HomeAssistant;
private get _flatConfig() {
const c = this.config!;
return {
...c,
icon_color_bigger: c.icon_color?.bigger,
icon_color_equal: c.icon_color?.equal,
icon_color_smaller: c.icon_color?.smaller,
arrow_color_bigger: c.arrow_color?.bigger,
arrow_color_equal: c.arrow_color?.equal,
arrow_color_smaller: c.arrow_color?.smaller,
};
}
private get _schema(): HaFormSchema[] {
const preset = this.config?.preset;
const presetFields: HaFormSchema[] =
preset === 'battery'
? [{ name: 'battery_percentage_entity', selector: { entity: {} } }]
: preset === 'grid'
? [
{ name: 'grid_buy_entity', selector: { entity: {} } },
{ name: 'grid_sell_entity', selector: { entity: {} } },
]
: [];
if (presetFields.length === 0) return BASE_SCHEMA;
const presetSection: HaFormSchema = {
name: 'preset_section',
type: 'expandable',
flatten: true,
title: localize('editor.settings.preset_settings', true),
icon: "mdi:shape",
schema: presetFields,
};
return [BASE_SCHEMA[0], presetSection, ...BASE_SCHEMA.slice(1)];
}
private _computeLabel = (schema: HaFormSchema) => {
const key = PRESET_LABEL_MAP[schema.name] ?? schema.name;
return `${localize('editor.settings.' + key)} ${!schema.required ? `(${localize('editor.optional')})` : ''}`;
};
protected render() {
// If its a placeholder, don't render anything
if (!this.hass || !this.config || this.config.preset == 'placeholder') {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._flatConfig}
.schema=${this._schema}
.computeLabel=${this._computeLabel}
@value-changed=${this._formValueChanged}
></ha-form>
`;
}
private _formValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this.config || !this.hass) return;
const {
icon_color_bigger, icon_color_equal, icon_color_smaller,
arrow_color_bigger, arrow_color_equal, arrow_color_smaller,
...rest
} = ev.detail.value;
const icon_color = (icon_color_bigger || icon_color_equal || icon_color_smaller)
? { bigger: icon_color_bigger || undefined, equal: icon_color_equal || undefined, smaller: icon_color_smaller || undefined }
: undefined;
const arrow_color = (arrow_color_bigger || arrow_color_equal || arrow_color_smaller)
? { bigger: arrow_color_bigger || undefined, equal: arrow_color_equal || undefined, smaller: arrow_color_smaller || undefined }
: undefined;
fireEvent<any>(this, 'config-changed', { ...rest, icon_color, arrow_color });
}
static get styles(): CSSResult {
return css`
.checkbox {
display: flex;
align-items: center;
padding: 8px 0;
}
.checkbox input {
height: 20px;
width: 20px;
margin-left: 0;
margin-right: 8px;
}
h3 {
margin-bottom: 0.5em;
}
.row {
margin-bottom: 12px;
margin-top: 12px;
display: block;
}
.side-by-side {
display: flex;
}
.side-by-side > * {
flex: 1 1 0%;
padding-right: 4px;
}
`;
}
}
customElements.define(ITEM_EDITOR_TAG, ItemEditor);
================================================
FILE: src/editor/items-editor.ts
================================================
import { LitElement, html } from 'lit';
import { EditorTarget, EntitySettings, HTMLElementValue } from '../types';
import { localize } from '../localize/localize';
import { property, state } from 'lit/decorators.js';
import { ITEMS_EDITOR_TAG } from '../card-tags';
import { repeat } from 'lit/directives/repeat.js';
import { css, CSSResult, nothing } from 'lit';
import { mdiClose, mdiPencil, mdiPlusCircleOutline } from '@mdi/js';
import { DefaultItem, PresetList, PresetObject } from '../presets';
import { fireCustomEvent, HomeAssistant } from '../utils';
export class ItemsEditor extends LitElement {
@property({ attribute: false }) entities?: EntitySettings[];
@property({ attribute: false }) hass?: HomeAssistant;
@state() private _selectedPreset: string = PresetList[0];
private _entityKeys = new WeakMap<EntitySettings, string>();
private _getKey(action: EntitySettings) {
if (!this._entityKeys.has(action)) {
this._entityKeys.set(action, Math.random().toString());
}
return this._entityKeys.get(action)!;
}
public disconnectedCallback() {
super.disconnectedCallback();
}
protected render() {
if (!this.entities || !this.hass) {
return nothing;
}
return html`
<h3>${localize('editor.settings.entities')}</h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._rowMoved}>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => html`
<div class="entity">
<div class="handle">
<ha-icon icon="mdi:drag"></ha-icon>
</div>
<ha-entity-picker
allow-custom-entity
hideClearIcon
.hass=${this.hass}
.configValue=${'entity'}
.value=${entityConf.entity}
.index=${index}
@value-changed=${this._valueChanged}
></ha-entity-picker>
<ha-icon-button
.label=${localize('editor.actions.remove')}
.path=${mdiClose}
class="remove-icon"
.index=${index}
@click=${this._removeRow}
></ha-icon-button>
<ha-icon-button
.label=${localize('editor.actions.edit')}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click="${this._editRow}"
></ha-icon-button>
</div>
`,
)}
</div>
</ha-sortable>
<div class="add-item row">
<ha-select
label="${localize('editor.settings.preset')}"
class="add-preset"
.value=${this._selectedPreset}
.options=${PresetList.map((val) => ({ value: val, label: val }))}
@selected=${(ev: CustomEvent<{ value: string }>) => { this._selectedPreset = ev.detail.value; }}
></ha-select>
<ha-entity-picker .hass=${this.hass} name="entity" class="add-entity"></ha-entity-picker>
<ha-icon-button
.label=${localize('editor.actions.add')}
.path=${mdiPlusCircleOutline}
class="add-icon"
@click="${this._addRow}"
></ha-icon-button>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
if (!this.entities || !this.hass) {
return;
}
const value = ev.detail.value;
const index = (ev.target as any).index;
const newConfigEntities = this.entities!.concat();
newConfigEntities[index] = {
...newConfigEntities[index],
entity: value || '',
};
fireCustomEvent<EntitySettings[]>(this, 'config-changed', newConfigEntities);
}
private _removeRow(ev: Event): void {
ev.stopPropagation();
const index = (ev.currentTarget as EditorTarget).index;
if (index != undefined) {
const entities = this.entities!.concat();
entities.splice(index, 1);
fireCustomEvent<EntitySettings[]>(this, 'config-changed', entities);
}
}
private _editRow(ev: Event): void {
ev.stopPropagation();
const index = (ev.target as EditorTarget).index;
if (index != undefined) {
fireCustomEvent<number>(this, 'edit-item', index);
}
}
private _addRow(ev: Event): void {
ev.stopPropagation();
if (!this.entities || !this.hass) {
return;
}
const preset = this._selectedPreset || 'placeholder';
const entity_id = (this.shadowRoot!.querySelector('.add-entity') as HTMLElementValue).value;
const item = Object.assign({}, DefaultItem, PresetObject[preset], {
entity: entity_id,
preset: entity_id == '' ? 'placeholder' : preset,
});
fireCustomEvent<EntitySettings[]>(this, 'config-changed', [...this.entities, item]);
}
private _rowMoved(ev: CustomEvent<{ oldIndex: number; newIndex: number }>): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
if (oldIndex === newIndex || !this.entities) return;
const newEntities = this.entities.concat();
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
fireCustomEvent<EntitySettings[]>(this, 'config-changed', newEntities);
}
static get styles(): CSSResult {
return css`
.entity,
.add-item {
display: flex;
align-items: center;
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
padding-right: 8px;
cursor: move;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.entity ha-entity-picker,
.add-item ha-entity-picker {
flex-grow: 1;
}
.entities {
margin-bottom: 8px;
}
.add-preset {
padding-right: 8px;
max-width: 130px;
}
.remove-icon,
.edit-icon,
.add-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
`;
}
}
customElements.define(ITEMS_EDITOR_TAG, ItemsEditor);
================================================
FILE: src/localize/languages/de.json
================================================
{
"common": {
"description": "Eine Karte zur Visualizierung von Stromverteilungen"
},
"editor": {
"actions": {
"add": "Hinzufügen",
"edit": "Bearbeiten",
"remove": "Entfernen"
},
"optional": "Optional",
"settings": {
"action_settings": "Interaktions Einstellungen",
"animation": "Animation",
"autarky": "Autarkie",
"attribute": "Attribut",
"background_color": "Hintergrundfarbe",
"battery_percentage": "Batterie Ladung %",
"arrow_color_bigger": "Pfeil - Größer",
"arrow_color_equal": "Pfeil - Gleich",
"arrow_color_smaller": "Pfeil - Kleiner",
"icon_color_bigger": "Symbol - Größer",
"icon_color_equal": "Symbol - Gleich",
"icon_color_smaller": "Symbol - Kleiner",
"calc_excluded": "Von Rechnungen ausschließen",
"center": "Mittelbereich",
"color": "Farbe",
"color_settings": "Farb Einstellungen",
"color_threshold": "Farb-Schwellenwert",
"decimals": "Dezimalstellen",
"display_abs": "Absolute Wertanzeige",
"double_tap_action": "Doppel Tipp Aktion",
"entities": "Entities",
"entity": "Element",
"general_settings": "Allgemeine Einstellungen",
"grid_buy": "Netz Ankauf",
"grid_sell": "Netz Verkauf",
"hide_arrows": "Pfeile Verstecken",
"lower_bound": "Untere Grenze",
"upper_bound": "Obere Grenze",
"preset_settings": "Vorlagen Einstellungen",
"icon": "Symbol",
"invert_value": "Wert Invertieren",
"name": "Name",
"preset": "Vorlagen",
"ratio": "Anteil",
"secondary_info": "Zusatzinformationen",
"secondary_info_entity": "Element",
"secondary_info_attribute": "Attribut",
"secondary_info_replace_name": "Namen Ersetzen",
"settings": "Einstellungen",
"tap_action": "Tipp Aktion",
"threshold": "Schwellenwert",
"title": "Titel",
"unit_of_display": "Angezeigte Einheit",
"value": "Wert"
}
}
}
================================================
FILE: src/localize/languages/en.json
================================================
{
"common": {
"description": "A Lovelace Card for visualizing power distributions."
},
"editor": {
"actions": {
"add": "Add",
"edit": "Edit",
"remove": "Remove"
},
"optional": "Optional",
"settings": {
"action_settings": "Action Settings",
"animation": "Animation",
"autarky": "autarky",
"attribute": "Attribute",
"background_color": "Background Color",
"battery_percentage": "Battery Charge %",
"arrow_color_bigger": "Arrow - Bigger",
"arrow_color_equal": "Arrow - Equal",
"arrow_color_smaller": "Arrow - Smaller",
"icon_color_bigger": "Icon - Bigger",
"icon_color_equal": "Icon - Equal",
"icon_color_smaller": "Icon - Smaller",
"calc_excluded": "Excluded from Calculations",
"center": "Center",
"color": "Color",
"color_settings": "Color Settings",
"color_threshold": "Color Threshold",
"decimals": "Decimals",
"display_abs": "Display Absolute Value",
"double_tap_action": "Double Tap Action",
"entities": "Entities",
"entity": "Entity",
"general_settings": "General Settings",
"grid_buy": "Grid Buy",
"grid_sell": "Grid Sell",
"hide_arrows": "Hide Arrows",
"lower_bound": "Lower Bound",
"upper_bound": "Upper Bound",
"preset_settings": "Preset Settings",
"icon": "Icon",
"invert_value": "Invert Value",
"name": "Name",
"preset": "Preset",
"ratio": "ratio",
"secondary_info": "Secondary Info",
"secondary_info_entity": "Entity",
"secondary_info_attribute": "Attribute",
"secondary_info_replace_name": "Replace Name",
"settings": "Settings",
"tap_action": "Tap Action",
"threshold": "Threshold",
"title": "Title",
"unit_of_display": "Unit of Display",
"value": "value"
}
}
}
================================================
FILE: src/localize/languages/sk.json
================================================
{
"common": {
"description": "A Lovelace Card for visualizing power distributions."
},
"editor": {
"actions": {
"add": "Pridať",
"edit": "Editovať",
"remove": "Odobrať"
},
"optional": "Voliteľné",
"settings": {
"action_settings": "Nastavenia akcie",
"animation": "Animácia",
"autarky": "sebestačnosť",
"attribute": "Atribút",
"background_color": "Farba pozadia",
"battery_percentage": "Nabitie batérie %",
"arrow_color_bigger": "Šípka - Väčšie",
"arrow_color_equal": "Šípka - Rovné",
"arrow_color_smaller": "Šípka - Menšie",
"icon_color_bigger": "Ikona - Väčšie",
"icon_color_equal": "Ikona - Rovné",
"icon_color_smaller": "Ikona - Menšie",
"calc_excluded": "Vylúčené z výpočtov",
"center": "Centrum",
"color": "Farba",
"color_settings": "Nastavenia farby",
"color_threshold": "Prah farby",
"decimals": "Desatinné čísla",
"display_abs": "Zobraziť absolútnu hodnotu",
"double_tap_action": "Akcia dvojitého klepnutia",
"entities": "Entity",
"entity": "Entita",
"general_settings": "Všeobecné nastavenia",
"grid_buy": "Sieť nákup",
"grid_sell": "Sieť predaj",
"hide_arrows": "Skryť šípky",
"lower_bound": "Dolná hranica",
"upper_bound": "Horná hranica",
"preset_settings": "Nastavenia predvoľby",
"icon": "Ikona",
"invert_value": "Invertovať hodnotu",
"name": "Názov",
"preset": "Predvoľba",
"ratio": "pomer",
"secondary_info": "Sekundárne informácie",
"secondary_info_entity": "Entita",
"secondary_info_attribute": "Atribút",
"secondary_info_replace_name": "Nahradiť názov",
"settings": "nastavenia",
"tap_action": "Akcia klepnutia",
"threshold": "Prah",
"title": "Titul",
"unit_of_display": "Jednotka zobrazenia",
"value": "hodnota"
}
}
}
================================================
FILE: src/localize/localize.ts
================================================
import * as en from './languages/en.json';
import * as de from './languages/de.json';
import { HaFormSchema } from '../editor/ha-form';
import * as sk from './languages/sk.json';
const languages = {
en: en,
de: de,
sk: sk,
};
/**
* Translating Strings to different languages.
* Thanks to custom-cards/spotify-card
* @param string The Section-Key Pair
* @param search String which should be replaced
* @param replace String to replace with
*/
export function localize(string: string, capitalized = false, search = '', replace = ''): string {
const lang = (localStorage.getItem('selectedLanguage') || navigator.language.split('-')[0] || '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']) as unknown as string;
}
if (translated === undefined)
translated = string.split('.').reduce((o, i) => o[i], languages['en']) as unknown as string;
if (search !== '' && replace !== '') {
translated = translated.replace(search, replace);
}
return capitalized ? capitalizeFirstLetter(translated) : translated;
}
function capitalizeFirstLetter(string: string) {
if (!string) return "";
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function computeLabel(schema: HaFormSchema) {
return `${localize('editor.settings.' + schema.name)} ${!schema.required ? `(${localize('editor.optional')})` : ''}`;
}
================================================
FILE: src/power-distribution-card.ts
================================================
import { LitElement, html, TemplateResult, PropertyValues, CSSResultGroup } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { createThing } from './utils';
import { version } from '../package.json';
import { PDCConfig, EntitySettings, ArrowStates, BarSettings } from './types';
import { DefaultItem, DefaultConfig, PresetList, PresetObject } from './presets';
import { styles, narrow_styles } from './styles';
import { localize } from './localize/localize';
import { actionHandler } from './action-handler';
import './editor/editor';
import { ActionHandlerEvent, handleAction, hasAction, HomeAssistant, LovelaceCard, LovelaceCardConfig, registerCustomCard, formatNumber } from './utils';
import { CARD_TAG, EDITOR_TAG } from './card-tags';
import { computeCssColor } from './utils/compute-color';
import { debounce } from './utils/debounce';
console.info(
`%c POWER-DISTRIBUTION-CARD %c ${version} `,
`font-weight: 500; color: black; background:#f6aa1c;`,
`font-weight: 500; color: #f6aa1c; background: #220901;`,
);
registerCustomCard(CARD_TAG, 'Power Distribution Card', localize('common.description'));
@customElement(CARD_TAG)
export class PowerDistributionCard extends LitElement {
/**
* Function for creating the editor for the power-distribution-card
*/
public static async getConfigElement(): Promise<LitElement> {
await import('./editor/editor');
return document.createElement(EDITOR_TAG) as LitElement;
}
/**
* Returns a mock config for preview in the card picker
*/
public static getStubConfig(): Record<string, unknown> {
return {
title: 'Title',
entities: [],
center: {
type: 'bars',
bars: [
{ preset: 'autarky', name: localize('editor.settings.autarky') },
{ preset: 'ratio', name: localize('editor.settings.ratio') },
],
},
};
}
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config!: PDCConfig;
@property() private _card!: LovelaceCard;
private _resizeObserver?: ResizeObserver;
@state() private _narrow = false;
/**
* Configuring all the passed Settings and Changing it to a more usefull Internal one.
* @param config The Config Object configured via YAML
*/
public async setConfig(config: PDCConfig): Promise<void> {
//The Addition of the last object is needed to override the entities array for the preset settings
const _config = Object.assign({}, DefaultConfig, config);
// Migrate old format: center.content -> center.bars or center.card
if (_config.center && 'content' in _config.center) {
const oldContent = (_config.center as any).content;
const { content: _removed, ..._centerWithoutContent } = _config.center as any;
if (_config.center.type === 'bars') {
_config.center = { ..._centerWithoutContent, bars: oldContent as BarSettings[] };
} else if (_config.center.type === 'card') {
_config.center = { ..._centerWithoutContent, card: oldContent as import('./utils').LovelaceCardConfig };
} else {
_config.center = _centerWithoutContent;
}
}
// Applying Defaults depending on preset
_config.entities = config.entities.map((item) => {
if (item.preset && PresetList.includes(item.preset)) {
return Object.assign({}, DefaultItem, PresetObject[item.preset], item);
} else {
return item;
}
});
this._config = _config;
}
public firstUpdated(): void {
const _config = this._config;
//unit-of-measurement Auto Configuration from hass element
_config.entities.forEach((item, index) => {
if (item.entity && !item.unit_of_measurement) {
const hass_uom = this._state({ entity: item.entity, attribute: 'unit_of_measurement' }) as string;
this._config.entities[index].unit_of_measurement = hass_uom || 'W';
}
});
// Applying the same to bars
if (_config.center.type == 'bars' && _config.center.bars) {
const bars = _config.center.bars.map((item) => {
if (item.unit_of_measurement) return item;
let hass_uom = '%';
if (item.entity) {
hass_uom = this._state({ entity: item.entity, attribute: 'unit_of_measurement' }) as string;
}
return Object.assign({}, item, { unit_of_measurement: item.unit_of_measurement || hass_uom });
});
this._config.center = {
...this._config.center,
bars: bars,
};
} else if (this._config.center.type == 'card' && this._config.center.card) {
this._card = this._createCardElement(this._config.center.card);
}
//Resize Observer
this._adjustWidth();
this._attachObserver();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._card || (!changedProps.has('hass') && !changedProps.has('editMode'))) {
return;
}
if (this.hass) {
this._card.hass = this.hass;
}
}
public static get styles(): CSSResultGroup {
return styles;
}
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback(): void {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
this._resizeObserver = new ResizeObserver(debounce(() => this._adjustWidth(), 250, false));
}
const card = this.shadowRoot?.querySelector('ha-card');
// If we show an error or warning there is no ha-card
if (!card) return;
this._resizeObserver.observe(card);
}
private _adjustWidth(): void {
const card = this.shadowRoot?.querySelector('ha-card');
if (!card) return;
this._narrow = card.offsetWidth < 400;
}
private _formatValue(rawValue: number, entity?: string, decimals?: number): [string, number] {
const precision = decimals != null
? decimals
: (entity ? (this.hass.entities[entity]?.display_precision ?? 2) : 2);
const factor = 10 ** precision;
const rounded = Math.round(rawValue * factor) / factor;
return [formatNumber(rounded, this.hass.locale), rounded];
}
/**
* Retrieving the sensor value of hass for a Item as a number
* @param item a Settings object
* @returns The current value from Homeassistant in Watts
*/
private _val(item: EntitySettings | BarSettings): number {
let modifier = item.invert_value ? -1 : 1;
//Proper K Scaling e.g. 1kW = 1000W
if (item.unit_of_measurement?.charAt(0) == 'k') modifier *= 1000;
// If an entity exists, check if the attribute setting is entered -> value from attribute else value from entity
let num = this._state(item as EntitySettings) as number;
//Applying Threshold
const threshold = (item as EntitySettings).threshold || null;
num = threshold ? (Math.abs(num) < threshold ? 0 : num) : num;
return num * modifier;
}
/**
* Retrieving the raw state of an sensor/attribute
* @param item A Settings object
* @returns entitys/attributes state
*/
private _state(item: EntitySettings): unknown {
return item.entity && this.hass.states[item.entity]
? item.attribute
? this.hass.states[item.entity].attributes[item.attribute]
: this.hass.states[item.entity].state
: null;
}
/**
* This is the main rendering function for this card
* @returns html for the power-distribution-card
*/
protected render(): TemplateResult {
const left_panel: TemplateResult[] = [];
const center_panel: (TemplateResult | LovelaceCard)[] = [];
const right_panel: TemplateResult[] = [];
let consumption = 0;
let production = 0;
this._config.entities.forEach((item, index) => {
const value = this._val(item);
if (!item.calc_excluded) {
if (item.producer && value > 0) {
production += value;
}
if (item.consumer && value < 0) {
consumption -= value;
}
}
const _item = this._render_item(value, item, index);
//Sorting the Items to either side
if (index % 2 == 0) left_panel.push(_item);
else right_panel.push(_item);
});
//Populating the Center Panel
const center = this._config.center;
switch (center.type) {
case 'none':
break;
case 'card':
if (this._card) {
center_panel.push(this._card);
} else {
console.warn('NO CARD');
}
break;
case 'bars':
center_panel.push(this._render_bars(consumption, production));
break;
}
return html` ${this._narrow ? narrow_styles : undefined}
<ha-card .header=${this._config.title}>
<div class="card-content">
<div id="left-panel">${left_panel}</div>
<div id="center-panel">${center_panel}</div>
<div id="right-panel">${right_panel}</div>
</div>
</ha-card>`;
}
private _handleAction(ev: ActionHandlerEvent): void {
if (this.hass && this._config && ev.detail.action) {
handleAction(
this,
this.hass,
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
entity: (ev.currentTarget as any).entity,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tap_action: (ev.currentTarget as any).tap_action,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
double_tap_action: (ev.currentTarget as any).double_tap_action,
},
ev.detail.action,
);
}
}
/**
* Creating a Item Element
* @param value The Value of the Sensor
* @param item The EntitySettings Object of the Item
* @param index The index of the Item. This is needed for the Arrow Directions.
* @returns Html for a single Item
*/
private _render_item(value: number, item: EntitySettings, index: number): TemplateResult {
//Placeholder item
if (!item.entity) {
return html`<item class="placeholder"></item>`;
}
let math_value = value;
//Unit-Of-Display and Unit_of_measurement
let unit_of_display = item.unit_of_display || 'W';
const uod_split = unit_of_display.charAt(0);
if (uod_split[0] == 'k') {
math_value /= 1000;
} else if (item.unit_of_display == 'adaptive') {
//Using the uom suffix enables to adapt the initial unit to the automatic scaling naming
let uom_suffix = 'W';
if (item.unit_of_measurement) {
uom_suffix =
item.unit_of_measurement[0] == 'k' ? item.unit_of_measurement.substring(1) : item.unit_of_measurement;
}
if (Math.abs(math_value) > 999) {
math_value /= 1000;
unit_of_display = 'k' + uom_suffix;
} else {
unit_of_display = uom_suffix;
}
}
// Arrow directions
const state = item.invert_arrow ? math_value * -1 : math_value;
// Toggle Absolute Values
math_value = item.display_abs ? Math.abs(math_value) : math_value;
// Decimal Precision
let [formatValue, formatted_math_value] = this._formatValue(math_value, item.entity, item.decimals);
//NaNFlag for Offline Sensors for example
const NanFlag = isNaN(formatted_math_value);
// Secondary info
let secondary_info: string | undefined;
if (item.secondary_info_entity) {
if (item.secondary_info_attribute) {
secondary_info =
this._state({ entity: item.secondary_info_entity, attribute: item.secondary_info_attribute }) + '';
} else {
const siRaw = this._state({ entity: item.secondary_info_entity }) as string;
if (isNaN(parseFloat(siRaw))) {
secondary_info = String(siRaw);
} else {
secondary_info = `${this._formatValue(parseFloat(siRaw), item.secondary_info_entity, item.secondary_info_decimals)[0]}${this._state({ entity: item.secondary_info_entity, attribute: 'unit_of_measurement' }) || ''}`;
}
}
}
// Secondary info replace name
let displayName = item.name;
if (item.secondary_info_replace_name) {
displayName = secondary_info;
secondary_info = undefined;
}
//Preset Features
// 1. Battery Icon
let icon = item.icon;
if (item.preset === 'battery' && item.battery_percentage_entity) {
const bat_val = this._val({ entity: item.battery_percentage_entity });
if (!isNaN(bat_val)) {
icon = 'mdi:battery';
// mdi:battery-100 and -0 don't exist thats why we have to handle it seperately
if (bat_val < 5) {
icon = 'mdi:battery-outline';
} else if (bat_val < 95) {
icon = 'mdi:battery-' + (bat_val / 10).toFixed(0) + '0';
}
}
}
// 2. Grid Buy-Sell
let nameReplaceFlag = false;
let grid_buy_sell = html``;
if (item.preset === 'grid' && (item.grid_buy_entity || item.grid_sell_entity)) {
nameReplaceFlag = true;
const gridBuyValue = item.grid_buy_entity
? this._formatValue(this._val({ entity: item.grid_buy_entity }), item.grid_buy_entity, item.decimals)[0]
: undefined;
const gridSellValue = item.grid_sell_entity
? this._formatValue(this._val({ entity: item.grid_sell_entity }), item.grid_sell_entity, item.decimals)[0]
: undefined;
grid_buy_sell = html`
<div class="buy-sell">
${item.grid_buy_entity
? html`<div class="grid-buy">
B:
${gridBuyValue}${this._state({
entity: item.grid_buy_entity,
attribute: 'unit_of_measurement',
}) || undefined}
</div>`
: undefined}
${item.grid_sell_entity
? html`<div class="grid-sell">
S:
${gridSellValue}${this._state({
entity: item.grid_sell_entity,
attribute: 'unit_of_measurement',
}) || undefined}
</div>`
: undefined}
</div>
`;
}
// COLOR CHANGE
const ct = item.color_threshold || 0;
// Icon color dependant on state
let icon_color: string | undefined;
if (item.icon_color) {
if (state > ct) icon_color = item.icon_color.bigger;
if (state < ct) icon_color = item.icon_color.smaller;
if (state == ct) icon_color = item.icon_color.equal;
if (icon_color) icon_color = computeCssColor(icon_color);
}
// Arrow color
let arrow_color: string | undefined;
if (item.arrow_color) {
if (state > ct) arrow_color = item.arrow_color.bigger;
if (state < ct) arrow_color = item.arrow_color.smaller;
if (state == ct) arrow_color = item.arrow_color.equal;
if (arrow_color) arrow_color = computeCssColor(arrow_color);
}
return html`
<item
.entity=${item.entity}
.tap_action=${item.tap_action}
.double_tap_action=${item.double_tap_action}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasDoubleClick: hasAction(item.double_tap_action),
})}
>
<badge>
<icon>
<ha-icon icon="${icon}" style="${icon_color ? `color:${icon_color};` : ''}"></ha-icon>
${secondary_info ? html`<p class="secondary">${secondary_info}</p>` : null}
</icon>
${nameReplaceFlag ? grid_buy_sell : html`<p class="subtitle">${displayName}</p>`}
</badge>
<value>
<p>${NanFlag ? `` : formatValue} ${NanFlag ? `` : unit_of_display}</p>
${!item.hide_arrows
? this._render_arrow(
//This takes the side the item is on (index even = left) into account for the arrows
value == 0 || NanFlag
? 'none'
: index % 2 == 0
? state > 0
? 'right'
: 'left'
: state > 0
? 'left'
: 'right',
arrow_color,
)
: html``
}
</value>
</item>
`;
}
/**
* Render function for Generating Arrows (CSS Only)
* @param direction One of three Options: none, right, left
* @param index To detect which side the item is on and adapt the direction accordingly
*/
private _render_arrow(direction: ArrowStates, color?: string): TemplateResult {
const a = this._config.animation;
if (direction == 'none') {
return html` <div class="blank" style="${color ? `background-color:${color};` : ''}"></div> `;
} else {
return html`
<div class="arrow-container ${direction}">
<div class="arrow ${a} " style="border-left-color: ${color};"></div>
<div class="arrow ${a} ${a == 'flash' ? 'delay-1' : ''}" style="border-left-color: ${color};"></div>
<div class="arrow ${a} ${a == 'flash' ? 'delay-2' : ''}" style="border-left-color: ${color};"></div>
<div class="arrow ${a}" style="border-left-color: ${color};"></div>
</div>
`;
}
}
/**
* Render Support Function Calculating and Generating the Autarky and Ratio Bars
* @param consumption the total home consumption
* @param production the total home production
* @returns html containing the bars as Template Results
*/
private _render_bars(consumption: number, production: number): TemplateResult {
const bars: TemplateResult[] = [];
if (!this._config.center.bars || this._config.center.bars.length == 0) return html``;
this._config.center.bars.forEach((element) => {
let value = -1;
switch (element.preset) {
case 'autarky': //Autarky in Percent = Home Production(Solar, Battery)*100 / Home Consumption
if (!element.entity)
value = consumption != 0 ? Math.min(Math.round((production * 100) / Math.abs(consumption)), 100) : 0;
break;
case 'ratio': //Ratio in Percent = Home Consumption / Home Production(Solar, Battery)*100
if (!element.entity)
value = production != 0 ? Math.min(Math.round((Math.abs(consumption) * 100) / production), 100) : 0;
break;
}
const rawValue = value < 0 ? parseInt(this._val(element).toFixed(0), 10) : value;
const lb = element.lower_bound ?? 0;
const ub = element.upper_bound ?? 100;
const barHeight = Math.min(Math.max(((rawValue - lb) / (ub - lb)) * 100, 0), 100);
bars.push(html`
<div
class="bar-element"
.entity=${element.entity}
.tap_action=${element.tap_action}
.double_tap_action=${element.double_tap_action}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasDoubleClick: hasAction(element.double_tap_action),
})}
style="${element.tap_action || element.double_tap_action ? 'cursor: pointer;' : ''}"
>
<p class="bar-percentage">${Math.round(barHeight)}${element.unit_of_measurement || '%'}</p>
<div class="bar-wrapper" style="${element.bar_bg_color ? `background-color:${computeCssColor(element.bar_bg_color)};` : ''}">
<bar style="height:${barHeight}%; background-color:${element.bar_color ? computeCssColor(element.bar_color) : ''};" />
</div>
<p>${element.name || ''}</p>
</div>
`);
});
return html`${bars}`;
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = createThing(cardConfig) as LovelaceCard;
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
'll-rebuild',
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
},
{ once: true },
);
return element;
}
private _rebuildCard(cardElToReplace: LovelaceCard, config: LovelaceCardConfig): void {
const newCardEl = this._createCardElement(config);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
}
if (this._card === cardElToReplace) {
this._card = newCardEl;
}
}
}
================================================
FILE: src/presets.ts
================================================
import { EntitySettings, PDCConfig } from './types';
export type PresetType = (typeof PresetList)[number];
export const PresetList = [
'battery',
'car_charger',
'consumer',
'grid',
'home',
'hydro',
'pool',
'producer',
'solar',
'wind',
'heating',
'placeholder',
] as const;
export const PresetObject: { [key: string]: EntitySettings } = {
battery: {
consumer: true,
icon: 'mdi:battery-outline',
name: 'battery',
producer: true,
},
car_charger: {
consumer: true,
icon: 'mdi:car-electric',
name: 'car',
},
consumer: {
consumer: true,
icon: 'mdi:lightbulb',
name: 'consumer',
},
grid: {
icon: 'mdi:transmission-tower',
name: 'grid',
},
home: {
consumer: true,
icon: 'mdi:home-assistant',
name: 'home',
},
hydro: {
icon: 'mdi:hydro-power',
name: 'hydro',
producer: true,
},
pool: {
consumer: true,
icon: 'mdi:pool',
name: 'pool',
},
producer: {
icon: 'mdi:lightning-bolt-outline',
name: 'producer',
producer: true,
},
solar: {
icon: 'mdi:solar-power',
name: 'solar',
producer: true,
},
wind: {
icon: 'mdi:wind-turbine',
name: 'wind',
producer: true,
},
heating: {
icon: 'mdi:radiator',
name: 'heating',
consumer: true,
},
placeholder: {
name: 'placeholder',
},
};
export const DefaultItem: EntitySettings = {
decimals: 2,
display_abs: true,
name: '',
unit_of_display: 'W',
};
export const DefaultConfig: PDCConfig = {
type: '',
title: undefined,
animation: 'flash',
entities: [],
center: {
type: 'none',
},
};
================================================
FILE: src/styles.ts
================================================
import { css, html } from 'lit';
export const styles = css`
* {
box-sizing: border-box;
}
p {
margin: 4px 0 4px 0;
text-align: center;
}
.card-content {
display: grid;
grid-template-columns: 1.5fr 1fr 1.5fr;
column-gap: 10px;
}
#center-panel {
display: flex;
align-items: center;
justify-content: center;
grid-column: 2;
flex-wrap: wrap;
min-width: 100px;
}
#center-panel > div {
display: flex;
width: 100%;
min-height: 150px;
max-height: 200px;
flex-basis: 50%;
flex-flow: column;
}
#center-panel > div > p {
flex: 0 1 auto;
}
.bar-wrapper {
position: relative;
width: 50%;
height: 80%;
margin: auto;
flex: 1 1 auto;
background-color: rgba(114, 114, 114, 0.2);
}
bar {
position: absolute;
right: 0;
bottom: 0;
left: 0;
background-color: var(--secondary-text-color);
}
item {
display: block;
overflow: hidden;
margin-bottom: 10px;
cursor: pointer;
}
.buy-sell {
height: 28px;
display: flex;
flex-direction: column;
font-size: 11px;
line-height: 14px;
text-align: center;
}
.grid-buy {
color: red;
}
.grid-sell {
color: green;
}
.placeholder {
height: 62px;
}
#right-panel > item > value {
float: left;
}
#right-panel > item > badge {
float: right;
}
badge {
float: left;
width: 50%;
padding: 4px;
border: 1px solid;
border-color: var(--disabled-text-color);
border-radius: 1em;
position: relative;
}
icon > ha-icon {
display: block;
width: 24px;
margin: 0 auto;
color: var(--state-icon-color);
}
.secondary {
position: absolute;
top: 4px;
right: 8%;
font-size: 80%;
}
value {
float: right;
width: 50%;
min-width: 54px;
}
value > p {
height: 1em;
}
/**************
ARROW ANIMATION
**************/
.blank {
width: 55px;
height: 4px;
margin: 8px auto 8px auto;
opacity: 0.2;
background-color: var(--secondary-text-color);
}
.arrow-container {
display: flex;
width: 55px;
height: 16px;
overflow: hidden;
margin: auto;
}
.left {
transform: rotate(180deg);
}
.arrow {
width: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-left: 16px solid var(--secondary-text-color);
margin: 0 1.5px;
}
.flash {
animation: flash 3s infinite steps(1);
opacity: 0.2;
}
@keyframes flash {
0%,
66% {
opacity: 0.2;
}
33% {
opacity: 0.8;
}
}
.delay-1 {
animation-delay: 1s;
}
.delay-2 {
animation-delay: 2s;
}
.slide {
animation: slide 1.5s linear infinite both;
position: relative;
left: -19px;
}
@keyframes slide {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(19px);
transform: translateX(19px);
}
}
`;
export const narrow_styles = html`
<style>
/**********
Mobile View
**********/
.card-content {
grid-template-columns: 1fr 1fr 1fr;
}
.placeholder {
height: 114px !important;
}
item > badge,
item > value {
display: block;
float: none !important;
width: 72px;
margin: 0 auto;
}
.arrow {
margin: 0px 8px;
}
</style>
`;
================================================
FILE: src/types.ts
================================================
import { PresetType } from './presets';
import { ActionConfig, LovelaceCardConfig } from './utils';
import { NavigateOptions } from './utils/hass-types/navigate';
declare global {
interface HASSDomEvents {
"action": { action: string };
"config-changed": { config: any };
"hass-more-info": { entityId: string };
"ll-custom": ActionConfig;
"ll-rebuild": Record<string, unknown>;
"ll-upgrade": Record<string, unknown>;
"show-dialog": { dialogTag: string; dialogParams: unknown; dialogImport?: () => Promise<void>; addHistory?: boolean };
"location-changed": NavigateOptions;
}
}
export interface PDCConfig extends LovelaceCardConfig {
title?: string;
animation?: 'none' | 'flash' | 'slide';
entities: EntitySettings[];
center: center;
}
export interface EntitySettings extends presetFeatures {
attribute?: string;
arrow_color?: { bigger?: string; equal?: string; smaller?: string };
calc_excluded?: boolean;
consumer?: boolean;
color_threshold?: number;
decimals?: number;
display_abs?: boolean;
double_tap_action?: ActionConfig;
entity?: string;
hide_arrows?: boolean;
icon?: string;
icon_color?: { bigger?: string; equal?: string; smaller?: string };
invert_value?: boolean;
invert_arrow?: boolean;
name?: string | undefined;
preset?: PresetType;
producer?: boolean;
secondary_info_attribute?: string;
secondary_info_decimals?: number;
secondary_info_entity?: string;
secondary_info_replace_name?: boolean;
tap_action?: ActionConfig;
threshold?: number;
unit_of_display?: string;
unit_of_measurement?: string;
}
export interface center {
type: 'none' | 'card' | 'bars';
bars?: BarSettings[];
card?: LovelaceCardConfig;
}
export interface presetFeatures {
battery_percentage_entity?: string;
grid_sell_entity?: string;
grid_buy_entity?: string;
}
export interface BarSettings {
bar_color?: string;
bar_bg_color?: string;
entity?: string;
invert_value?: boolean;
lower_bound?: number;
name?: string | undefined;
preset?: 'autarky' | 'ratio' | '';
tap_action?: ActionConfig;
unit_of_measurement?: string;
upper_bound?: number;
double_tap_action?: ActionConfig;
}
export type ArrowStates = 'right' | 'left' | 'none';
export interface Target extends EventTarget {
checked?: boolean;
configValue?: string;
i?: number;
value?: string | EntitySettings[] | BarSettings[] | { bigger: string; equal: string; smaller: string };
}
export interface CustomValueEvent<T> extends Event {
target: Target;
// currentTarget?: {
// i?: number;
// value?: string;
// };
detail?: {
value?: T;
};
}
export interface EditorTarget extends EventTarget {
value?: string;
index?: number;
checked?: boolean;
configValue?: string;
type?: HTMLInputElement['type'];
config?: ActionConfig;
}
export interface HTMLElementValue extends HTMLElement {
value: string;
}
declare global {
interface Window {
loadCardHelpers: () => Promise<void>;
customCards: { type?: string; name?: string; description?: string; preview?: boolean }[];
ResizeObserver: { new (callback: ResizeObserverCallback): ResizeObserver; prototype: ResizeObserver };
}
interface Element {
offsetWidth: number;
}
}
================================================
FILE: src/utils/compute-color.ts
================================================
export const THEME_COLORS = new Set([
"primary",
"accent",
"red",
"pink",
"purple",
"deep-purple",
"indigo",
"blue",
"light-blue",
"cyan",
"teal",
"green",
"light-green",
"lime",
"yellow",
"amber",
"orange",
"deep-orange",
"brown",
"light-grey",
"grey",
"dark-grey",
"blue-grey",
"black",
"white",
]);
const YAML_ONLY_THEMES_COLORS = new Set([
"primary-text",
"secondary-text",
"disabled",
]);
export function computeCssVariableName(color: string): string {
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
return `--${color}-color`;
}
return color;
}
export function computeCssColor(color: string): string {
const cssVarName = computeCssVariableName(color);
if (cssVarName !== color) {
return `var(${cssVarName})`;
}
return color;
}
/**
* Validates if a string is a valid color.
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
*/
export function isValidColorString(color: string | undefined): boolean {
if (!color || typeof color !== "string") {
return false;
}
// Check if it's a theme color
if (THEME_COLORS.has(color)) {
return true;
}
// Check if it's a hex color
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(color)) {
return true;
}
// Check if it's a valid CSS color name by trying to parse it
// Use CSS.supports() for a more efficient test without DOM manipulation
// This checks if the browser recognizes the color value
try {
const style = new Option().style;
style.color = color;
return style.color !== "";
} catch {
return false;
}
}
================================================
FILE: src/utils/create-thing.ts
================================================
import { fireEvent } from "./hass-types/fire_event";
import type { LovelaceCardConfig } from "./hass-types/lovelace";
const TIMEOUT = 2000;
const _createErrorCardElement = (error: string, config: LovelaceCardConfig) => {
const el = document.createElement("hui-error-card") as any;
try {
el.setConfig({ type: "error", error, config });
} catch (_err) {
// ignore
}
return el;
};
const _createElement = (tag: string, config: LovelaceCardConfig) => {
const element = document.createElement(tag) as any;
try {
element.setConfig(config);
} catch (err) {
console.error(tag, err);
return _createErrorCardElement((err as Error).message, config);
}
return element;
};
export const createThing = (cardConfig: LovelaceCardConfig) => {
if (!cardConfig || typeof cardConfig !== "object" || !cardConfig.type) {
return _createErrorCardElement("No type defined", cardConfig);
}
const { type } = cardConfig;
if (type.startsWith("custom:")) {
const tag = type.slice("custom:".length);
if (customElements.get(tag)) {
return _createElement(tag, cardConfig);
}
const element = _createErrorCardElement(
`Custom element doesn't exist: ${tag}.`,
cardConfig
);
element.style.display = "None";
const timer = window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
fireEvent(element, "ll-rebuild");
});
return element;
}
const tag = `hui-${type}-card`;
if (customElements.get(tag)) {
return _createElement(tag, cardConfig);
}
const element = _createErrorCardElement(
`Unknown card type: ${type}.`,
cardConfig
);
element.style.display = "None";
const timer = window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
fireEvent(element, "ll-rebuild");
});
return element;
};
================================================
FILE: src/utils/custom-cards.ts
================================================
import { repository } from "../../package.json";
export function registerCustomCard(type: string, name: string, description: string): void {
const windowWithCards = window as unknown as Window & {
customCards: unknown[];
};
windowWithCards.customCards = windowWithCards.customCards || [];
windowWithCards.customCards.push({
type,
name,
description,
preview: true,
documentationURL: `${repository.url}/readme.md`,
});
}
================================================
FILE: src/utils/debounce.ts
================================================
// From: src/common/util/debounce.ts https://raw.githubusercontent.com/home-assistant/frontend/446661915bbfd74b119176076d6d5f6ae7e392fa/src/common/util/debounce.ts
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge. The trailing edge only fires if there were additional calls
// during the wait period.
export const debounce = <T extends any[]>(
func: (...args: T) => void,
wait: number,
immediate = false
) => {
let timeout: number | undefined;
let trailingArgs: T | undefined;
const debouncedFunc = (...args: T): void => {
const isLeading = immediate && !timeout;
if (timeout) {
trailingArgs = args;
}
clearTimeout(timeout);
timeout = window.setTimeout(() => {
timeout = undefined;
if (trailingArgs) {
func(...trailingArgs);
trailingArgs = undefined;
} else if (!immediate) {
func(...args);
}
}, wait);
if (isLeading) {
func(...args);
}
};
debouncedFunc.cancel = () => {
clearTimeout(timeout);
trailingArgs = undefined;
};
return debouncedFunc;
};
================================================
FILE: src/utils/get-lovelace.ts
================================================
export const getLovelace = () => {
const root = document
.querySelector("home-assistant")
?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot;
const resolver =
root?.querySelector("ha-drawer partial-panel-resolver") ||
root?.querySelector("app-drawer-layout partial-panel-resolver");
const huiRoot = (((resolver as any)?.shadowRoot || resolver) as any)
?.querySelector("ha-panel-lovelace")
?.shadowRoot?.querySelector("hui-root");
if (huiRoot) {
const ll = huiRoot.lovelace;
ll.current_view = huiRoot.___curView;
return ll;
}
return null;
};
================================================
FILE: src/utils/ha-component-loader.ts
================================================
export const loadHaComponents = () => {
if (!customElements.get("ha-entity-picker")) {
loadCustomElement("hui-entities-card").then((el: any) => el?.getConfigElement());
}
};
export const loadCustomElement = async <T = any>(name: string) => {
let Component = customElements.get(name) as T;
if (Component) {
return Component;
}
await customElements.whenDefined(name);
return customElements.get(name) as T;
};
================================================
FILE: src/utils/hass-types/action.ts
================================================
/**
* Types are from the original Homeassistant Repository:
* https://github.com/home-assistant/frontend/blob/dev/src/data/lovelace/config/action.ts
*/
import type { HassServiceTarget } from "home-assistant-js-websocket";
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
}
export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service" | "perform-action";
/** @deprecated "service" is kept for backwards compatibility. Replaced by "perform_action". */
service?: string;
perform_action: string;
target?: HassServiceTarget;
/** @deprecated "service_data" is kept for backwards compatibility. Replaced by "data". */
service_data?: Record<string, unknown>;
data?: Record<string, unknown>;
}
export interface NavigateActionConfig extends BaseActionConfig {
action: "navigate";
navigation_path: string;
navigation_replace?: boolean;
}
export interface UrlActionConfig extends BaseActionConfig {
action: "url";
url_path: string;
}
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity?: string;
}
export interface AssistActionConfig extends BaseActionConfig {
action: "assist";
pipeline_id?: string;
start_listening?: boolean;
}
export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
export interface CustomActionConfig extends BaseActionConfig {
action: "fire-dom-event";
}
export interface BaseActionConfig {
action: string;
confirmation?: ConfirmationRestrictionConfig;
}
export interface ConfirmationRestrictionConfig {
text?: string;
exemptions?: RestrictionConfig[];
}
export interface RestrictionConfig {
user: string;
}
export type ActionConfig =
| ToggleActionConfig
| CallServiceActionConfig
| NavigateActionConfig
| UrlActionConfig
| MoreInfoActionConfig
| AssistActionConfig
| NoActionConfig
| CustomActionConfig;
export interface ActionConfigParams {
entity?: string;
camera_image?: string;
image_entity?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export type IntegrationType =
| "device"
| "helper"
| "hub"
| "service"
| "hardware"
| "entity"
| "system";
================================================
FILE: src/utils/hass-types/action_handler.ts
================================================
/**
* Types are from the original Homeassistant Repository:
* https://github.com/home-assistant/frontend/blob/dev/src/data/lovelace/action_handler.ts
*/
import { HASSDomEvent } from "./event";
export interface ActionHandlerOptions {
hasTap?: boolean;
hasHold?: boolean;
hasDoubleClick?: boolean;
disabled?: boolean;
}
export interface ActionHandlerDetail {
action: "hold" | "tap" | "double_tap";
}
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
================================================
FILE: src/utils/hass-types/event.ts
================================================
/**
* Types are from the original Homeassistant Repository:
* https://github.com/home-assistant/frontend/blob/dev/src/common/dom/fire_event.ts
*/
declare global {
interface HASSDomEvents {}
}
export type ValidHassDomEvent = keyof HASSDomEvents;
export interface HASSDomEvent<T> extends Event {
detail: T;
}
================================================
FILE: src/utils/hass-types/fire_event.ts
================================================
// Polymer legacy event helpers used courtesy of the Polymer project.
//
// Copyright (c) 2017 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
declare global {
interface HASSDomEvents {}
}
export type ValidHassDomEvent = keyof HASSDomEvents;
export interface HASSDomEvent<T> extends Event {
detail: T;
}
/**
* Dispatches a custom event with an optional detail value.
*
* @param {string} type Name of event type.
* @param {*=} detail Detail value containing event-specific
* payload.
* @param {{ bubbles: (boolean|undefined),
* cancelable: (boolean|undefined),
* composed: (boolean|undefined) }=}
* options Object specifying options. These may include:
* `bubbles` (boolean, defaults to `true`),
* `cancelable` (boolean, defaults to false), and
* `node` on which to fire the event (HTMLElement, defaults to `this`).
* @return {Event} The new event that was fired.
*/
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
node: HTMLElement | Window,
type: HassEvent,
detail?: HASSDomEvents[HassEvent],
options?: {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
}
) => {
options = options || {};
// @ts-ignore
detail = detail === null || detail === undefined ? {} : detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
});
(event as any).detail = detail;
node.dispatchEvent(event);
return event;
};
================================================
FILE: src/utils/hass-types/format-number.ts
================================================
import { shouldPolyfill } from "@formatjs/intl-numberformat/should-polyfill.js";
import { FrontendLocaleData, NumberFormat } from "./homeassistant";
export async function applyPolyfills(): Promise<void> {
if (shouldPolyfill()) {
await import("@formatjs/intl-numberformat/polyfill-force.js");
}
}
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData
): string | string[] | undefined => {
switch (localeOptions.number_format) {
case NumberFormat.comma_decimal:
return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
case NumberFormat.decimal_comma:
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.quote_decimal:
return ["de-CH"]; // Use German (Switzerland) formatting 1'234'567.89
case NumberFormat.system:
return undefined;
default:
return localeOptions.language;
}
};
/**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
*
* @param num The number to format
* @param localeOptions The user-selected language and formatting, from `hass.locale`
* @param options Intl.NumberFormatOptions to use
*/
export const formatNumber = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): string =>
formatNumberToParts(num, localeOptions, options)
.map((part) => part.value)
.join("");
/**
* Returns an array of objects containing the formatted number in parts
* Similar to Intl.NumberFormat.prototype.formatToParts()
*
* Input params - same as for formatNumber()
*/
export const formatNumberToParts = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): any[] => {
const locale = localeOptions
? numberFormatToLocale(localeOptions)
: undefined;
if (
localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num))
) {
return new Intl.NumberFormat(
locale,
getDefaultFormatOptions(num, options)
).formatToParts(Number(num));
}
if (
!Number.isNaN(Number(num)) &&
num !== "" &&
localeOptions?.number_format === NumberFormat.none
) {
// If NumberFormat is none, use en-US format without grouping.
return new Intl.NumberFormat(
"en-US",
getDefaultFormatOptions(num, {
...options,
useGrouping: false,
})
).formatToParts(Number(num));
}
return [{ type: "literal", value: num }];
};
/**
* Generates default options for Intl.NumberFormat
* @param num The number to be formatted
* @param options The Intl.NumberFormatOptions that should be included in the returned options
*/
export const getDefaultFormatOptions = (
num: string | number,
options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => {
const defaultOptions: Intl.NumberFormatOptions = {
maximumFractionDigits: 2,
...options,
};
if (typeof num !== "string") {
return defaultOptions;
}
// Keep decimal trailing zeros if they are present in a string numeric value
if (
!options ||
(options.minimumFractionDigits === undefined &&
options.maximumFractionDigits === undefined)
) {
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
defaultOptions.minimumFractionDigits = digits;
defaultOptions.maximumFractionDigits = digits;
}
return defaultOptions;
};
================================================
FILE: src/utils/hass-types/get_main_window.ts
================================================
// From: https://github.com/home-assistant/frontend/blob/dev/src/data/main_window.ts
export const MAIN_WINDOW_NAME = "ha-main-window";
// From: https://github.com/home-assistant/frontend/blob/dev/src/common/dom/get_main_window.ts
export const mainWindow = (() => {
try {
return window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top!;
} catch {
return window;
}
})();
================================================
FILE: src/utils/hass-types/handle-action.ts
================================================
import { ActionConfig } from "./action";
import { fireEvent } from "./fire_event";
import { forwardHaptic } from "./haptics";
import { HomeAssistant } from "./homeassistant";
import { domainToName } from "./integration";
import { navigate } from "./navigate";
import { showConfirmationDialog } from "./show-dialog-box";
import { showVoiceCommandDialog } from "./show-ha-voice-command-dialog";
import { showToast } from "./toast";
import { toggleEntity } from "./toggle-entity";
export interface ActionConfigParams {
entity?: string;
camera_image?: string;
image_entity?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export const handleAction = async (
node: HTMLElement,
hass: HomeAssistant,
config: ActionConfigParams,
action: string
): Promise<void> => {
let actionConfig: ActionConfig | undefined;
if (action === "double_tap" && config.double_tap_action) {
actionConfig = config.double_tap_action;
} else if (action === "hold" && config.hold_action) {
actionConfig = config.hold_action;
} else if (action === "tap" && config.tap_action) {
actionConfig = config.tap_action;
}
if (!actionConfig) {
actionConfig = {
action: "more-info",
};
}
if (
actionConfig.confirmation &&
(!actionConfig.confirmation.exemptions ||
!actionConfig.confirmation.exemptions.some(
(e) => e.user === hass!.user?.id
))
) {
forwardHaptic(node, "warning");
let serviceName;
if (
actionConfig.action === "call-service" ||
actionConfig.action === "perform-action"
) {
const [domain, service] = (actionConfig.perform_action ||
actionConfig.service)!.split(".", 2);
const serviceDomains = hass.services;
if (domain in serviceDomains && service in serviceDomains[domain]) {
await hass.loadBackendTranslation("title");
const localize = await hass.loadBackendTranslation("services");
serviceName = `${domainToName(localize, domain)}: ${
localize(`component.${domain}.services.${serviceName}.name`) ||
serviceDomains[domain][service].name ||
service
}`;
}
}
if (
!(await showConfirmationDialog(node, {
text:
actionConfig.confirmation.text ||
hass.localize("ui.panel.lovelace.cards.actions.action_confirmation", {
action:
serviceName ||
hass.localize(
`ui.panel.lovelace.editor.action-editor.actions.${actionConfig.action}`
) ||
actionConfig.action,
}),
}))
) {
return;
}
}
switch (actionConfig.action) {
case "more-info": {
const entityId =
actionConfig.entity ||
config.entity ||
config.camera_image ||
config.image_entity;
if (entityId) {
fireEvent(node, "hass-more-info", { entityId });
} else {
showToast(node, {
message: hass.localize(
"ui.panel.lovelace.cards.actions.no_entity_more_info"
),
});
forwardHaptic(node, "failure");
}
break;
}
case "navigate":
if (actionConfig.navigation_path) {
navigate(actionConfig.navigation_path, {
replace: actionConfig.navigation_replace,
});
} else {
showToast(node, {
message: hass.localize(
"ui.panel.lovelace.cards.actions.no_navigation_path"
),
});
forwardHaptic(node, "failure");
}
break;
case "url": {
if (actionConfig.url_path) {
window.open(actionConfig.url_path);
} else {
showToast(node, {
message: hass.localize("ui.panel.lovelace.cards.actions.no_url"),
});
forwardHaptic(node, "failure");
}
break;
}
case "toggle": {
if (config.entity) {
toggleEntity(hass, config.entity!);
forwardHaptic(node, "light");
} else {
showToast(node, {
message: hass.localize(
"ui.panel.lovelace.cards.actions.no_entity_toggle"
),
});
forwardHaptic(node, "failure");
}
break;
}
case "perform-action":
case "call-service": {
if (!actionConfig.perform_action && !actionConfig.service) {
showToast(node, {
message: hass.localize("ui.panel.lovelace.cards.actions.no_action"),
});
forwardHaptic(node, "failure");
return;
}
const [domain, service] = (actionConfig.perform_action ||
actionConfig.service)!.split(".", 2);
hass.callService(
domain,
service,
actionConfig.data ?? actionConfig.service_data,
actionConfig.target
);
forwardHaptic(node, "light");
break;
}
case "assist": {
showVoiceCommandDialog(node, hass, {
start_listening: actionConfig.start_listening ?? false,
pipeline_id: actionConfig.pipeline_id ?? "last_used",
});
break;
}
case "fire-dom-event": {
fireEvent(node, "ll-custom", actionConfig);
}
}};
================================================
FILE: src/utils/hass-types/haptics.ts
================================================
/**
* Broadcast haptic feedback requests
*/
import { HASSDomEvent } from "./event";
import { fireEvent } from "./fire_event";
// Allowed types are from iOS HIG.
// https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics
// Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible.
export type HapticType =
| "success"
| "warning"
| "failure"
| "light"
| "medium"
| "heavy"
| "selection";
declare global {
// for fire event
interface HASSDomEvents {
haptic: HapticType;
}
interface GlobalEventHandlersEventMap {
haptic: HASSDomEvent<HapticType>;
}
}
export const forwardHaptic = (node: HTMLElement, hapticType: HapticType) => {
fireEvent(node, "haptic", hapticType);
};
================================================
FILE: src/utils/hass-types/has-action.ts
================================================
// From: https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/common/has-action.ts
import { ActionConfig } from "./action";
export function hasAction(config?: ActionConfig): boolean {
return config !== undefined && config.action !== "none";
}
================================================
FILE: src/utils/hass-types/homeassistant.ts
================================================
/**
* This contains the typings for the hass Homeassistant object from various sources.
* The main file is:
* https://github.com/home-assistant/frontend/blob/dev/src/types.ts
*/
import type {
Auth,
Connection,
HassConfig,
HassEntities,
HassEntity,
HassServices,
HassServiceTarget,
MessageBase,
} from "home-assistant-js-websocket";
import { HTMLTemplateResult } from "lit";
type EntityCategory = "config" | "diagnostic";
export interface EntityRegistryDisplayEntry {
entity_id: string;
name?: string;
icon?: string;
device_id?: string;
area_id?: string;
labels: string[];
hidden?: boolean;
entity_category?: EntityCategory;
translation_key?: string;
platform?: string;
display_precision?: number;
has_entity_name?: boolean;
}
export interface RegistryEntry {
created_at: number;
modified_at: number;
}
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;
model: string | null;
model_id: string | null;
name: string | null;
labels: string[];
sw_version: string | null;
hw_version: string | null;
serial_number: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;
entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null;
primary_config_entry: string | null;
}
export interface AreaRegistryEntry extends RegistryEntry {
aliases: string[];
area_id: string;
floor_id: string | null;
humidity_entity_id: string | null;
icon: string | null;
labels: string[];
name: string;
picture: string | null;
temperature_entity_id: string | null;
}
export interface FloorRegistryEntry extends RegistryEntry {
floor_id: string;
name: string;
level: number | null;
icon: string | null;
aliases: string[];
}
export interface ThemeVars {
// Incomplete
"primary-color": string;
"text-primary-color": string;
"accent-color": string;
[key: string]: string;
}
export type Theme = ThemeVars & {
modes?: {
light?: ThemeVars;
dark?: ThemeVars;
};
};
export interface Themes {
default_theme: string;
default_dark_theme: string | null;
themes: Record<string, Theme>;
// Currently effective dark mode. Will never be undefined. If user selected "auto"
// in theme picker, this property will still contain either true or false based on
// what has been determined via system preferences and support from the selected theme.
darkMode: boolean;
// Currently globally active theme name
theme: string;
}
export enum NumberFormat {
language = "language",
system = "system",
comma_decimal = "comma_decimal",
decimal_comma = "decimal_comma",
quote_decimal = "quote_decimal",
space_comma = "space_comma",
none = "none",
}
export enum TimeFormat {
language = "language",
system = "system",
am_pm = "12",
twenty_four = "24",
}
export enum TimeZone {
local = "local",
server = "server",
}
export enum DateFormat {
language = "language",
system = "system",
DMY = "DMY",
MDY = "MDY",
YMD = "YMD",
}
export enum FirstWeekday {
language = "language",
monday = "monday",
tuesday = "tuesday",
wednesday = "wednesday",
thursday = "thursday",
friday = "friday",
saturday = "saturday",
sunday = "sunday",
}
export interface FrontendLocaleData {
language: string;
number_format: NumberFormat;
time_format: TimeFormat;
date_format: DateFormat;
first_weekday: FirstWeekday;
time_zone: TimeZone;
}
export type LocalizeFunc = (
key: string,
values?: Record<
string,
string | number | HTMLTemplateResult | null | undefined
>
) => string;
export interface ValueChangedEvent<T> extends CustomEvent {
detail: {
value: T;
};
}
export type Constructor<T = any> = new (...args: any[]) => T;
export interface ClassElement {
kind: "field" | "method";
key: PropertyKey;
placement: "static" | "prototype" | "own";
initializer?: (...args) => unknown;
extras?: ClassElement[];
finisher?: <T>(cls: Constructor<T>) => undefined | Constructor<T>;
descriptor?: PropertyDescriptor;
}
export interface Credential {
auth_provider_type: string;
auth_provider_id: string;
}
export interface MFAModule {
id: string;
name: string;
enabled: boolean;
}
export interface CurrentUser {
id: string;
is_owner: boolean;
is_admin: boolean;
name: string;
credentials: Credential[];
mfa_modules: MFAModule[];
}
// Currently selected theme and its settings. These are the values stored in local storage.
// Note: These values are not meant to be used at runtime to check whether dark mode is active
// or which theme name to use, as this interface represents the config data for the theme picker.
// The actually active dark mode and theme name can be read from hass.themes.
export interface ThemeSettings {
theme: string;
// Radio box selection for theme picker. Do not use in Lovelace rendering as
// it can be undefined == auto.
// Property hass.themes.darkMode carries effective current mode.
dark?: boolean;
primaryColor?: string;
accentColor?: string;
}
export interface PanelInfo<T = Record<string, any> | null> {
component_name: string;
config: T;
icon: string | null;
title: string | null;
url_path: string;
config_panel_domain?: string;
}
export type Panels = Record<string, PanelInfo>;
export interface CalendarViewChanged {
end: Date;
start: Date;
view: string;
}
export type FullCalendarView =
| "dayGridMonth"
| "dayGridWeek"
| "dayGridDay"
| "listWeek";
export type ThemeMode = "auto" | "light" | "dark";
export interface ToggleButton {
label: string;
iconPath?: string;
value: string;
}
export interface Translation {
nativeName: string;
isRTL: boolean;
hash: string;
}
export interface TranslationMetadata {
fragments: string[];
translations: Record<string, Translation>;
}
export interface IconMetaFile {
version: string;
parts: IconMeta[];
}
export interface IconMeta {
start: string;
file: string;
}
export interface Notification {
notification_id: string;
message: string;
title: string;
status: "read" | "unread";
created_at: string;
}
export type Resources = Record<string, Record<string, string>>;
export interface Context {
id: string;
parent_id?: string;
user_id?: string | null;
}
export interface ServiceCallResponse<T = any> {
context: Context;
response?: T;
}
export interface ServiceCallRequest {
domain: string;
service: string;
serviceData?: Record<string, any>;
target?: HassServiceTarget;
}
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
}
export type TranslationCategory =
| "title"
| "state"
| "entity"
| "entity_component"
| "exceptions"
| "config"
| "config_subentries"
| "config_panel"
| "options"
| "device_automation"
| "mfa_setup"
| "system_health"
| "application_credentials"
| "issues"
| "selector"
| "services";
export const getHassTranslations = async (
hass: HomeAssistant,
language: string,
category: TranslationCategory,
integration?: string | string[],
config_flow?: boolean
): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
type: "frontend/get_translations",
language,
category,
integration,
config_flow,
});
return result.resources;
};
export interface HomeAssistant {
auth: Auth & { external?: any };
connection: Connection;
connected: boolean;
states: HassEntities;
entities: Record<string, EntityRegistryDisplayEntry>;
devices: Record<string, DeviceRegistryEntry>;
areas: Record<string, AreaRegistryEntry>;
floors: Record<string, FloorRegistryEntry>;
services: HassServices;
config: HassConfig;
themes: Themes;
selectedTheme: ThemeSettings | null;
panels: Panels;
panelUrl: string;
// i18n
// current effective language in that order:
// - backend saved user selected language
// - language in local app storage
// - browser language
// - english (en)
language: string;
// local stored language, keep that name for backward compatibility
selectedLanguage: string | null;
locale: FrontendLocaleData;
resources: Resources;
localize: LocalizeFunc;
translationMetadata: TranslationMetadata;
suspendWhenHidden: boolean;
enableShortcuts: boolean;
vibrate: boolean;
debugConnection: boolean;
dockedSidebar: "docked" | "always_hidden" | "auto";
defaultPanel: string;
moreInfoEntityId: string | null;
user?: CurrentUser;
userData?: CoreFrontendUserData | null;
hassUrl(path?): string;
callService<T = any>(
domain: ServiceCallRequest["domain"],
service: ServiceCallRequest["service"],
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"],
notifyOnError?: boolean,
returnResponse?: boolean
): Promise<ServiceCallResponse<T>>;
callApi<T>(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
parameters?: Record<string, any>,
headers?: Record<string, string>
): Promise<T>;
callApiRaw( // introduced in 2024.11
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
parameters?: Record<string, any>,
headers?: Record<string, string>,
signal?: AbortSignal
): Promise<Response>;
fetchWithAuth(path: string, init?: Record<string, any>): Promise<Response>;
sendWS(msg: MessageBase): void;
callWS<T>(msg: MessageBase): Promise<T>;
loadBackendTranslation(
category: Parameters<typeof getHassTranslations>[2],
integrations?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4]
): Promise<LocalizeFunc>;
loadFragmentTranslation(fragment: string): Promise<LocalizeFunc | undefined>;
formatEntityState(stateObj: HassEntity, state?: string): string;
formatEntityAttributeValue(
stateObj: HassEntity,
attribute: string,
value?: any
): string;
formatEntityAttributeName(stateObj: HassEntity, attribute: string): string;
}
================================================
FILE: src/utils/hass-types/integration.ts
================================================
// From: https://github.com/home-assistant/frontend/blob/dev/src/data/integration.ts
import { LocalizeFunc } from "./homeassistant";
export type IntegrationType =
| "device"
| "helper"
| "hub"
| "service"
| "hardware"
| "entity"
| "system";
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;
domain: string;
name: string;
config_flow: boolean;
documentation: string;
issue_tracker?: string;
dependencies?: string[];
after_dependencies?: string[];
codeowners?: string[];
requirements?: string[];
ssdp?: { manufacturer?: string; modelName?: string; st?: string }[];
zeroconf?: string[];
homekit?: { models: string[] };
integration_type?: IntegrationType;
loggers?: string[];
quality_scale?:
| "bronze"
| "silver"
| "gold"
| "platinum"
| "no_score"
| "internal"
| "legacy"
| "custom";
iot_class:
| "assumed_state"
| "cloud_polling"
| "cloud_push"
| "local_polling"
| "local_push";
single_config_entry?: boolean;
version?: string;
}
export const domainToName = (
localize: LocalizeFunc,
domain: string,
manifest?: IntegrationManifest
) => localize(`component.${domain}.title`) || manifest?.name || domain;
================================================
FILE: src/utils/hass-types/localize.ts
================================================
export type LocalizeKeys =
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
| `panel.${string}`
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}`
| `ui.common.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.selectors.file.${string}`
| `ui.components.logbook.messages.detected_device_classes.${string}`
| `ui.components.logbook.messages.cleared_device_classes.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
| `ui.dialogs.quick-bar.commands.${string}`
| `ui.dialogs.unhealthy.reasons.${string}`
| `ui.dialogs.unsupported.reasons.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.storage.segments.${string}`
| `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}`
| `ui.panel.lovelace.editor.${string}`
| `ui.panel.page-authorize.form.${string}`
| `component.${string}`;
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
export type FlattenObjectKeys<
T extends Record<string, any>,
Key extends keyof T = keyof T,
> = Key extends string
? T[Key] extends Record<string, unknown>
? `${Key}.${FlattenObjectKeys<T[Key]>}`
: `${Key}`
: never;
export type TranslationDict = typeof import('../../localize/languages/en.json');
================================================
FILE: src/utils/hass-types/lovelace.ts
================================================
import { HomeAssistant } from "./homeassistant";
export type Condition =
| LocationCondition
| NumericStateCondition
| StateCondition
| ScreenCondition
| UserCondition
| OrCondition
| AndCondition
| NotCondition;
// Legacy conditional card condition
export interface LegacyCondition {
entity?: string;
state?: string | string[];
state_not?: string | string[];
}
interface BaseCondition {
condition: string;
}
export interface LocationCondition extends BaseCondition {
condition: "location";
locations?: string[];
}
export interface NumericStateCondition extends BaseCondition {
condition: "numeric_state";
entity?: string;
below?: string | number;
above?: string | number;
}
export interface StateCondition extends BaseCondition {
condition: "state";
entity?: string;
state?: string | string[];
state_not?: string | string[];
}
export interface ScreenCondition extends BaseCondition {
condition: "screen";
media_query?: string;
}
export interface UserCondition extends BaseCondition {
condition: "user";
users?: string[];
}
export interface OrCondition extends BaseCondition {
condition: "or";
conditions?: Condition[];
}
export interface AndCondition extends BaseCondition {
condition: "and";
conditions?: Condition[];
}
export interface NotCondition extends BaseCondition {
condition: "not";
conditions?: Condition[];
}
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
view_layout?: any;
/** @deprecated Use `grid_options` instead */
layout_options?: LovelaceLayoutOptions;
grid_options?: LovelaceGridOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
}
export interface LovelaceLayoutOptions {
grid_columns?: number | "full";
grid_rows?: number | "auto";
grid_max_columns?: number;
grid_min_columns?: number;
grid_min_rows?: number;
grid_max_rows?: number;
}
export interface LovelaceGridOptions {
columns?: number | "full";
rows?: number | "auto";
max_columns?: number;
min_columns?: number;
min_rows?: number;
max_rows?: number;
fixed_rows?: boolean;
fixed_columns?: boolean;
}
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
preview?: boolean;
layout?: string;
connectedWhileHidden?: boolean;
getCardSize(): number | Promise<number>;
/** @deprecated Use `getGridOptions` instead */
getLayoutOptions?(): LovelaceLayoutOptions;
getGridOptions?(): LovelaceGridOptions;
setConfig(config: LovelaceCardConfig): void;
}
================================================
FILE: src/utils/hass-types/navigate.ts
================================================
// Partially from https://github.com/home-assistant/frontend/blob/dev/src/common/navigate.ts
import { fireEvent } from "./fire_event";
import { mainWindow } from "./get_main_window";
export interface NavigateOptions {
replace?: boolean;
data?: any;
}
export const navigate = (
path: string,
options?: NavigateOptions,
) => {
const replace = options?.replace || false;
if (replace) {
history.replaceState(
history.state?.root ? { root: true } : (options?.data ?? null),
"",
`${mainWindow.location.pathname}#${path}`
);
} else {
history.pushState(null, "", path);
}
fireEvent(window, "location-changed", {
replace
});
};
================================================
FILE: src/utils/hass-types/show-dialog-box.ts
================================================
// Derived From: https://github.com/home-assistant/frontend/blob/dev/src/dialogs/generic/show-dialog-box.ts
import { TemplateResult } from "lit";
import { fireEvent } from "./fire_event";
interface BaseDialogBoxParams {
confirmText?: string;
text?: string | TemplateResult;
title?: string;
warning?: boolean;
}
export interface AlertDialogParams extends BaseDialogBoxParams {
confirm?: () => void;
}
export interface ConfirmationDialogParams extends BaseDialogBoxParams {
dismissText?: string;
confirm?: () => void;
cancel?: () => void;
destructive?: boolean;
}
export interface PromptDialogParams extends BaseDialogBoxParams {
inputLabel?: string;
dismissText?: string;
inputType?: string;
defaultValue?: string;
placeholder?: string;
confirm?: (out?: string) => void;
cancel?: () => void;
inputMin?: number | string;
inputMax?: number | string;
}
export interface DialogBoxParams
extends ConfirmationDialogParams,
PromptDialogParams {
confirm?: (out?: string) => void;
confirmation?: boolean;
prompt?: boolean;
}
const showDialogHelper = (
element: HTMLElement,
dialogParams: DialogBoxParams,
extra?: {
confirmation?: DialogBoxParams["confirmation"];
prompt?: DialogBoxParams["prompt"];
}
) =>
new Promise((resolve) => {
const origCancel = dialogParams.cancel;
const origConfirm = dialogParams.confirm;
fireEvent(element, "show-dialog", {
dialogTag: "dialog-box",
dialogParams: {
...dialogParams,
...extra,
cancel: () => {
resolve(extra?.prompt ? null : false);
if (origCancel) {
origCancel();
}
},
confirm: (out) => {
resolve(extra?.prompt ? out : true);
if (origConfirm) {
origConfirm(out);
}
},
},
});
});
export const showConfirmationDialog = (
element: HTMLElement,
dialogParams: ConfirmationDialogParams
) =>
showDialogHelper(element, dialogParams, {
confirmation: true,
}) as Promise<boolean>;
================================================
FILE: src/utils/hass-types/show-ha-voice-command-dialog.ts
================================================
import { fireEvent } from "./fire_event";
import type { HomeAssistant } from "./homeassistant";
export interface VoiceCommandDialogParams {
pipeline_id: "last_used" | "preferred" | string;
start_listening?: boolean;
}
export const showVoiceCommandDialog = (
element: HTMLElement,
hass: HomeAssistant,
dialogParams: VoiceCommandDialogParams
): void => {
if (hass.auth.external?.config.hasAssist) {
hass.auth.external!.fireMessage({
type: "assist/show",
payload: {
pipeline_id: dialogParams.pipeline_id,
// Start listening by default for app
start_listening: dialogParams.start_listening ?? true,
},
});
return;
}
fireEvent(element, "show-dialog", {
dialogTag: "ha-voice-command-dialog",
dialogParams: {
pipeline_id: dialogParams.pipeline_id,
// Don't start listening by default for web
start_listening: dialogParams.start_listening ?? false,
},
});
};
================================================
FILE: src/utils/hass-types/toast.ts
================================================
import { fireEvent } from "./fire_event";
declare global {
// for fire event
interface HASSDomEvents {
"hass-notification": ShowToastParams;
}
}
export interface ShowToastParams {
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
id?: string;
message:
| string
| { translationKey: string; args?: Record<string, string> };
action?: ToastActionParams;
duration?: number;
dismissable?: boolean;
}
export interface ToastActionParams {
action: () => void;
text:
| string
| { translationKey: string; args?: Record<string, string> };
}
export const showToast = (el: HTMLElement, params: ShowToastParams) =>
fireEvent(el, "hass-notification", params);
================================================
FILE: src/utils/hass-types/toggle-entity.ts
================================================
import { HomeAssistant, ServiceCallResponse } from "./homeassistant";
// From: https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/common/entity/toggle-entity.ts
/** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "off"];
export const toggleEntity = (
hass: HomeAssistant,
entityId: string
): Promise<ServiceCallResponse> => {
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
return turnOnOffEntity(hass, entityId, turnOn);
};
// From: https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/common/entity/turn-on-off-entity.ts
export const turnOnOffEntity = (
hass: HomeAssistant,
entityId: string,
turnOn = true
): Promise<ServiceCallResponse> => {
const stateDomain = computeDomain(entityId);
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
let service;
switch (stateDomain) {
case "lock":
service = turnOn ? "unlock" : "lock";
break;
case "cover":
service = turnOn ? "open_cover" : "close_cover";
break;
case "button":
case "input_button":
service = "press";
break;
case "scene":
service = "turn_on";
break;
case "valve":
service = turnOn ? "open_valve" : "close_valve";
break;
default:
service = turnOn ? "turn_on" : "turn_off";
}
return hass.callService(serviceDomain, service, { entity_id: entityId });
};
// From: https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_domain.ts
export const computeDomain = (entityId: string): string =>
entityId.substring(0, entityId.indexOf("."));
================================================
FILE: src/utils/index.ts
================================================
export * from './custom-cards';
export { createThing } from './create-thing';
export { getLovelace } from './get-lovelace';
export * from './hass-types/handle-action';
export * from './hass-types/has-action';
export * from './hass-types/action_handler';
export { ActionConfig } from './hass-types/action';
export * from './hass-types/event';
export * from './hass-types/homeassistant';
export * from './hass-types/lovelace';
export { fireEvent } from './hass-types/fire_event';
export { formatNumber, applyPolyfills } from './hass-types/format-number';
export function fireCustomEvent<T>(node: HTMLElement | Window, type: string, detail: T): void {
const event = new CustomEvent(type, { bubbles: false, composed: false, detail: detail });
node.dispatchEvent(event);
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2017",
"module": "esnext",
"moduleResolution": "Bundler",
"lib": ["ES2021", "dom", "dom.iterable"],
"noEmit": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
"noImplicitAny": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"experimentalDecorators": true
}
}
gitextract_bav71kz2/ ├── .github/ │ └── workflows/ │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── .prettierrc.js ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── eslint.config.mjs ├── hacs.json ├── package.json ├── rollup.config.mjs ├── src/ │ ├── action-handler.ts │ ├── card-tags.ts │ ├── deep-equal.ts │ ├── editor/ │ │ ├── bar-editor.ts │ │ ├── editor.ts │ │ ├── ha-form.ts │ │ ├── item-editor.ts │ │ └── items-editor.ts │ ├── localize/ │ │ ├── languages/ │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ └── sk.json │ │ └── localize.ts │ ├── power-distribution-card.ts │ ├── presets.ts │ ├── styles.ts │ ├── types.ts │ └── utils/ │ ├── compute-color.ts │ ├── create-thing.ts │ ├── custom-cards.ts │ ├── debounce.ts │ ├── get-lovelace.ts │ ├── ha-component-loader.ts │ ├── hass-types/ │ │ ├── action.ts │ │ ├── action_handler.ts │ │ ├── event.ts │ │ ├── fire_event.ts │ │ ├── format-number.ts │ │ ├── get_main_window.ts │ │ ├── handle-action.ts │ │ ├── haptics.ts │ │ ├── has-action.ts │ │ ├── homeassistant.ts │ │ ├── integration.ts │ │ ├── localize.ts │ │ ├── lovelace.ts │ │ ├── navigate.ts │ │ ├── show-dialog-box.ts │ │ ├── show-ha-voice-command-dialog.ts │ │ ├── toast.ts │ │ └── toggle-entity.ts │ └── index.ts └── tsconfig.json
SYMBOL INDEX (276 symbols across 33 files)
FILE: src/action-handler.ts
type ActionHandlerMock (line 10) | interface ActionHandlerMock extends HTMLElement {
type ActionHandlerElement (line 14) | interface ActionHandlerElement extends HTMLElement {
type ActionHandlerOptions (line 23) | interface ActionHandlerOptions {
class ActionHandler (line 29) | class ActionHandler extends HTMLElement implements ActionHandlerMock {
method bind (line 34) | public bind(element: ActionHandlerElement, options: ActionHandlerOptio...
method update (line 100) | update(part: AttributePart, [options]: DirectiveParameters<this>) {
method render (line 105) | render(_options?: ActionHandlerOptions) {}
FILE: src/card-tags.ts
constant CARD_TAG (line 5) | const CARD_TAG = `power-distribution-card${__DEV_SUFFIX__}`;
constant EDITOR_TAG (line 6) | const EDITOR_TAG = `${CARD_TAG}-editor`;
constant ITEM_EDITOR_TAG (line 7) | const ITEM_EDITOR_TAG = `${CARD_TAG}-item-editor`;
constant BAR_EDITOR_TAG (line 8) | const BAR_EDITOR_TAG = `${CARD_TAG}-bar-editor`;
constant ITEMS_EDITOR_TAG (line 9) | const ITEMS_EDITOR_TAG = `${CARD_TAG}-items-editor`;
constant ACTION_HANDLER_TAG (line 10) | const ACTION_HANDLER_TAG = `action-handler${__DEV_SUFFIX__}-power-distri...
FILE: src/editor/bar-editor.ts
constant BAR_PRESETS (line 14) | const BAR_PRESETS = ['autarky', 'ratio', ''];
constant SCHEMA (line 16) | const SCHEMA: HaFormSchema[] = [
class ItemEditor (line 54) | class ItemEditor extends LitElement {
method render (line 71) | protected render() {
method valueChanged (line 144) | protected valueChanged(ev: CustomEvent<{ value: BarSettings }>) {
method _addBar (line 161) | protected _addBar() {
method _selectBar (line 171) | protected _selectBar(ev: CustomEvent<{ name: string }>) {
method _moveRight (line 175) | protected _moveRight() {
method _moveLeft (line 187) | protected _moveLeft() {
method _delete (line 199) | protected _delete() {
method styles (line 214) | static get styles(): CSSResultGroup {
FILE: src/editor/editor.ts
type EditorType (line 31) | type EditorType = 'main' | 'item' | 'bars' | 'card';
type Editor (line 33) | type Editor = {
constant SCHEMA (line 38) | const SCHEMA: HaFormSchema[] = [
class PowerDistributionCardEditor (line 43) | class PowerDistributionCardEditor extends LitElement {
method setConfig (line 49) | public setConfig(config: PDCConfig) {
method firstUpdated (line 69) | protected firstUpdated() {
method render (line 73) | protected render() {
method _enableCenterEditor (line 105) | protected _enableCenterEditor(ev: any): void {
method _enableItemEditor (line 111) | protected _enableItemEditor(ev: any): void {
method _goBack (line 120) | protected _goBack(): void {
method _valueChanged (line 124) | protected _valueChanged(ev: CustomValueEvent<unknown>) {
method _renderMainEditor (line 161) | protected _renderMainEditor(): TemplateResult {
method _renderItemEditor (line 203) | protected _renderItemEditor() {
method _renderBarEditor (line 221) | protected _renderBarEditor() {
method _itemChanged (line 230) | private _itemChanged(ev: CustomEvent<EntitySettings>) {
method _renderCardEditor (line 243) | private _renderCardEditor(): TemplateResult {
method _cardChanged (line 256) | private _cardChanged(ev: CustomEvent): void {
method styles (line 270) | static get styles(): CSSResultGroup[] {
FILE: src/editor/ha-form.ts
type HaDurationData (line 3) | interface HaDurationData {
type HaFormSchema (line 10) | type HaFormSchema =
type HaFormBaseSchema (line 24) | interface HaFormBaseSchema {
type HaFormGridSchema (line 38) | interface HaFormGridSchema extends HaFormBaseSchema {
type HaFormExpandableSchema (line 45) | interface HaFormExpandableSchema extends HaFormBaseSchema {
type HaFormOptionalActionsSchema (line 56) | interface HaFormOptionalActionsSchema extends HaFormBaseSchema {
type HaFormSelector (line 62) | interface HaFormSelector extends HaFormBaseSchema {
type HaFormConstantSchema (line 67) | interface HaFormConstantSchema extends HaFormBaseSchema {
type HaFormIntegerSchema (line 72) | interface HaFormIntegerSchema extends HaFormBaseSchema {
type HaFormSelectSchema (line 79) | interface HaFormSelectSchema extends HaFormBaseSchema {
type HaFormMultiSelectSchema (line 84) | interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type HaFormFloatSchema (line 92) | interface HaFormFloatSchema extends HaFormBaseSchema {
type HaFormStringSchema (line 96) | interface HaFormStringSchema extends HaFormBaseSchema {
type HaFormBooleanSchema (line 103) | interface HaFormBooleanSchema extends HaFormBaseSchema {
type HaFormTimeSchema (line 107) | interface HaFormTimeSchema extends HaFormBaseSchema {
type SchemaUnion (line 112) | type SchemaUnion<
type HaFormDataContainer (line 119) | type HaFormDataContainer = Record<string, HaFormData>;
type HaFormData (line 121) | type HaFormData =
type HaFormStringData (line 130) | type HaFormStringData = string;
type HaFormIntegerData (line 131) | type HaFormIntegerData = number;
type HaFormFloatData (line 132) | type HaFormFloatData = number;
type HaFormBooleanData (line 133) | type HaFormBooleanData = boolean;
type HaFormSelectData (line 134) | type HaFormSelectData = string;
type HaFormMultiSelectData (line 135) | type HaFormMultiSelectData = string[];
type HaFormTimeData (line 136) | type HaFormTimeData = HaDurationData;
type HaFormElement (line 138) | interface HaFormElement extends LitElement {
type Selector (line 145) | type Selector =
type ActionSelector (line 172) | interface ActionSelector {
type AddonSelector (line 177) | interface AddonSelector {
type AreaSelector (line 184) | interface AreaSelector {
type AttributeSelector (line 200) | interface AttributeSelector {
type BooleanSelector (line 206) | interface BooleanSelector {
type ColorRGBSelector (line 211) | interface ColorRGBSelector {
type UiColorSelector (line 216) | interface UiColorSelector {
type ColorTempSelector (line 221) | interface ColorTempSelector {
type DateSelector (line 228) | interface DateSelector {
type DateTimeSelector (line 233) | interface DateTimeSelector {
type DeviceSelector (line 238) | interface DeviceSelector {
type DurationSelector (line 251) | interface DurationSelector {
type EntitySelector (line 257) | interface EntitySelector {
type IconSelector (line 268) | interface IconSelector {
type LocationSelector (line 275) | interface LocationSelector {
type LocationSelectorValue (line 279) | interface LocationSelectorValue {
type MediaSelector (line 285) | interface MediaSelector {
type MediaSelectorValue (line 290) | interface MediaSelectorValue {
type NumberSelector (line 303) | interface NumberSelector {
type ObjectSelector (line 313) | interface ObjectSelector {
type SelectOption (line 318) | interface SelectOption {
type SelectSelector (line 323) | interface SelectSelector {
type StringSelector (line 332) | interface StringSelector {
type TargetSelector (line 353) | interface TargetSelector {
type TemplateSelector (line 368) | interface TemplateSelector {
type ThemeSelector (line 373) | interface ThemeSelector {
type TimeSelector (line 377) | interface TimeSelector {
type UiAction (line 382) | type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
type UiActionSelector (line 384) | interface UiActionSelector {
type ToggleActionConfig (line 389) | interface ToggleActionConfig extends BaseActionConfig {
type CallServiceActionConfig (line 393) | interface CallServiceActionConfig extends BaseActionConfig {
type NavigateActionConfig (line 404) | interface NavigateActionConfig extends BaseActionConfig {
type UrlActionConfig (line 409) | interface UrlActionConfig extends BaseActionConfig {
type MoreInfoActionConfig (line 414) | interface MoreInfoActionConfig extends BaseActionConfig {
type NoActionConfig (line 418) | interface NoActionConfig extends BaseActionConfig {
type CustomActionConfig (line 422) | interface CustomActionConfig extends BaseActionConfig {
type AssistActionConfig (line 426) | interface AssistActionConfig extends BaseActionConfig {
type BaseActionConfig (line 432) | interface BaseActionConfig {
type ConfirmationRestrictionConfig (line 437) | interface ConfirmationRestrictionConfig {
type RestrictionConfig (line 442) | interface RestrictionConfig {
type ActionConfig (line 446) | type ActionConfig =
FILE: src/editor/item-editor.ts
constant BASE_SCHEMA (line 11) | const BASE_SCHEMA: HaFormSchema[] = [
constant PRESET_LABEL_MAP (line 119) | const PRESET_LABEL_MAP: Record<string, string> = {
class ItemEditor (line 127) | class ItemEditor extends LitElement {
method _flatConfig (line 132) | private get _flatConfig() {
method _schema (line 145) | private get _schema(): HaFormSchema[] {
method render (line 175) | protected render() {
method _formValueChanged (line 192) | private _formValueChanged(ev: CustomEvent): void {
method styles (line 212) | static get styles(): CSSResult {
FILE: src/editor/items-editor.ts
class ItemsEditor (line 14) | class ItemsEditor extends LitElement {
method _getKey (line 23) | private _getKey(action: EntitySettings) {
method disconnectedCallback (line 31) | public disconnectedCallback() {
method render (line 35) | protected render() {
method _valueChanged (line 105) | private _valueChanged(ev: CustomEvent): void {
method _removeRow (line 121) | private _removeRow(ev: Event): void {
method _editRow (line 131) | private _editRow(ev: Event): void {
method _addRow (line 140) | private _addRow(ev: Event): void {
method _rowMoved (line 157) | private _rowMoved(ev: CustomEvent<{ oldIndex: number; newIndex: number...
method styles (line 168) | static get styles(): CSSResult {
FILE: src/localize/localize.ts
function localize (line 19) | function localize(string: string, capitalized = false, search = '', repl...
function capitalizeFirstLetter (line 40) | function capitalizeFirstLetter(string: string) {
function computeLabel (line 45) | function computeLabel(schema: HaFormSchema) {
FILE: src/power-distribution-card.ts
class PowerDistributionCard (line 31) | class PowerDistributionCard extends LitElement {
method getConfigElement (line 35) | public static async getConfigElement(): Promise<LitElement> {
method getStubConfig (line 43) | public static getStubConfig(): Record<string, unknown> {
method setConfig (line 70) | public async setConfig(config: PDCConfig): Promise<void> {
method firstUpdated (line 99) | public firstUpdated(): void {
method updated (line 135) | protected updated(changedProps: PropertyValues): void {
method styles (line 145) | public static get styles(): CSSResultGroup {
method connectedCallback (line 149) | public connectedCallback(): void {
method disconnectedCallback (line 154) | public disconnectedCallback(): void {
method _attachObserver (line 160) | private async _attachObserver(): Promise<void> {
method _adjustWidth (line 170) | private _adjustWidth(): void {
method _formatValue (line 177) | private _formatValue(rawValue: number, entity?: string, decimals?: num...
method _val (line 191) | private _val(item: EntitySettings | BarSettings): number {
method _state (line 208) | private _state(item: EntitySettings): unknown {
method render (line 220) | protected render(): TemplateResult {
method _handleAction (line 273) | private _handleAction(ev: ActionHandlerEvent): void {
method _render_item (line 298) | private _render_item(value: number, item: EntitySettings, index: numbe...
method _render_arrow (line 475) | private _render_arrow(direction: ArrowStates, color?: string): Templat...
method _render_bars (line 497) | private _render_bars(consumption: number, production: number): Templat...
method _createCardElement (line 540) | private _createCardElement(cardConfig: LovelaceCardConfig) {
method _rebuildCard (line 556) | private _rebuildCard(cardElToReplace: LovelaceCard, config: LovelaceCa...
FILE: src/presets.ts
type PresetType (line 3) | type PresetType = (typeof PresetList)[number];
FILE: src/types.ts
type HASSDomEvents (line 6) | interface HASSDomEvents {
type PDCConfig (line 18) | interface PDCConfig extends LovelaceCardConfig {
type EntitySettings (line 25) | interface EntitySettings extends presetFeatures {
type center (line 53) | interface center {
type presetFeatures (line 59) | interface presetFeatures {
type BarSettings (line 64) | interface BarSettings {
type ArrowStates (line 78) | type ArrowStates = 'right' | 'left' | 'none';
type Target (line 80) | interface Target extends EventTarget {
type CustomValueEvent (line 87) | interface CustomValueEvent<T> extends Event {
type EditorTarget (line 98) | interface EditorTarget extends EventTarget {
type HTMLElementValue (line 107) | interface HTMLElementValue extends HTMLElement {
type Window (line 111) | interface Window {
type Element (line 117) | interface Element {
FILE: src/utils/compute-color.ts
constant THEME_COLORS (line 1) | const THEME_COLORS = new Set([
constant YAML_ONLY_THEMES_COLORS (line 29) | const YAML_ONLY_THEMES_COLORS = new Set([
function computeCssVariableName (line 35) | function computeCssVariableName(color: string): string {
function computeCssColor (line 42) | function computeCssColor(color: string): string {
function isValidColorString (line 54) | function isValidColorString(color: string | undefined): boolean {
FILE: src/utils/create-thing.ts
constant TIMEOUT (line 4) | const TIMEOUT = 2000;
FILE: src/utils/custom-cards.ts
function registerCustomCard (line 3) | function registerCustomCard(type: string, name: string, description: str...
FILE: src/utils/hass-types/action.ts
type ToggleActionConfig (line 9) | interface ToggleActionConfig extends BaseActionConfig {
type CallServiceActionConfig (line 13) | interface CallServiceActionConfig extends BaseActionConfig {
type NavigateActionConfig (line 24) | interface NavigateActionConfig extends BaseActionConfig {
type UrlActionConfig (line 30) | interface UrlActionConfig extends BaseActionConfig {
type MoreInfoActionConfig (line 35) | interface MoreInfoActionConfig extends BaseActionConfig {
type AssistActionConfig (line 40) | interface AssistActionConfig extends BaseActionConfig {
type NoActionConfig (line 46) | interface NoActionConfig extends BaseActionConfig {
type CustomActionConfig (line 50) | interface CustomActionConfig extends BaseActionConfig {
type BaseActionConfig (line 54) | interface BaseActionConfig {
type ConfirmationRestrictionConfig (line 59) | interface ConfirmationRestrictionConfig {
type RestrictionConfig (line 64) | interface RestrictionConfig {
type ActionConfig (line 68) | type ActionConfig =
type ActionConfigParams (line 79) | interface ActionConfigParams {
type IntegrationType (line 89) | type IntegrationType =
FILE: src/utils/hass-types/action_handler.ts
type ActionHandlerOptions (line 8) | interface ActionHandlerOptions {
type ActionHandlerDetail (line 15) | interface ActionHandlerDetail {
type ActionHandlerEvent (line 19) | type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;
FILE: src/utils/hass-types/event.ts
type HASSDomEvents (line 7) | interface HASSDomEvents {}
type ValidHassDomEvent (line 10) | type ValidHassDomEvent = keyof HASSDomEvents;
type HASSDomEvent (line 12) | interface HASSDomEvent<T> extends Event {
FILE: src/utils/hass-types/fire_event.ts
type HASSDomEvents (line 32) | interface HASSDomEvents {}
type ValidHassDomEvent (line 35) | type ValidHassDomEvent = keyof HASSDomEvents;
type HASSDomEvent (line 37) | interface HASSDomEvent<T> extends Event {
FILE: src/utils/hass-types/format-number.ts
function applyPolyfills (line 4) | async function applyPolyfills(): Promise<void> {
FILE: src/utils/hass-types/get_main_window.ts
constant MAIN_WINDOW_NAME (line 2) | const MAIN_WINDOW_NAME = "ha-main-window";
FILE: src/utils/hass-types/handle-action.ts
type ActionConfigParams (line 12) | interface ActionConfigParams {
FILE: src/utils/hass-types/haptics.ts
type HapticType (line 11) | type HapticType =
type HASSDomEvents (line 22) | interface HASSDomEvents {
type GlobalEventHandlersEventMap (line 26) | interface GlobalEventHandlersEventMap {
FILE: src/utils/hass-types/has-action.ts
function hasAction (line 4) | function hasAction(config?: ActionConfig): boolean {
FILE: src/utils/hass-types/homeassistant.ts
type EntityCategory (line 21) | type EntityCategory = "config" | "diagnostic";
type EntityRegistryDisplayEntry (line 23) | interface EntityRegistryDisplayEntry {
type RegistryEntry (line 38) | interface RegistryEntry {
type DeviceRegistryEntry (line 43) | interface DeviceRegistryEntry extends RegistryEntry {
type AreaRegistryEntry (line 66) | interface AreaRegistryEntry extends RegistryEntry {
type FloorRegistryEntry (line 78) | interface FloorRegistryEntry extends RegistryEntry {
type ThemeVars (line 87) | interface ThemeVars {
type Theme (line 95) | type Theme = ThemeVars & {
type Themes (line 102) | interface Themes {
type NumberFormat (line 114) | enum NumberFormat {
type TimeFormat (line 124) | enum TimeFormat {
type TimeZone (line 131) | enum TimeZone {
type DateFormat (line 136) | enum DateFormat {
type FirstWeekday (line 144) | enum FirstWeekday {
type FrontendLocaleData (line 155) | interface FrontendLocaleData {
type LocalizeFunc (line 164) | type LocalizeFunc = (
type ValueChangedEvent (line 173) | interface ValueChangedEvent<T> extends CustomEvent {
type Constructor (line 179) | type Constructor<T = any> = new (...args: any[]) => T;
type ClassElement (line 181) | interface ClassElement {
type Credential (line 191) | interface Credential {
type MFAModule (line 196) | interface MFAModule {
type CurrentUser (line 202) | interface CurrentUser {
type ThemeSettings (line 215) | interface ThemeSettings {
type PanelInfo (line 225) | interface PanelInfo<T = Record<string, any> | null> {
type Panels (line 234) | type Panels = Record<string, PanelInfo>;
type CalendarViewChanged (line 236) | interface CalendarViewChanged {
type FullCalendarView (line 242) | type FullCalendarView =
type ThemeMode (line 248) | type ThemeMode = "auto" | "light" | "dark";
type ToggleButton (line 250) | interface ToggleButton {
type Translation (line 256) | interface Translation {
type TranslationMetadata (line 262) | interface TranslationMetadata {
type IconMetaFile (line 267) | interface IconMetaFile {
type IconMeta (line 272) | interface IconMeta {
type Notification (line 277) | interface Notification {
type Resources (line 285) | type Resources = Record<string, Record<string, string>>;
type Context (line 287) | interface Context {
type ServiceCallResponse (line 293) | interface ServiceCallResponse<T = any> {
type ServiceCallRequest (line 298) | interface ServiceCallRequest {
type CoreFrontendUserData (line 306) | interface CoreFrontendUserData {
type TranslationCategory (line 311) | type TranslationCategory =
type HomeAssistant (line 347) | interface HomeAssistant {
FILE: src/utils/hass-types/integration.ts
type IntegrationType (line 4) | type IntegrationType =
type IntegrationManifest (line 13) | interface IntegrationManifest {
FILE: src/utils/hass-types/localize.ts
type LocalizeKeys (line 1) | type LocalizeKeys =
type FlattenObjectKeys (line 30) | type FlattenObjectKeys<
type TranslationDict (line 39) | type TranslationDict = typeof import('../../localize/languages/en.json');
FILE: src/utils/hass-types/lovelace.ts
type Condition (line 3) | type Condition =
type LegacyCondition (line 14) | interface LegacyCondition {
type BaseCondition (line 20) | interface BaseCondition {
type LocationCondition (line 24) | interface LocationCondition extends BaseCondition {
type NumericStateCondition (line 29) | interface NumericStateCondition extends BaseCondition {
type StateCondition (line 36) | interface StateCondition extends BaseCondition {
type ScreenCondition (line 43) | interface ScreenCondition extends BaseCondition {
type UserCondition (line 48) | interface UserCondition extends BaseCondition {
type OrCondition (line 53) | interface OrCondition extends BaseCondition {
type AndCondition (line 58) | interface AndCondition extends BaseCondition {
type NotCondition (line 63) | interface NotCondition extends BaseCondition {
type LovelaceCardConfig (line 69) | interface LovelaceCardConfig {
type LovelaceLayoutOptions (line 81) | interface LovelaceLayoutOptions {
type LovelaceGridOptions (line 90) | interface LovelaceGridOptions {
type LovelaceCard (line 101) | interface LovelaceCard extends HTMLElement {
FILE: src/utils/hass-types/navigate.ts
type NavigateOptions (line 7) | interface NavigateOptions {
FILE: src/utils/hass-types/show-dialog-box.ts
type BaseDialogBoxParams (line 6) | interface BaseDialogBoxParams {
type AlertDialogParams (line 13) | interface AlertDialogParams extends BaseDialogBoxParams {
type ConfirmationDialogParams (line 17) | interface ConfirmationDialogParams extends BaseDialogBoxParams {
type PromptDialogParams (line 24) | interface PromptDialogParams extends BaseDialogBoxParams {
type DialogBoxParams (line 36) | interface DialogBoxParams
FILE: src/utils/hass-types/show-ha-voice-command-dialog.ts
type VoiceCommandDialogParams (line 6) | interface VoiceCommandDialogParams {
FILE: src/utils/hass-types/toast.ts
type HASSDomEvents (line 5) | interface HASSDomEvents {
type ShowToastParams (line 10) | interface ShowToastParams {
type ToastActionParams (line 21) | interface ToastActionParams {
FILE: src/utils/hass-types/toggle-entity.ts
constant STATES_OFF (line 5) | const STATES_OFF = ["closed", "locked", "off"];
FILE: src/utils/index.ts
function fireCustomEvent (line 15) | function fireCustomEvent<T>(node: HTMLElement | Window, type: string, de...
Condensed preview — 53 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (166K chars).
[
{
"path": ".github/workflows/build.yaml",
"chars": 389,
"preview": "name: \"Build\"\r\n\r\non:\r\n push:\r\n branches:\r\n - master\r\n pull_request:\r\n branches:\r\n - master\r\n\r\njobs:\r\n "
},
{
"path": ".github/workflows/release.yaml",
"chars": 825,
"preview": "name: Release\r\n\r\non:\r\n release:\r\n types: [published]\r\n\r\njobs:\r\n release:\r\n name: Prepare release\r\n runs-on: u"
},
{
"path": ".gitignore",
"chars": 1816,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": ".prettierrc.js",
"chars": 144,
"preview": "module.exports = {\r\n semi: true,\r\n trailingComma: 'all',\r\n singleQuote: true,\r\n printWidth: 120,\r\n tabWidth: 2,\r\n "
},
{
"path": ".vscode/settings.json",
"chars": 56,
"preview": "{\n \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2025 JonahKr\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 17186,
"preview": "# power-distribution-card\n[ 2017 Evgeny Poberezkin\r\n// eslint"
},
{
"path": "src/editor/bar-editor.ts",
"chars": 7425,
"preview": "import { LitElement, html, css, nothing, CSSResultGroup } from 'lit';\nimport { property, state } from 'lit/decorators.js"
},
{
"path": "src/editor/editor.ts",
"chars": 9165,
"preview": "import { LitElement, TemplateResult, html, css, CSSResultGroup, nothing } from 'lit';\nimport { html as staticHtml, unsaf"
},
{
"path": "src/editor/ha-form.ts",
"chars": 10367,
"preview": "import type { LitElement } from \"lit\";\n\ninterface HaDurationData {\n hours?: number;\n minutes?: number;\n seconds?: num"
},
{
"path": "src/editor/item-editor.ts",
"chars": 7343,
"preview": "import { LitElement, html, css, CSSResult, nothing } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport { "
},
{
"path": "src/editor/items-editor.ts",
"chars": 6263,
"preview": "import { LitElement, html } from 'lit';\n\nimport { EditorTarget, EntitySettings, HTMLElementValue } from '../types';\nimpo"
},
{
"path": "src/localize/languages/de.json",
"chars": 2061,
"preview": "{\r\n \"common\": {\r\n \"description\": \"Eine Karte zur Visualizierung von Stromverteilungen\"\r\n },\r\n \"editor\": {\r\n \"ac"
},
{
"path": "src/localize/languages/en.json",
"chars": 1947,
"preview": "{\r\n \"common\": {\r\n \"description\": \"A Lovelace Card for visualizing power distributions.\"\r\n },\r\n \"editor\": {\r\n \"a"
},
{
"path": "src/localize/languages/sk.json",
"chars": 1952,
"preview": "{\n \"common\": {\n \"description\": \"A Lovelace Card for visualizing power distributions.\"\n },\n \"editor\": {\n \"action"
},
{
"path": "src/localize/localize.ts",
"chars": 1597,
"preview": "import * as en from './languages/en.json';\r\nimport * as de from './languages/de.json';\r\nimport { HaFormSchema } from '.."
},
{
"path": "src/power-distribution-card.ts",
"chars": 20107,
"preview": "import { LitElement, html, TemplateResult, PropertyValues, CSSResultGroup } from 'lit';\n\nimport { customElement, propert"
},
{
"path": "src/presets.ts",
"chars": 1738,
"preview": "import { EntitySettings, PDCConfig } from './types';\r\n\r\nexport type PresetType = (typeof PresetList)[number];\r\n\r\nexport "
},
{
"path": "src/styles.ts",
"chars": 3701,
"preview": "import { css, html } from 'lit';\r\n\r\nexport const styles = css`\r\n * {\r\n box-sizing: border-box;\r\n }\r\n\r\n p {\r\n ma"
},
{
"path": "src/types.ts",
"chars": 3373,
"preview": "import { PresetType } from './presets';\r\nimport { ActionConfig, LovelaceCardConfig } from './utils';\r\nimport { NavigateO"
},
{
"path": "src/utils/compute-color.ts",
"chars": 1633,
"preview": "export const THEME_COLORS = new Set([\n \"primary\",\n \"accent\",\n \"red\",\n \"pink\",\n \"purple\",\n \"deep-purple\",\n \"indigo"
},
{
"path": "src/utils/create-thing.ts",
"chars": 1991,
"preview": "import { fireEvent } from \"./hass-types/fire_event\";\nimport type { LovelaceCardConfig } from \"./hass-types/lovelace\";\n\nc"
},
{
"path": "src/utils/custom-cards.ts",
"chars": 490,
"preview": "import { repository } from \"../../package.json\";\n\nexport function registerCustomCard(type: string, name: string, descrip"
},
{
"path": "src/utils/debounce.ts",
"chars": 1269,
"preview": "// From: src/common/util/debounce.ts https://raw.githubusercontent.com/home-assistant/frontend/446661915bbfd74b119176076"
},
{
"path": "src/utils/get-lovelace.ts",
"chars": 605,
"preview": "export const getLovelace = () => {\n const root = document\n .querySelector(\"home-assistant\")\n ?.shadowRoot?.queryS"
},
{
"path": "src/utils/ha-component-loader.ts",
"chars": 436,
"preview": "\nexport const loadHaComponents = () => {\n if (!customElements.get(\"ha-entity-picker\")) {\n loadCustomElement(\"hui"
},
{
"path": "src/utils/hass-types/action.ts",
"chars": 2255,
"preview": "\n/**\n * Types are from the original Homeassistant Repository:\n * https://github.com/home-assistant/frontend/blob/dev/sr"
},
{
"path": "src/utils/hass-types/action_handler.ts",
"chars": 483,
"preview": "/**\n * Types are from the original Homeassistant Repository:\n * https://github.com/home-assistant/frontend/blob/dev/src"
},
{
"path": "src/utils/hass-types/event.ts",
"chars": 317,
"preview": "/**\n * Types are from the original Homeassistant Repository:\n * https://github.com/home-assistant/frontend/blob/dev/src"
},
{
"path": "src/utils/hass-types/fire_event.ts",
"chars": 3071,
"preview": "// Polymer legacy event helpers used courtesy of the Polymer project.\n//\n// Copyright (c) 2017 The Polymer Authors. All "
},
{
"path": "src/utils/hass-types/format-number.ts",
"chars": 3650,
"preview": "import { shouldPolyfill } from \"@formatjs/intl-numberformat/should-polyfill.js\";\nimport { FrontendLocaleData, NumberForm"
},
{
"path": "src/utils/hass-types/get_main_window.ts",
"chars": 447,
"preview": "// From: https://github.com/home-assistant/frontend/blob/dev/src/data/main_window.ts\nexport const MAIN_WINDOW_NAME = \"ha"
},
{
"path": "src/utils/hass-types/handle-action.ts",
"chars": 5165,
"preview": "import { ActionConfig } from \"./action\";\nimport { fireEvent } from \"./fire_event\";\nimport { forwardHaptic } from \"./hapt"
},
{
"path": "src/utils/hass-types/haptics.ts",
"chars": 813,
"preview": "/**\n * Broadcast haptic feedback requests\n */\n\nimport { HASSDomEvent } from \"./event\";\nimport { fireEvent } from \"./fire"
},
{
"path": "src/utils/hass-types/has-action.ts",
"chars": 264,
"preview": "// From: https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/common/has-action.ts\nimport { ActionCon"
},
{
"path": "src/utils/hass-types/homeassistant.ts",
"chars": 10263,
"preview": "/**\n * This contains the typings for the hass Homeassistant object from various sources.\n * The main file is:\n * https:/"
},
{
"path": "src/utils/hass-types/integration.ts",
"chars": 1258,
"preview": "// From: https://github.com/home-assistant/frontend/blob/dev/src/data/integration.ts\nimport { LocalizeFunc } from \"./hom"
},
{
"path": "src/utils/hass-types/localize.ts",
"chars": 1639,
"preview": "export type LocalizeKeys =\n | FlattenObjectKeys<Omit<TranslationDict, \"supervisor\">>\n | `panel.${string}`\n | `ui.card"
},
{
"path": "src/utils/hass-types/lovelace.ts",
"chars": 2533,
"preview": "import { HomeAssistant } from \"./homeassistant\";\n\nexport type Condition =\n | LocationCondition\n | NumericStateConditio"
},
{
"path": "src/utils/hass-types/navigate.ts",
"chars": 710,
"preview": "// Partially from https://github.com/home-assistant/frontend/blob/dev/src/common/navigate.ts\n\nimport { fireEvent } from "
},
{
"path": "src/utils/hass-types/show-dialog-box.ts",
"chars": 2057,
"preview": "// Derived From: https://github.com/home-assistant/frontend/blob/dev/src/dialogs/generic/show-dialog-box.ts\nimport { Tem"
},
{
"path": "src/utils/hass-types/show-ha-voice-command-dialog.ts",
"chars": 956,
"preview": "import { fireEvent } from \"./fire_event\";\nimport type { HomeAssistant } from \"./homeassistant\";\n\n\n\nexport interface Voic"
},
{
"path": "src/utils/hass-types/toast.ts",
"chars": 774,
"preview": "import { fireEvent } from \"./fire_event\";\n\ndeclare global {\n // for fire event\n interface HASSDomEvents {\n \"hass-no"
},
{
"path": "src/utils/hass-types/toggle-entity.ts",
"chars": 1658,
"preview": "import { HomeAssistant, ServiceCallResponse } from \"./homeassistant\";\n\n// From: https://github.com/home-assistant/fronte"
},
{
"path": "src/utils/index.ts",
"chars": 773,
"preview": "export * from './custom-cards';\nexport { createThing } from './create-thing';\nexport { getLovelace } from './get-lovelac"
},
{
"path": "tsconfig.json",
"chars": 452,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es2017\",\n \"module\": \"esnext\",\n \"moduleResolution\": \"Bundler\",\n \"lib\": ["
}
]
About this extraction
This page contains the full source code of the JonahKr/power-distribution-card GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 53 files (151.4 KB), approximately 39.7k tokens, and a symbol index with 276 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.