├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ └── feature-request.yml
├── issue_label_bot.yaml
└── workflows
│ ├── build.yaml
│ ├── check.py
│ ├── stale.yaml
│ └── validate.yaml
├── .gitignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── dist
└── scheduler-card.js
├── hacs.json
├── package.json
├── rollup.config.js
├── screenshots
├── Demonstration.gif
├── Screenshot_Editor_1.png
├── Screenshot_Editor_2.png
├── Screenshot_Overview.png
├── action_variable_example.png
├── action_variable_list_example.png
├── entities_example.png
├── groups_example.png
├── instructions_select_entity.png
├── instructions_timepicker.png
├── tags_example.png
├── timescheme_example.png
└── version_badge.png
├── src
├── components
│ ├── button-group.ts
│ ├── generic-dialog.ts
│ ├── my-relative-time.ts
│ ├── scheduler-select.ts
│ ├── scheduler-selector.ts
│ ├── subscribe-mixin.ts
│ ├── time-picker.ts
│ ├── timeslot-editor.ts
│ ├── variable-picker.ts
│ └── variable-slider.ts
├── config-validation.ts
├── const.ts
├── data
│ ├── actions
│ │ ├── assign_action.ts
│ │ ├── compare_actions.ts
│ │ ├── compute_action_display.ts
│ │ ├── compute_actions.ts
│ │ ├── compute_common_actions.ts
│ │ ├── import_action.ts
│ │ └── migrate_action_config.ts
│ ├── compute_states.ts
│ ├── custom_dialog.ts
│ ├── date-time
│ │ ├── day_object.ts
│ │ ├── format_date.ts
│ │ ├── format_time.ts
│ │ ├── format_weekday.ts
│ │ ├── relative_time.ts
│ │ ├── start_of_week.ts
│ │ ├── string_to_date.ts
│ │ ├── time.ts
│ │ ├── weekday_to_list.ts
│ │ └── weekday_type.ts
│ ├── entities
│ │ ├── compute_entities.ts
│ │ ├── compute_supported_features.ts
│ │ ├── entity_filter.ts
│ │ └── parse_entity.ts
│ ├── entity_group.ts
│ ├── item_display
│ │ ├── compute_days_display.ts
│ │ ├── compute_schedule_display.ts
│ │ └── compute_time_display.ts
│ ├── match_pattern.ts
│ ├── variables
│ │ ├── compute_merged_variable.ts
│ │ ├── compute_variables.ts
│ │ ├── level_variable.ts
│ │ ├── list_variable.ts
│ │ └── text_variable.ts
│ └── websockets.ts
├── editor
│ ├── scheduler-editor-entity.ts
│ ├── scheduler-editor-options.ts
│ ├── scheduler-editor-time.ts
│ └── scheduler-editor.ts
├── helpers.ts
├── load-ha-form.js
├── localize
│ ├── languages
│ │ ├── cs.json
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── et.json
│ │ ├── fi.json
│ │ ├── fr.json
│ │ ├── he.json
│ │ ├── hu.json
│ │ ├── it.json
│ │ ├── lv.json
│ │ ├── nl.json
│ │ ├── no.json
│ │ ├── pl.json
│ │ ├── pt-BR.json
│ │ ├── pt.json
│ │ ├── ro.json
│ │ ├── ru.json
│ │ ├── sk.json
│ │ ├── sl.json
│ │ ├── uk.json
│ │ └── zh-Hans.json
│ └── localize.ts
├── scheduler-card-editor.ts
├── scheduler-card.ts
├── standard-configuration
│ ├── action_icons.ts
│ ├── action_name.ts
│ ├── actions.ts
│ ├── attribute.ts
│ ├── group.ts
│ ├── group_name.ts
│ ├── standardActions.ts
│ ├── standardIcon.ts
│ ├── standardStates.ts
│ ├── state_icons.ts
│ ├── states.ts
│ ├── variable_icons.ts
│ ├── variable_name.ts
│ ├── variable_options.ts
│ └── variables.ts
├── styles.ts
└── types.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
3 | extends: [
4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
7 | ],
8 | parserOptions: {
9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
10 | sourceType: 'module', // Allows for the use of imports
11 | experimentalDecorators: true,
12 | },
13 | rules: {
14 | "@typescript-eslint/camelcase": 0,
15 | "@typescript-eslint/no-explicit-any": "off",
16 | "@typescript-eslint/explicit-function-return-type": "off",
17 | "@typescript-eslint/no-empty-function": "off",
18 | "@typescript-eslint/no-non-null-assertion": "off",
19 | "@typescript-eslint/no-unused-vars": "off",
20 | "@typescript-eslint/no-use-before-define": "off",
21 | "@typescript-eslint/ban-ts-ignore": "off"
22 | }
23 | };
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help improve Scheduler-card
3 | labels: bug
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Note that this place is only for reporting issues with the card, not the [component](https://github.com/nielsfaber/scheduler-component).
9 |
10 | ---
11 |
12 | Thank you for taking the effort to fill in all relevant details.
13 | - type: checkboxes
14 | id: check_existing
15 | attributes:
16 | label: Checklist
17 | description: Please check the following items before submitting your request.
18 | options:
19 | - label: I checked for similar existing issues (both open and closed) before posting.
20 | required: true
21 | - label: I will participate in further discussion about this issue and can help by testing (if requested).
22 | required: true
23 | - type: input
24 | id: card-version
25 | attributes:
26 | label: Card Version
27 | description: "Which version of Scheduler-card are you running? If you're not sure, you can check it via the [browser logs](https://zapier.com/help/troubleshoot/behavior/view-and-save-your-browser-console-logs)."
28 | placeholder: "v1.0.0"
29 | validations:
30 | required: true
31 | - type: input
32 | id: component-version
33 | attributes:
34 | label: Component Version
35 | description: "Which version of Scheduler-com ponent are you running (if relevant)? See [here](https://github.com/nielsfaber/scheduler-component#updating) for instructions how to check it."
36 | - type: textarea
37 | id: bug-description
38 | attributes:
39 | label: Bug description
40 | description: What happened and what did you expect instead?
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: steps
45 | attributes:
46 | label: Steps to reproduce
47 | description: Which steps did you take to see this bug?
48 | validations:
49 | required: true
50 | - type: textarea
51 | id: extra
52 | attributes:
53 | label: Additional info
54 | description: Add additional info which you see relevant here (optional).
55 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Questions/discussion
4 | url: https://community.home-assistant.io/t/scheduler-card-custom-component/217458/1381
5 | about: Ask questions, discuss configuration, share your setup
6 | - name: Bugs/features for scheduler-component
7 | url: https://github.com/nielsfaber/scheduler-component/issues
8 | about: Report problems or propose new features for the scheduler-component
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | labels: feature_request
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Your feature requests should be kept small, else it will probably remain open for a long time or not be implemented at all.
9 | Note that this place is only for proposing improvements for the card, not the [component](https://github.com/nielsfaber/scheduler-component).
10 |
11 | ---
12 |
13 | Thank you for taking the effort to fill in all relevant details.
14 | - type: checkboxes
15 | id: check_existing
16 | attributes:
17 | label: Checklist
18 | description: Please check the following items before submitting your request.
19 | options:
20 | - label: I checked for similar existing requests (both open and closed) before posting.
21 | required: true
22 | - label: My request is generic, other users may benefit from it too.
23 | required: true
24 | - label: I will participate in further discussion about this feature and can test it (if requested) once it's done.
25 | required: true
26 | - type: textarea
27 | id: proposal
28 | attributes:
29 | label: Proposal
30 | description: A clear description of what you want to see changed or added.
31 | validations:
32 | required: true
33 | - type: textarea
34 | id: extra
35 | attributes:
36 | label: Additional info
37 | description: Some example of how the new functionality should look like.
38 | validations:
39 | required: true
--------------------------------------------------------------------------------
/.github/issue_label_bot.yaml:
--------------------------------------------------------------------------------
1 | label-alias:
2 | bug: 'bug'
3 | feature_request: 'enhancement'
4 | question: 'question'
5 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - uses: JasonEtco/upload-to-release@master
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | with:
18 | args: ./dist/scheduler-card.js text/javascript
19 |
--------------------------------------------------------------------------------
/.github/workflows/check.py:
--------------------------------------------------------------------------------
1 | # Setup
2 | import glob
3 | import json
4 | from colorama import init, Fore, Style
5 |
6 | init()
7 | from icu import Locale
8 |
9 | # Crossvalidator
10 | try:
11 | english_file = open("./src/localize/languages/en.json")
12 | except FileNotFoundError:
13 | print(
14 | "❗ The original",
15 | Style.BRIGHT + "English file",
16 | Style.DIM + Fore.RED + "was not found.",
17 | Style.RESET_ALL + "It needs to be fixed before this PR can be merged.",
18 | )
19 | raise Exception("English file not found")
20 |
21 | try:
22 | english_file = json.load(english_file)
23 | except json.decoder.JSONDecodeError as e:
24 | print(
25 | "❗ The original",
26 | Style.BRIGHT + "English file",
27 | Style.DIM + Fore.RED + "contains invalid JSON.",
28 | Style.RESET_ALL + "It needs to be fixed before this PR can be merged.",
29 | )
30 | print(
31 | "You may need to add a",
32 | Fore.GREEN + "comma at the end of a line" + Style.RESET_ALL,
33 | "before adding another line.",
34 | )
35 | print(
36 | "The error is",
37 | Style.BRIGHT + Fore.RED + str(e).replace("Expecting", "expecting"),
38 | )
39 | raise Exception("English file invalid JSON")
40 | english_lang = Locale("en_US")
41 |
42 |
43 | def cross_validate(english_value, other_language_value, other_language, key_name=None):
44 | this_lang = other_language.split("/")[-1].split(".js")[0].replace("-", "_")
45 | this_lang = Locale(this_lang).getDisplayName(english_lang)
46 | if other_language_value is None:
47 | print(
48 | "🟡 In" + Style.BRIGHT + Fore.YELLOW,
49 | f"{this_lang}" + Style.RESET_ALL,
50 | f"there is no value for {Fore.YELLOW + key_name + Fore.WHITE}.",
51 | )
52 | elif type(english_value) != type(other_language_value):
53 | raise Exception(
54 | f"The type of the English value ({english_value}) and the type of"
55 | + f"{this_lang}'s value ({other_language_value}) are different for key {key_name}."
56 | )
57 | elif isinstance(english_value, dict):
58 | for name, item in english_value.items():
59 | cross_validate(item, other_language_value.get(name), other_language, name)
60 |
61 |
62 | # The thing
63 | for filename in glob.glob("./src/localize/languages/*.json"):
64 | try:
65 | file = json.load(open(filename, encoding="utf-8"))
66 | cross_validate(english_file, file, filename)
67 | except json.decoder.JSONDecodeError as e:
68 | print(
69 | "❗ The file",
70 | Style.BRIGHT + filename,
71 | Style.DIM + Fore.RED + "contains invalid JSON.",
72 | Style.RESET_ALL + "It needs to be fixed before this PR can be merged.",
73 | )
74 | print(
75 | "You may need to add a",
76 | Fore.GREEN + "comma at the end of a line" + Fore.WHITE,
77 | "before adding another line.",
78 | )
79 | print(
80 | "The error is",
81 | Style.BRIGHT + Fore.RED + str(e).replace("Expecting", "expecting"),
82 | )
83 | raise Exception(f"{filename} contains invalid JSON")
84 | print(
85 | "🎉 All JSON files in",
86 | Style.BRIGHT + Fore.BLUE + "src/localize/languages" + Style.RESET_ALL,
87 | "were validated",
88 | Fore.GREEN + "successfully.",
89 | )
90 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yaml:
--------------------------------------------------------------------------------
1 | name: "Close stale issues/pull requests"
2 | on:
3 | schedule:
4 | - cron: "0 0 * * *"
5 | workflow_dispatch:
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v3
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days'
15 | stale-pr-message: 'This pull request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days'
16 | days-before-stale: 60
17 | days-before-close: 14
18 | operations-per-run: 500
19 | exempt-issue-labels: 'will pick this up at some point'
20 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yaml:
--------------------------------------------------------------------------------
1 | name: Validate
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | hacs_validate:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - uses: "actions/checkout@v4"
14 | - name: HACS validation
15 | uses: "hacs/action@main"
16 | with:
17 | CATEGORY: "plugin"
18 | language_validate:
19 | runs-on: "ubuntu-latest"
20 | steps:
21 | - uses: "actions/checkout@v4"
22 | - name: Setup Python
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: '3.8.x'
26 | - uses: actions/cache@v4
27 | name: Load pip cache
28 | with:
29 | path: ~/.cache/pip
30 | key: language-validate-pip-cache
31 | - name: Install dependencies
32 | run: |
33 | python3 -m pip install colorama setuptools
34 | python3 -m pip install pyicu
35 | - name: Language validation
36 | run: |
37 | script -e -c "python3 .github/workflows/check.py"
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | README.html
4 | *.DS_Store
5 | .vscode
6 | ._*
7 | stats.html
8 | dist/
9 | !dist/scheduler-card.js
10 | .history
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "es5",
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "tabWidth": 2
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in contributing to this project!
4 | You can contribute to this project by submitting a PR with your changes.
5 | Note that it may take between 1-4 weeks before your work can be reviewed and approved.
6 |
7 | ## Building
8 |
9 | * Install Node.js (latest LTS release).
10 | * On Linux, macOS, or Windows WSL, consider using [nvm.sh](https://github.com/nvm-sh/nvm/blob/master/README.md)
11 | * On Windows native, see [Nodejs.org](https://nodejs.org/)
12 | * `git clone https://github.com/nielsfaber/scheduler-card.git`
13 | * `cd scheduler-card`
14 | * `npm install --no-package-lock`
15 | * `npm start` # To develop interactively
16 | * `npm run build` # Run lint, prettier, rollup (update 'dist/scheduler-card.js')
17 |
18 | ## Submitting new translations
19 |
20 | If you would like to add a translation of the card in your native language, you can take the [english translation file](https://github.com/nielsfaber/scheduler-card/blob/main/src/localize/languages/en.json) as starting point and replace the texts on the right side of each line with local translation. Words within {curly brackets} should not be translated.
21 | Note that a translation file should be complete before it can be accepted as PR.
22 |
23 | ## Submitting new features
24 |
25 | If you wish to implement a new feature for this project, please first create a feature request to explain your idea and for further discussion/alignment.
26 | This avoids the risk of spending effort on code which may not be adopted by the project.
27 |
28 | ## Warning!
29 |
30 | Please note that a major rewrite of the codebase is ongoing as of March 2024. If you are
31 | planning on submitting a significant contribution, get in touch to request a branch for
32 | the new version to be published so that you can rebase your work on it.
33 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Scheduler Card",
3 | "render_readme": true,
4 | "filename": "scheduler-card.js",
5 | "homeassistant": "2025.5.0"
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scheduler-card",
3 | "version": "1.0.0",
4 | "description": "Scheduler card for Lovelace",
5 | "main": "dist/scheduler-card.js",
6 | "scripts": {
7 | "build": "npm run lint && npm run format && npm run rollup",
8 | "rollup": "rollup -c",
9 | "lint": "eslint src/**/*.ts --fix",
10 | "format": "prettier --write '**/*.ts'",
11 | "start": "rollup -c --watch"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/nielsfaber/scheduler-card.git"
16 | },
17 | "author": "nielsfaber",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/nielsfaber/scheduler-card/issues"
21 | },
22 | "homepage": "https://github.com/nielsfaber/scheduler-card#readme",
23 | "dependencies": {
24 | "@formatjs/intl-utils": "^3.8.4",
25 | "@mdi/js": "^6.4.95",
26 | "@typescript-eslint/eslint-plugin": "^2.6.0",
27 | "@typescript-eslint/parser": "^2.6.0",
28 | "custom-card-helpers": "1.8.0",
29 | "eslint-config-airbnb-base": "^14.0.0",
30 | "eslint-config-prettier": "^6.5.0",
31 | "eslint-plugin-import": "^2.18.2",
32 | "eslint-plugin-prettier": "^3.1.1",
33 | "fecha": "^4.2.1",
34 | "home-assistant-js-websocket": "^5.7.0",
35 | "lit": "^2.0.0",
36 | "rollup": "^1.32.1",
37 | "rollup-plugin-commonjs": "^10.1.0",
38 | "rollup-plugin-json": "^4.0.0",
39 | "rollup-plugin-node-resolve": "^5.2.0",
40 | "rollup-plugin-serve": "^1.0.1",
41 | "rollup-plugin-terser": "^5.1.2",
42 | "rollup-plugin-typescript2": "^0.31.1",
43 | "rollup-plugin-uglify": "^6.0.3",
44 | "typescript": "^3.6.4"
45 | },
46 | "devDependencies": {
47 | "eslint": "^6.8.0",
48 | "prettier": "1.19.1",
49 | "rollup-plugin-visualizer": "^4.2.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from 'rollup-plugin-node-resolve';
2 | import typescript from 'rollup-plugin-typescript2';
3 | import json from 'rollup-plugin-json';
4 | import { terser } from 'rollup-plugin-terser';
5 | import commonjs from 'rollup-plugin-commonjs';
6 | import visualizer from 'rollup-plugin-visualizer';
7 |
8 |
9 | const plugins = [
10 | nodeResolve(),
11 | commonjs({
12 | include: 'node_modules/**',
13 | }),
14 | typescript(),
15 | json(),
16 | visualizer(),
17 | terser(),
18 | ];
19 |
20 | export default [
21 | {
22 | input: 'src/scheduler-card.ts',
23 | output: {
24 | dir: 'dist',
25 | format: 'iife',
26 | sourcemap: false,
27 | },
28 | plugins: [...plugins],
29 | context: 'window',
30 | },
31 | ];
32 |
--------------------------------------------------------------------------------
/screenshots/Demonstration.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/Demonstration.gif
--------------------------------------------------------------------------------
/screenshots/Screenshot_Editor_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/Screenshot_Editor_1.png
--------------------------------------------------------------------------------
/screenshots/Screenshot_Editor_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/Screenshot_Editor_2.png
--------------------------------------------------------------------------------
/screenshots/Screenshot_Overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/Screenshot_Overview.png
--------------------------------------------------------------------------------
/screenshots/action_variable_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/action_variable_example.png
--------------------------------------------------------------------------------
/screenshots/action_variable_list_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/action_variable_list_example.png
--------------------------------------------------------------------------------
/screenshots/entities_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/entities_example.png
--------------------------------------------------------------------------------
/screenshots/groups_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/groups_example.png
--------------------------------------------------------------------------------
/screenshots/instructions_select_entity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/instructions_select_entity.png
--------------------------------------------------------------------------------
/screenshots/instructions_timepicker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/instructions_timepicker.png
--------------------------------------------------------------------------------
/screenshots/tags_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/tags_example.png
--------------------------------------------------------------------------------
/screenshots/timescheme_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/timescheme_example.png
--------------------------------------------------------------------------------
/screenshots/version_badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nielsfaber/scheduler-card/04be0e41aedaad224fadace3c8b53cf6706554c6/screenshots/version_badge.png
--------------------------------------------------------------------------------
/src/components/button-group.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 | import { PrettyPrintIcon, PrettyPrintName } from '../helpers';
4 | import { commonStyle } from '../styles';
5 |
6 | export type ButtonItem = {
7 | name?: string;
8 | icon?: string;
9 | value?: string;
10 | id?: string;
11 | disabled?: boolean;
12 | };
13 |
14 | function name(item: ButtonItem) {
15 | return item.name?.trim() || item.value || item.id || '';
16 | }
17 |
18 | function value(item: ButtonItem, index: number) {
19 | return item.id || item.value || index;
20 | }
21 |
22 | @customElement('button-group')
23 | export class ButtonGroup extends LitElement {
24 | @property({ type: Array }) items: ButtonItem[] = [];
25 | @property() value: string | null | number | (string | number)[] = null;
26 | @property({ type: Number }) min?: number;
27 | @property({ type: Boolean }) optional?: boolean;
28 | @property({ type: Boolean }) multiple?: boolean;
29 |
30 | render() {
31 | if (!this.items.length) {
32 | return html`
33 |
34 |
35 |
36 | `;
37 | }
38 | return this.items.map((val, key) => this.renderButton(val, key));
39 | }
40 |
41 | renderButton(item: ButtonItem, index: number) {
42 | const selection = Array.isArray(this.value) ? this.value : [this.value];
43 | const id = value(item, index);
44 |
45 | return html`
46 | this.selectItem(id)}
50 | >
51 | ${item.icon
52 | ? html`
53 |
54 | `
55 | : ''}
56 | ${PrettyPrintName(name(item))}
57 |
58 | `;
59 | }
60 |
61 | selectItem(val: string | number) {
62 | if (!Array.isArray(this.value)) {
63 | if (val == this.value) {
64 | if (this.optional) this.value = null;
65 | else return;
66 | } else this.value = val;
67 | } else if (!this.multiple) {
68 | this.value = this.value.includes(val) ? [] : Array(val);
69 | } else {
70 | let value = Array.isArray(this.value) ? [...this.value] : [];
71 | if (value.includes(val)) {
72 | if (this.min !== undefined && value.length <= this.min) return;
73 | value = value.filter(e => e != val);
74 | } else value.push(val);
75 | this.value = value;
76 | }
77 |
78 | const selection = Array.isArray(this.value)
79 | ? this.value.map(e => this.items.find((v, k) => value(v, k) == e))
80 | : this.value !== null
81 | ? this.items.find((v, k) => value(v, k) == this.value)
82 | : null;
83 | const myEvent = new CustomEvent('change', { detail: selection });
84 | this.dispatchEvent(myEvent);
85 | }
86 |
87 | static styles = commonStyle;
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/generic-dialog.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css, CSSResultGroup, TemplateResult } from 'lit';
2 | import { property, customElement, state } from 'lit/decorators.js';
3 | import { HomeAssistant } from 'custom-card-helpers';
4 | import { mdiClose } from '@mdi/js';
5 |
6 | export type DialogParams = {
7 | title: string;
8 | description: string | TemplateResult;
9 | primaryButtonLabel: string;
10 | secondaryButtonLabel?: string;
11 | primaryButtonCritical?: boolean;
12 | cancel: () => void;
13 | confirm: () => void;
14 | };
15 |
16 | @customElement('generic-dialog')
17 | export class GenericDialog extends LitElement {
18 | @property({ attribute: false }) public hass!: HomeAssistant;
19 |
20 | @state() private _params?: DialogParams;
21 |
22 | public async showDialog(params: DialogParams): Promise {
23 | this._params = params;
24 | await this.updateComplete;
25 | }
26 |
27 | public async closeDialog() {
28 | if (this._params) this._params.cancel();
29 | this._params = undefined;
30 | }
31 |
32 | render() {
33 | if (!this._params) return html``;
34 | return html`
35 |
36 |
37 |
43 |
44 | ${this._params.title}
45 |
46 |
47 |
48 | ${this._params.description}
49 |
50 |
51 | ${this._params.secondaryButtonLabel
52 | ? html`
53 |
54 | ${this._params.secondaryButtonLabel}
55 |
56 | `
57 | : ''}
58 |
64 | ${this._params.primaryButtonLabel}
65 |
66 |
67 | `;
68 | }
69 |
70 | confirmClick() {
71 | this._params!.confirm();
72 | }
73 |
74 | cancelClick() {
75 | this._params!.cancel();
76 | }
77 |
78 | static get styles(): CSSResultGroup {
79 | return css`
80 | div.wrapper {
81 | color: var(--primary-text-color);
82 | }
83 | `;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/my-relative-time.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 | import { HomeAssistant } from 'custom-card-helpers';
4 | import { capitalize, getLocale } from '../helpers';
5 | import { formatWeekday } from '../data/date-time/format_weekday';
6 | import { localize } from '../localize/localize';
7 | import { formatTime } from '../data/date-time/format_time';
8 | import { formatDate } from '../data/date-time/format_date';
9 | import { selectUnit } from '@formatjs/intl-utils';
10 |
11 | const secondsPerMinute = 60;
12 | const secondsPerHour = 3600;
13 | const hoursPerDay = 24;
14 |
15 | @customElement('my-relative-time')
16 | export class MyRelativeTime extends LitElement {
17 | @property() _hass?: HomeAssistant;
18 | @property() datetime?: Date;
19 |
20 | updateInterval = 60;
21 | timer = 0;
22 |
23 | startRefreshTimer(updateInterval: number) {
24 | clearInterval(this.timer);
25 | this.timer = window.setInterval(() => {
26 | this.requestUpdate();
27 | }, updateInterval * 1000);
28 | this.updateInterval = updateInterval;
29 | }
30 |
31 | set hass(hass: HomeAssistant) {
32 | this._hass = hass;
33 | this.startRefreshTimer(this.updateInterval); //restart
34 | }
35 |
36 | relativeTime(dateObj: Date): string {
37 | if (!this._hass) return '';
38 | const now = new Date();
39 | let delta = (now.getTime() - dateObj.getTime()) / 1000;
40 | const tense = delta >= 0 ? 'past' : 'future';
41 | delta = Math.abs(delta);
42 | const roundedDelta = Math.round(delta);
43 |
44 | if (tense == 'future' && roundedDelta > 0) {
45 | if (delta / secondsPerHour >= 6) {
46 | const startOfToday = now.setHours(0, 0, 0, 0);
47 | const daysFromNow = Math.floor(
48 | (dateObj.valueOf() - startOfToday.valueOf()) / (hoursPerDay * secondsPerHour * 1000)
49 | );
50 | let day = '';
51 | if (daysFromNow > 14) {
52 | //October 12
53 | day = formatDate(dateObj, getLocale(this._hass));
54 | } else if (daysFromNow > 7) {
55 | //Next Friday
56 | day = localize(
57 | 'ui.components.date.next_week_day',
58 | getLocale(this._hass),
59 | '{weekday}',
60 | formatWeekday(dateObj, getLocale(this._hass))
61 | );
62 | } else if (daysFromNow == 1) {
63 | //Tomorrow
64 | day = localize('ui.components.date.tomorrow', getLocale(this._hass));
65 | } else if (daysFromNow > 0) {
66 | //Friday
67 | day = formatWeekday(dateObj, getLocale(this._hass));
68 | }
69 |
70 | let time = localize(
71 | 'ui.components.time.absolute',
72 | getLocale(this._hass),
73 | '{time}',
74 | formatTime(dateObj, getLocale(this._hass))
75 | );
76 |
77 | if (dateObj.getHours() == 12 && dateObj.getMinutes() == 0) {
78 | time = localize('ui.components.time.at_noon', getLocale(this._hass));
79 | } else if (dateObj.getHours() == 0 && dateObj.getMinutes() == 0) {
80 | time = localize('ui.components.time.at_midnight', getLocale(this._hass));
81 | }
82 | return String(day + ' ' + time).trim();
83 | } else if (Math.round(delta / secondsPerMinute) > 60 && Math.round(delta / secondsPerMinute) < 120) {
84 | // in 1 hour and 52 minutes
85 | const mins = Math.round(delta / secondsPerMinute - 60);
86 | const join = this._hass.localize('ui.common.and');
87 |
88 | // @ts-expect-error
89 | const text1 = new Intl.RelativeTimeFormat(getLocale(this._hass).language, { numeric: 'auto' }).format(
90 | 1,
91 | 'hour'
92 | );
93 | const text2 = Intl.NumberFormat(getLocale(this._hass).language, {
94 | style: 'unit',
95 | // @ts-expect-error
96 | unit: 'minute',
97 | unitDisplay: 'long',
98 | }).format(mins);
99 |
100 | return `${text1} ${join} ${text2}`;
101 | } else if (Math.round(delta) > 60 && Math.round(delta) < 120) {
102 | // in 1 minute and 52 seconds
103 | const seconds = Math.round(delta - 60);
104 | const join = this._hass.localize('ui.common.and');
105 |
106 | // @ts-expect-error
107 | const text1 = new Intl.RelativeTimeFormat(getLocale(this._hass).language, { numeric: 'auto' }).format(
108 | 1,
109 | 'minute'
110 | );
111 | const text2 = Intl.NumberFormat(getLocale(this._hass).language, {
112 | style: 'unit',
113 | // @ts-expect-error
114 | unit: 'second',
115 | unitDisplay: 'long',
116 | }).format(seconds);
117 |
118 | return `${text1} ${join} ${text2}`;
119 | }
120 | }
121 |
122 | // in 5 minutes/hours/seconds (or now)
123 | const diff = selectUnit(dateObj);
124 | // @ts-expect-error
125 | return new Intl.RelativeTimeFormat(getLocale(this._hass).language, { numeric: 'auto' }).format(
126 | diff.value,
127 | diff.unit
128 | );
129 | }
130 |
131 | render() {
132 | if (!this._hass || !this.datetime) return html``;
133 |
134 | const now = new Date();
135 | const secondsRemaining = Math.round((this.datetime.valueOf() - now.valueOf()) / 1000);
136 | let updateInterval = 60;
137 | if (Math.abs(secondsRemaining) <= 150) updateInterval = Math.max(Math.ceil(Math.abs(secondsRemaining)) / 10, 2);
138 | if (this.updateInterval != updateInterval) this.startRefreshTimer(updateInterval);
139 |
140 | return html`
141 | ${capitalize(this.relativeTime(this.datetime))}
142 | `;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/components/scheduler-selector.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, TemplateResult, css, PropertyValues, CSSResultGroup } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 | import { fireEvent } from 'custom-card-helpers';
4 | import { isDefined, isEqual } from '../helpers';
5 |
6 | import './scheduler-select';
7 |
8 | export type Option = {
9 | value: string;
10 | name: string;
11 | icon?: string;
12 | };
13 |
14 | @customElement('scheduler-selector')
15 | export class SchedulerSelector extends LitElement {
16 | @property()
17 | items: Option[] = [];
18 |
19 | @property({ type: Array })
20 | value: string[] = [];
21 |
22 | @property()
23 | label = '';
24 |
25 | @property({ type: Boolean })
26 | invalid = false;
27 |
28 | shouldUpdate(changedProps: PropertyValues) {
29 | if (changedProps.get('items')) {
30 | if (!isEqual(this.items, changedProps.get('items') as Option[])) this.firstUpdated();
31 | }
32 | return true;
33 | }
34 |
35 | protected firstUpdated() {
36 | //remove items from selection which are not in the list (anymore)
37 | if (this.value.some(e => !this.items.map(v => v.value).includes(e))) {
38 | this.value = this.value.filter(e => this.items.map(v => v.value).includes(e));
39 | fireEvent(this, 'value-changed', { value: this.value });
40 | }
41 | }
42 |
43 | protected render(): TemplateResult {
44 | return html`
45 |
46 | ${this.value.length
47 | ? this.value
48 | .map(val => this.items.find(e => e.value == val))
49 | .filter(isDefined)
50 | .map(
51 | e =>
52 | html`
53 |
54 |
55 | ${e.name}
56 |
57 | this._removeClick(e.value)}>
58 |
59 |
60 |
61 | `
62 | )
63 | : ''}
64 |
65 |
66 | !this.value.includes(e.value))}
68 | label=${this.label}
69 | .icons=${false}
70 | .allowCustomValue=${true}
71 | @value-changed=${this._addClick}
72 | ?invalid=${this.invalid && this.value.length != this.items.length}
73 | >
74 |
75 | `;
76 | }
77 |
78 | private _removeClick(value: string) {
79 | this.value = this.value.filter(e => e !== value);
80 | fireEvent(this, 'value-changed', { value: this.value });
81 | }
82 |
83 | private _addClick(ev: Event) {
84 | ev.stopPropagation();
85 | const target = ev.target as HTMLInputElement;
86 | const value = target.value;
87 | if (!this.value.includes(value)) this.value = [...this.value, value];
88 | target.value = '';
89 | fireEvent(this, 'value-changed', { value: [...this.value] });
90 | }
91 |
92 | static get styles(): CSSResultGroup {
93 | return css`
94 | div.chip-set {
95 | margin: 0px -4px;
96 | }
97 | div.chip {
98 | height: 32px;
99 | border-radius: 16px;
100 | border: 2px solid rgba(var(--rgb-primary-color), 0.54);
101 | line-height: 1.25rem;
102 | font-size: 0.875rem;
103 | font-weight: 400;
104 | padding: 0px 12px;
105 | display: inline-flex;
106 | align-items: center;
107 | box-sizing: border-box;
108 | margin: 4px;
109 | }
110 | .icon {
111 | vertical-align: middle;
112 | outline: none;
113 | display: flex;
114 | align-items: center;
115 | border-radius: 50%;
116 | padding: 6px;
117 | color: rgba(0, 0, 0, 0.54);
118 | background: rgb(168, 232, 251);
119 | --mdc-icon-size: 20px;
120 | margin-left: -14px !important;
121 | }
122 | .label {
123 | margin: 0px 4px;
124 | }
125 | .button {
126 | cursor: pointer;
127 | background: var(--secondary-text-color);
128 | border-radius: 50%;
129 | --mdc-icon-size: 14px;
130 | color: var(--card-background-color);
131 | width: 16px;
132 | height: 16px;
133 | padding: 1px;
134 | box-sizing: border-box;
135 | display: inline-flex;
136 | align-items: center;
137 | margin-right: -6px !important;
138 | }
139 | `;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/subscribe-mixin.ts:
--------------------------------------------------------------------------------
1 | import { PropertyValues, ReactiveElement } from 'lit';
2 | import { property } from 'lit/decorators.js';
3 | import { UnsubscribeFunc } from 'home-assistant-js-websocket';
4 | import { HomeAssistant } from 'custom-card-helpers';
5 |
6 | export interface HassSubscribeElement {
7 | hassSubscribe(): UnsubscribeFunc[];
8 | }
9 | export type Constructor = new (...args: any[]) => T;
10 |
11 | export const SubscribeMixin = >(superClass: T) => {
12 | class SubscribeClass extends superClass {
13 | @property({ attribute: false }) public hass?: HomeAssistant;
14 |
15 | private __unsubs?: Array>;
16 |
17 | public connectedCallback() {
18 | super.connectedCallback();
19 | this.__checkSubscribed();
20 | }
21 |
22 | public disconnectedCallback() {
23 | super.disconnectedCallback();
24 | if (this.__unsubs) {
25 | while (this.__unsubs.length) {
26 | const unsub = this.__unsubs.pop()!;
27 | if (unsub instanceof Promise) {
28 | unsub.then(unsubFunc => unsubFunc());
29 | } else {
30 | unsub();
31 | }
32 | }
33 | this.__unsubs = undefined;
34 | }
35 | }
36 |
37 | protected updated(changedProps: PropertyValues) {
38 | super.updated(changedProps);
39 | if (changedProps.has('hass')) {
40 | this.__checkSubscribed();
41 | }
42 | }
43 |
44 | protected hassSubscribe(): Array> {
45 | return [];
46 | }
47 |
48 | private __checkSubscribed(): void {
49 | if (this.__unsubs !== undefined || !((this as unknown) as Element).isConnected || this.hass === undefined) {
50 | return;
51 | }
52 | this.__unsubs = this.hassSubscribe();
53 | }
54 | }
55 | return SubscribeClass;
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/variable-picker.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 | import { Variable, LevelVariable, EVariableType, ListVariable, TextVariable } from '../types';
4 |
5 | import './variable-slider';
6 | import './button-group';
7 | import { fireEvent } from 'custom-card-helpers';
8 |
9 | @customElement('scheduler-variable-picker')
10 | export class SchedulerVariablePicker extends LitElement {
11 | @property()
12 | variable?: Variable | null;
13 |
14 | @property()
15 | value?: string | number | null;
16 |
17 | firstUpdated() {
18 | if (
19 | (this.value === null || this.value === undefined) &&
20 | this.variable?.type == EVariableType.Level &&
21 | !(this.variable as LevelVariable).optional
22 | )
23 | this.levelVariableUpdated((this.variable as LevelVariable).min);
24 | }
25 |
26 | render() {
27 | if (!this.variable) return html``;
28 | else if (this.variable.type == EVariableType.Level) return this.renderLevelVariable();
29 | else if (this.variable.type == EVariableType.List) return this.renderListVariable();
30 | else if (this.variable.type == EVariableType.Text) return this.renderTextVariable();
31 | else return html``;
32 | }
33 |
34 | private levelVariableUpdated(ev: CustomEvent | number) {
35 | const value = typeof ev == 'number' ? ev : Number(ev.detail.value);
36 | this.value = value;
37 | fireEvent(this, 'value-changed', { value: value });
38 | }
39 |
40 | renderLevelVariable() {
41 | const variable = this.variable as LevelVariable;
42 | const value = Number(this.value);
43 |
44 | return html`
45 |
56 |
57 | `;
58 | }
59 |
60 | private listVariableUpdated(ev: Event | string) {
61 | const value = typeof ev == 'string' ? ev : String((ev.target as HTMLInputElement).value);
62 | this.value = value;
63 | fireEvent(this, 'value-changed', { value: value });
64 | }
65 |
66 | renderListVariable() {
67 | const variable = this.variable as ListVariable;
68 | const options = variable.options;
69 | const value = String(this.value) || null;
70 | if (options.length == 1 && value != options[0].value) this.listVariableUpdated(options[0].value);
71 |
72 | return html`
73 |
74 | `;
75 | }
76 |
77 | renderTextVariable() {
78 | const variable = this.variable as TextVariable;
79 | const value = this.value;
80 |
81 | return html`
82 |
83 | `;
84 | }
85 |
86 | static styles = css`
87 | ha-textfield {
88 | width: 100%;
89 | }
90 | `;
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/variable-slider.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 | import { loadHaForm } from '../load-ha-form';
4 | import { commonStyle } from '../styles';
5 | import { fireEvent } from 'custom-card-helpers';
6 |
7 | @customElement('variable-slider')
8 | export class VariableSlider extends LitElement {
9 | @property({ type: Number })
10 | min = 0;
11 |
12 | @property({ type: Number })
13 | max = 255;
14 |
15 | @property({ type: Number })
16 | step = 1;
17 |
18 | //raw value
19 | @property({ type: Number })
20 | set value(value: number) {
21 | value = isNaN(value) ? this.min : this._roundedValue(value / this.scaleFactor);
22 | this._displayedValue = value;
23 | }
24 |
25 | @property({ type: Number })
26 | scaleFactor = 1;
27 |
28 | @property({ type: String })
29 | unit = '';
30 |
31 | @property({ type: Boolean })
32 | optional = false;
33 |
34 | @property({ type: Boolean })
35 | disabled = false;
36 |
37 | @property({ type: Number })
38 | _displayedValue = 0;
39 |
40 | firstUpdated() {
41 | (async () => await loadHaForm())();
42 |
43 | if (this.disabled && !this.optional) {
44 | this.disabled = false;
45 | this.requestUpdate();
46 | }
47 | }
48 |
49 | render() {
50 | return html`
51 |
52 |
53 | ${this.getCheckbox()}
54 |
55 |
56 | ${this.getSlider()}
57 |
58 |
59 | ${this._displayedValue}${this.unit}
60 |
61 |
62 | `;
63 | }
64 |
65 | getSlider() {
66 | if (!this.disabled) {
67 | return html`
68 |
76 | `;
77 | } else {
78 | return html`
79 |
87 | `;
88 | }
89 | }
90 |
91 | getCheckbox() {
92 | if (!this.optional) return html``;
93 | return html`
94 |
95 | `;
96 | }
97 |
98 | toggleChecked(e: Event) {
99 | const checked = (e.target as HTMLInputElement).checked;
100 | this.disabled = !checked;
101 | const value = this.disabled ? null : this._scaledValue(this._displayedValue);
102 | fireEvent(this, 'value-changed', { value: value });
103 | }
104 |
105 | private _updateValue(e: Event) {
106 | let value = Number((e.target as HTMLInputElement).value);
107 | this._displayedValue = value;
108 | value = this._scaledValue(this._displayedValue);
109 | fireEvent(this, 'value-changed', { value: value });
110 | }
111 |
112 | private _roundedValue(value: number) {
113 | value = Math.round(value / this.step) * this.step;
114 | value = parseFloat(value.toPrecision(12));
115 | if (value > this.max) value = this.max;
116 | else if (value < this.min) value = this.min;
117 | return value;
118 | }
119 |
120 | private _scaledValue(value: number) {
121 | value = this._roundedValue(value);
122 | value = value * this.scaleFactor;
123 | value = parseFloat(value.toFixed(2));
124 | return value;
125 | }
126 |
127 | static styles = css`
128 | ${commonStyle} :host {
129 | width: 100%;
130 | }
131 | ha-slider {
132 | width: 100%;
133 | }
134 | `;
135 | }
136 |
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | import { CardConfig } from './types';
2 |
3 | export const CARD_VERSION = 'v3.2.15';
4 |
5 | export const DefaultTimeStep = 10;
6 |
7 | export const DefaultGroupIcon = 'folder-outline';
8 | export const DefaultEntityIcon = 'folder-outline';
9 | export const DefaultActionIcon = 'flash';
10 | export const DeadEntityName = '(unknown entity)';
11 | export const DeadEntityIcon = 'help-circle-outline';
12 |
13 | export const FieldTemperature = 'temperature';
14 | export const WorkdaySensor = 'binary_sensor.workday_sensor';
15 |
16 | export const NotifyDomain = 'notify';
17 |
18 | export enum ETabOptions {
19 | Entity = 'entity',
20 | Time = 'time',
21 | Options = 'options',
22 | }
23 |
24 | export const DefaultCardConfig: CardConfig = {
25 | type: 'scheduler-card',
26 | discover_existing: true,
27 | standard_configuration: true,
28 | include: [],
29 | exclude: [],
30 | groups: [],
31 | customize: {},
32 | title: true,
33 | time_step: 10,
34 | show_header_toggle: false,
35 | display_options: {
36 | primary_info: 'default',
37 | secondary_info: ['relative-time', 'additional-tasks'],
38 | icon: 'action',
39 | },
40 | tags: [],
41 | sort_by: ['relative-time', 'state'],
42 | };
43 |
44 | export const WebsocketEvent = 'scheduler_updated';
45 |
--------------------------------------------------------------------------------
/src/data/actions/assign_action.ts:
--------------------------------------------------------------------------------
1 | import { Action, EVariableType, LevelVariable, ListVariable, ServiceCall, TextVariable } from '../../types';
2 | import { omit } from '../../helpers';
3 |
4 | export const assignAction = (entity_id: string, action: Action) => {
5 | let output: ServiceCall = {
6 | entity_id: entity_id,
7 | service: action.service,
8 | service_data: { ...action.service_data },
9 | };
10 |
11 | Object.entries(action.variables || {}).forEach(([key, config]) => {
12 | const serviceArgs = Object.keys(output.service_data || {});
13 | if (serviceArgs.includes(key)) return;
14 |
15 | if (config.type == EVariableType.Level) {
16 | config = config as LevelVariable;
17 | output = {
18 | ...output,
19 | service_data: config.optional
20 | ? omit(output.service_data || {}, key)
21 | : {
22 | ...output.service_data,
23 | [key]: parseFloat((config.min * config.scale_factor).toPrecision(12)) || 0,
24 | },
25 | };
26 | } else if (config.type == EVariableType.List) {
27 | config = config as ListVariable;
28 | output = {
29 | ...output,
30 | service_data: {
31 | ...output.service_data,
32 | [key]: config.options.length ? config.options[0].value : undefined,
33 | },
34 | };
35 | } else if (config.type == EVariableType.Text) {
36 | config = config as TextVariable;
37 | output = {
38 | ...output,
39 | service_data: {
40 | ...output.service_data,
41 | [key]: '',
42 | },
43 | };
44 | }
45 | });
46 |
47 | return output;
48 | };
49 |
--------------------------------------------------------------------------------
/src/data/actions/compare_actions.ts:
--------------------------------------------------------------------------------
1 | import { isEqual } from '../../helpers';
2 | import { Action, EVariableType, LevelVariable, ListVariable } from '../../types';
3 |
4 | export function compareActions(actionA: Action, actionB: Action, allowVars = false) {
5 | const isOptional = (variable: string, action: Action) => {
6 | return (
7 | Object.keys(action.variables || {}).includes(variable) &&
8 | action.variables![variable].type == EVariableType.Level &&
9 | (action.variables![variable] as LevelVariable).optional
10 | );
11 | };
12 |
13 | //actions should have the same service
14 | if (actionA.service !== actionB.service) return false;
15 |
16 | const serviceDataA = Object.keys(actionA.service_data || {});
17 | const variablesA = Object.keys(actionA.variables || {});
18 |
19 | const serviceDataB = Object.keys(actionB.service_data || {});
20 | const variablesB = Object.keys(actionB.variables || {});
21 |
22 | const argsA = [...new Set([...serviceDataA, ...variablesA])];
23 | const argsB = [...new Set([...serviceDataB, ...variablesB])];
24 | const allArgs = [...new Set([...argsA, ...argsB])];
25 |
26 | return allArgs.every(arg => {
27 | // both actions must have the parameter in common
28 | if (!argsA.includes(arg)) return isOptional(arg, actionB);
29 | if (!argsB.includes(arg)) return isOptional(arg, actionA);
30 |
31 | // if its a fixed parameter it must be equal
32 | if (
33 | serviceDataA.filter(e => !variablesA.includes(e)).includes(arg) &&
34 | serviceDataB.filter(e => !variablesB.includes(e)).includes(arg)
35 | )
36 | return isEqual(actionA.service_data![arg], actionB.service_data![arg]);
37 |
38 | // if both are variables they are assumed to be equal
39 | if (variablesA.includes(arg) && variablesB.includes(arg)) return true;
40 |
41 | if (!allowVars) return false;
42 |
43 | // compare a fixed value with variable
44 | const value = serviceDataA.includes(arg) ? actionA.service_data![arg] : actionB.service_data![arg];
45 |
46 | const variable = variablesA.includes(arg) ? actionA.variables![arg] : actionB.variables![arg];
47 |
48 | if (variable.type === EVariableType.List) {
49 | return (variable as ListVariable).options.some(e => e.value === value);
50 | } else if (variable.type === EVariableType.Level) return !isNaN(value);
51 | else if (variable.type == EVariableType.Text) return true;
52 |
53 | return false;
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/src/data/actions/compute_action_display.ts:
--------------------------------------------------------------------------------
1 | import { computeEntity } from 'custom-card-helpers';
2 | import { Action, EVariableType, LevelVariable, ListVariable, TextVariable } from '../../types';
3 | import { levelVariableDisplay } from '../variables/level_variable';
4 | import { PrettyPrintName } from '../../helpers';
5 | import { listVariableDisplay } from '../variables/list_variable';
6 | import { textVariableDisplay } from '../variables/text_variable';
7 |
8 | const wildcardPattern = /\{([^\}]+)\}/;
9 | const parameterPattern = /\[([^\]]+)\]/;
10 | const MAX_RECURSION_DEPTH = 100;
11 |
12 | export function computeActionDisplay(action: Action) {
13 | let name = action.name;
14 | if (!name) name = PrettyPrintName(computeEntity(action.service));
15 |
16 | const replaceWildcards = (string: string, recursionDepth = 0): string => {
17 | const res = wildcardPattern.exec(string);
18 | if (!res) return string;
19 | const field = res[1];
20 |
21 | if (!Object.keys(action.service_data || {}).includes(field)) return string.replace(res[0], '');
22 |
23 | let replacement: string;
24 | if (Object.keys(action.variables || {}).includes(field)) {
25 | if (action.variables![field].type == EVariableType.Level)
26 | replacement = levelVariableDisplay(action.service_data![field], action.variables![field] as LevelVariable);
27 | else if (action.variables![field].type == EVariableType.List)
28 | replacement = listVariableDisplay(action.service_data![field], action.variables![field] as ListVariable);
29 | else replacement = textVariableDisplay(action.service_data![field], action.variables![field] as TextVariable);
30 | } else {
31 | replacement = action.service_data![field];
32 | }
33 | string = string.replace(res[0], replacement);
34 | if (recursionDepth >= MAX_RECURSION_DEPTH) return string;
35 | return replaceWildcards(string);
36 | };
37 |
38 | const replaceSubstrings = (string: string, recursionDepth = 0): string => {
39 | const res = parameterPattern.exec(string);
40 | if (!res) return string;
41 |
42 | const field = res[1].match(wildcardPattern)![1];
43 | const hasWildcard = Object.keys(action.service_data || {}).includes(field);
44 | if (hasWildcard) string = string.replace(res[0], replaceWildcards(res[1]));
45 | else string = string.replace(res[0], '');
46 | if (recursionDepth >= MAX_RECURSION_DEPTH) return string;
47 | return replaceSubstrings(string);
48 | };
49 |
50 | name = replaceSubstrings(name);
51 | name = replaceWildcards(name);
52 | return name || '';
53 | }
54 |
--------------------------------------------------------------------------------
/src/data/actions/compute_actions.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant, computeDomain, computeEntity } from 'custom-card-helpers';
2 | import { CardConfig, Action, ListVariable, EVariableType } from '../../types';
3 | import { standardActions } from '../../standard-configuration/standardActions';
4 | import { matchPattern } from '../match_pattern';
5 | import { isDefined, flatten, omit, pick } from '../../helpers';
6 | import { compareActions } from './compare_actions';
7 | import { computeCommonActions } from './compute_common_actions';
8 | import { computeVariables } from '../variables/compute_variables';
9 | import { HassEntity } from 'home-assistant-js-websocket';
10 | import { computeSupportedFeatures } from '../entities/compute_supported_features';
11 | import { computeActionDisplay } from './compute_action_display';
12 |
13 | function parseString(str: string) {
14 | return str
15 | .replace(/_/g, ' ')
16 | .trim()
17 | .toLowerCase();
18 | }
19 |
20 | export function computeActions(entity_id: string | string[], hass: HomeAssistant, config: CardConfig): Action[] {
21 | if (Array.isArray(entity_id)) {
22 | let actions = entity_id.map(e => computeActions(e, hass, config));
23 | return computeCommonActions(actions);
24 | }
25 |
26 | const stateObj = hass.states[entity_id] as HassEntity | undefined;
27 |
28 | //fetch standard actions for entity
29 | let actions = config.standard_configuration ? standardActions(entity_id, hass) : [];
30 |
31 | //get excluded actions for entity
32 | const excludedActions: string[] = flatten(
33 | Object.entries(config.customize)
34 | .filter(([a]) => matchPattern(a, entity_id))
35 | .sort((a, b) => b[0].length - a[0].length)
36 | .map(([, b]) => b.exclude_actions)
37 | .filter(isDefined)
38 | );
39 | if (excludedActions.length) {
40 | actions = actions.filter(e => !excludedActions.some(a => parseString(computeActionDisplay(e)).includes(a)));
41 | }
42 |
43 | //get customizations for entity
44 | const customizedActions: Action[] = flatten(
45 | Object.entries(config.customize)
46 | .filter(([a]) => matchPattern(a, entity_id))
47 | .sort((a, b) => b[0].length - a[0].length)
48 | .map(([, b]) => b.actions)
49 | .filter(isDefined)
50 | );
51 | if (customizedActions.length) {
52 | customizedActions.forEach(action => {
53 | //ensure services have domain prefixed
54 | if (!computeDomain(action.service).length)
55 | action = { ...action, service: computeDomain(entity_id) + '.' + computeEntity(action.service) };
56 |
57 | //ensure service call has no entity_id
58 | if (action.service_data) action = { ...action, service_data: omit(action.service_data, 'entity_id') };
59 | let res = actions.findIndex(e => compareActions(e, action));
60 | if (res < 0) {
61 | //try to find it in unfiltered list of built-in actions
62 | let allActions = config.standard_configuration ? standardActions(entity_id, hass, false) : [];
63 | const match = allActions.find(e => compareActions(e, action));
64 | if (match) {
65 | actions = [...actions, match];
66 | res = actions.length - 1;
67 | }
68 | }
69 |
70 | if (res >= 0) {
71 | //standard action should be overwritten
72 | Object.assign(actions, {
73 | [res]: { ...actions[res], ...omit(action, 'variables') },
74 | });
75 |
76 | if (Object.keys(action.variables || {}).length) {
77 | let variableConfig = actions[res].variables || {};
78 |
79 | //merge variable config
80 | variableConfig = Object.entries(variableConfig)
81 | .map(([field, variable]) => {
82 | return Object.keys(action.variables!).includes(field)
83 | ? [field, { ...variable, ...action.variables![field] }]
84 | : [field, action.variables![field]];
85 | })
86 | .reduce((obj, [key, val]) => (val ? Object.assign(obj, { [key as string]: val }) : obj), {});
87 |
88 | //add new variables
89 | const newVariables = Object.keys(action.variables!).filter(e => !Object.keys(variableConfig).includes(e));
90 |
91 | variableConfig = {
92 | ...variableConfig,
93 | ...computeVariables(pick(action.variables, newVariables)),
94 | };
95 |
96 | Object.assign(actions, {
97 | [res]: { ...actions[res], variables: variableConfig },
98 | });
99 | }
100 | } else {
101 | //add a new action
102 | action = {
103 | ...action,
104 | variables: computeVariables(action.variables),
105 | };
106 | actions.push(action);
107 | }
108 | });
109 | }
110 |
111 | //filter by supported_features
112 | const supportedFeatures = computeSupportedFeatures(stateObj);
113 | actions = actions.filter(e => !e.supported_feature || e.supported_feature & supportedFeatures);
114 |
115 | //list variable with 1 option is not really a variable
116 | actions = actions.map(action => {
117 | if (Object.keys(action.variables || {}).length) {
118 | Object.keys(action.variables!).forEach(field => {
119 | if (
120 | action.variables![field].type == EVariableType.List &&
121 | (action.variables![field] as ListVariable).options.length == 1
122 | ) {
123 | action = {
124 | ...action,
125 | service_data: {
126 | ...action.service_data,
127 | [field]: (action.variables![field] as ListVariable).options[0].value,
128 | },
129 | variables: omit(action.variables!, field),
130 | };
131 | }
132 | });
133 | }
134 | return action;
135 | });
136 |
137 | return actions;
138 | }
139 |
--------------------------------------------------------------------------------
/src/data/actions/compute_common_actions.ts:
--------------------------------------------------------------------------------
1 | import { isDefined } from '../../helpers';
2 | import { Action, VariableDictionary, EVariableType, ListVariable } from '../../types';
3 | import { compareActions } from './compare_actions';
4 | import { computeMergedVariable } from '../variables/compute_merged_variable';
5 |
6 | export function computeCommonActions(actionLists: Action[][]) {
7 | //calculate actions that are in common for all
8 |
9 | if (actionLists.length == 1) return actionLists[0];
10 |
11 | let commonActions = actionLists[0].filter(action =>
12 | actionLists.slice(1).every(list => list.some(e => compareActions(action, e)))
13 | );
14 |
15 | commonActions = commonActions
16 | .map(action => {
17 | const mergedVariables: VariableDictionary = Object.entries(action.variables || {})
18 | .map(([field, variable]) => {
19 | //compute action per entity
20 | const actions = actionLists.map(e => e.find(k => compareActions(k, action)));
21 |
22 | //remove the variable if it is not in common
23 | if (!actions.every(e => e && e.variables && field in e.variables)) {
24 | return [field, undefined];
25 | }
26 |
27 | const variables = actions.map(e => e!.variables![field]);
28 |
29 | if (!variables.every(e => variable.type == e.type)) return [field, undefined];
30 |
31 | //compute intersection of variables
32 | return [field, computeMergedVariable(...variables)];
33 | })
34 | .reduce((obj, [key, val]) => (val ? Object.assign(obj, { [key as string]: val }) : obj), {});
35 |
36 | if (Object.values(mergedVariables).find(e => e.type == EVariableType.List && !(e as ListVariable).options.length))
37 | return undefined;
38 |
39 | action = {
40 | ...action,
41 | variables: mergedVariables,
42 | };
43 |
44 | return action;
45 | })
46 | .filter(isDefined);
47 |
48 | return commonActions;
49 | }
50 |
--------------------------------------------------------------------------------
/src/data/actions/import_action.ts:
--------------------------------------------------------------------------------
1 | import { ServiceCall, Action, VariableDictionary, EVariableType, ListVariable } from '../../types';
2 | import { standardActions } from '../../standard-configuration/standardActions';
3 | import { unique } from '../../helpers';
4 | import { HomeAssistant } from 'custom-card-helpers';
5 | import { compareActions } from './compare_actions';
6 | import { listVariable } from '../variables/list_variable';
7 |
8 | export function importAction(action: ServiceCall, hass: HomeAssistant): Action {
9 | const id = action.entity_id || action.service;
10 | const service = action.service;
11 | const serviceData = action.service_data || {};
12 | const serviceArgs = Object.keys(serviceData);
13 |
14 | let actions = standardActions(id, hass, false);
15 | let matches = actions.filter(e => compareActions(action, e, true));
16 |
17 | if (matches.length == 1) actions = matches;
18 | else {
19 | //only find actions with matching service name
20 | actions = actions.filter(e => e.service == service);
21 |
22 | // if action has a fixed argument, it should be provided to be a match
23 | actions = actions.filter(e => Object.keys(e.service_data || {}).every(k => serviceArgs.includes(k)));
24 | }
25 |
26 | if (actions.length > 1) {
27 | //the match is ambiguous, check service_data to find the action with best overlap
28 |
29 | actions.sort((a, b) => {
30 | const fixedArgsOverlapA = Object.entries(a.service_data || {})
31 | .map(([k, v]): number => (k in serviceData ? (serviceData[k] == v ? 1 : -1) : 0))
32 | .reduce((sum, e) => sum + e, 0);
33 | const fixedArgsOverlapB = Object.entries(b.service_data || {})
34 | .map(([k, v]): number => (k in serviceData ? (serviceData[k] == v ? 1 : -1) : 0))
35 | .reduce((sum, e) => sum + e, 0);
36 |
37 | //if one of the services has more fixed serviceArgs in common, it is preferred
38 | if (fixedArgsOverlapA > fixedArgsOverlapB) return -1;
39 | if (fixedArgsOverlapA < fixedArgsOverlapB) return 1;
40 |
41 | const serviceDataA = unique([...Object.keys(a.service_data || {}), ...Object.keys(a.variables || {})]);
42 | const serviceDataB = unique([...Object.keys(b.service_data || {}), ...Object.keys(b.variables || {})]);
43 |
44 | const overlapA = serviceArgs.filter(e => serviceDataA.includes(e)).length;
45 | const overlapB = serviceArgs.filter(e => serviceDataB.includes(e)).length;
46 |
47 | //if one of the services has more variable serviceArgs in common, it is preferred
48 | if (overlapA > overlapB) return -1;
49 | if (overlapA < overlapB) return 1;
50 |
51 | const extraKeysA = serviceDataA.filter(e => !serviceArgs.includes(e)).length;
52 | const extraKeysB = serviceDataB.filter(e => !serviceArgs.includes(e)).length;
53 |
54 | //if one of the services has less extra serviceArgs, it is preferred
55 | if (extraKeysA > extraKeysB) return 1;
56 | if (extraKeysA < extraKeysB) return -1;
57 | return 0;
58 | });
59 | }
60 | if (actions.length) {
61 | // add option to the list if it is not existing
62 | let variables: VariableDictionary = { ...(actions[0].variables || {}) };
63 | Object.entries(serviceData).forEach(([key, val]) => {
64 | if (!Object.keys(variables || {}).includes(key)) return;
65 | if (variables[key].type != EVariableType.List) return;
66 | variables = {
67 | ...variables,
68 | [key]: (variables[key] as ListVariable).options.some(e => e.value == val)
69 | ? variables[key]
70 | : { ...variables[key], options: [...(variables[key] as ListVariable).options, { value: val }] },
71 | };
72 | });
73 |
74 | return {
75 | ...actions[0],
76 | service_data: { ...(actions[0].service_data || {}), ...serviceData },
77 | variables: variables,
78 | };
79 | } else {
80 | //unknown action, add from config
81 | return {
82 | service: service,
83 | service_data: action.service_data,
84 | };
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/data/actions/migrate_action_config.ts:
--------------------------------------------------------------------------------
1 | import { ServiceCall, Action, EVariableType, ListVariable, LevelVariable } from '../../types';
2 | import { HomeAssistant } from 'custom-card-helpers';
3 | import { importAction } from './import_action';
4 | import { compareActions } from './compare_actions';
5 | import { assignAction } from './assign_action';
6 |
7 | export const migrateActionConfig = (
8 | config: ServiceCall,
9 | entities: string[],
10 | actions: Action[],
11 | hass: HomeAssistant
12 | ): ServiceCall[] | null => {
13 | if (!config) return null;
14 |
15 | const action = importAction(config, hass);
16 | let match = actions.find(e => compareActions(e, action, true));
17 |
18 | if (!match) return null;
19 |
20 | let output: ServiceCall[] | null = entities.map(e => assignAction(e, match!));
21 |
22 | output = Object.keys(match.variables || {})
23 | .filter(k => Object.keys(config.service_data || {}).includes(k))
24 | .reduce((output: ServiceCall[] | null, variable) => {
25 | if (!output) return output;
26 | switch (match!.variables![variable].type) {
27 | case EVariableType.Level:
28 | //keep the selected level variable while maintaining min/max/step size/scale factor
29 | const levelVariable = match!.variables![variable] as LevelVariable;
30 | let val = Number(config.service_data![variable]);
31 | val = val / levelVariable.scale_factor;
32 | val = Math.round(val / levelVariable.step) * levelVariable.step;
33 | val = parseFloat(val.toPrecision(12));
34 | if (val > levelVariable.max) val = levelVariable.max;
35 | else if (val < levelVariable.min) val = levelVariable.min;
36 | val = val * levelVariable.scale_factor;
37 | val = parseFloat(val.toFixed(2));
38 | return output.map(e => Object.assign(e, { service_data: { ...e.service_data, [variable]: val } }));
39 |
40 | case EVariableType.List:
41 | const listVariable = match!.variables![variable] as ListVariable;
42 | if (listVariable.options.some(e => e.value == config.service_data![variable]))
43 | //keep the selected list variable if it is in common
44 | return output.map(e =>
45 | Object.assign(e, { service_data: { ...e.service_data, [variable]: config.service_data![variable] } })
46 | );
47 | else return null;
48 |
49 | case EVariableType.Text:
50 | //keep the selected text variable
51 | return output.map(e =>
52 | Object.assign(e, { service_data: { ...e.service_data, [variable]: config.service_data![variable] } })
53 | );
54 | default:
55 | return output;
56 | }
57 | }, output);
58 |
59 | return output;
60 | };
61 |
--------------------------------------------------------------------------------
/src/data/compute_states.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { CardConfig, Variable } from '../types';
3 | import { standardStates } from '../standard-configuration/standardStates';
4 | import { isDefined } from '../helpers';
5 | import { matchPattern } from './match_pattern';
6 | import { listVariable } from './variables/list_variable';
7 | import { levelVariable } from './variables/level_variable';
8 |
9 | export function computeStates(entity_id: string, hass: HomeAssistant, config: CardConfig): Variable | null {
10 | //fetch standard states for entity
11 | let stateConfig = config.standard_configuration ? standardStates(entity_id, hass) : null;
12 |
13 | //get customizations for entity
14 | const customizedStates = Object.entries(config.customize)
15 | .filter(([a]) => matchPattern(a, entity_id))
16 | .sort((a, b) => b[0].length - a[0].length)
17 | .map(([, b]) => b.states)
18 | .filter(isDefined);
19 | if (customizedStates.length) {
20 | customizedStates.forEach(userConfig => {
21 | if (Array.isArray(userConfig)) {
22 | stateConfig = listVariable({ options: userConfig.map(e => Object({ value: e })) });
23 | } else {
24 | stateConfig = levelVariable(userConfig);
25 | }
26 | });
27 | }
28 | return stateConfig;
29 | }
30 |
--------------------------------------------------------------------------------
/src/data/custom_dialog.ts:
--------------------------------------------------------------------------------
1 | import { fireEvent } from 'custom-card-helpers';
2 |
3 | type ValidHassDomEvent = keyof HASSDomEvents;
4 |
5 | export interface ProvideHassElement {
6 | provideHass(element: HTMLElement);
7 | }
8 |
9 | interface HassDialog extends HTMLElement {
10 | showDialog(params: T);
11 | closeDialog?: () => boolean | void;
12 | }
13 |
14 | interface ShowDialogParams {
15 | dialogTag: string;
16 | dialogImport: () => Promise;
17 | dialogParams: T;
18 | addHistory?: boolean;
19 | }
20 |
21 | interface LoadedDialogInfo {
22 | element: Promise;
23 | closedFocusTargets?: Set;
24 | }
25 |
26 | interface LoadedDialogsDict {
27 | [tag: string]: LoadedDialogInfo;
28 | }
29 |
30 | const LOADED: LoadedDialogsDict = {};
31 |
32 | export const isEmbeddedInPopup = (element: HTMLElement) => {
33 | let root = element as any;
34 | while (root && root.parentNode) {
35 | if (root.parentNode === document) {
36 | break;
37 | } else if (root.parentNode instanceof DocumentFragment) {
38 | root = root.parentNode.host;
39 | } else {
40 | root = root.parentNode;
41 | }
42 | if (root.tagName.toUpperCase() == 'BODY') return false;
43 | else if (root.tagName.toUpperCase() == 'BROWSER-MOD-POPUP') return true;
44 | }
45 | return false;
46 | };
47 |
48 | export const getPopupRootElement = (element: HTMLElement) => {
49 | let root = element as any;
50 | while (root && root.parentNode) {
51 | if (root.parentNode === document) {
52 | break;
53 | } else if (root.parentNode instanceof DocumentFragment) {
54 | root = root.parentNode.host;
55 | } else {
56 | root = root.parentNode;
57 | }
58 | if (root.tagName.toUpperCase() == 'BODY') break;
59 | }
60 | return root;
61 | };
62 |
63 | export const showDialog = async (
64 | element: HTMLElement & ProvideHassElement,
65 | config: ShowDialogParams,
66 | useAlternativeDialog?: boolean
67 | ) => {
68 | const popupRoot = useAlternativeDialog
69 | ? getPopupRootElement(element)
70 | : useAlternativeDialog === undefined
71 | ? isEmbeddedInPopup(element)
72 | ? getPopupRootElement(element)
73 | : null
74 | : null;
75 |
76 | if (popupRoot === null) {
77 | fireEvent(element, 'show-dialog', config);
78 | } else {
79 | if (!(config.dialogTag in LOADED)) {
80 | if (!config.dialogImport) {
81 | return;
82 | }
83 | LOADED[config.dialogTag] = {
84 | element: config.dialogImport().then(() => {
85 | const dialogEl = document.createElement(config.dialogTag) as HassDialog;
86 | element.provideHass(dialogEl);
87 | return dialogEl;
88 | }),
89 | };
90 | }
91 | const dialogElement = await LOADED[config.dialogTag].element;
92 | popupRoot.appendChild(dialogElement);
93 | dialogElement.showDialog(config.dialogParams);
94 | }
95 | };
96 |
--------------------------------------------------------------------------------
/src/data/date-time/day_object.ts:
--------------------------------------------------------------------------------
1 | export function weekday(ts: Date) {
2 | let day = ts.getDay();
3 | if (day == 0) day = 7;
4 | return day;
5 | }
6 |
--------------------------------------------------------------------------------
/src/data/date-time/format_date.ts:
--------------------------------------------------------------------------------
1 | import { format } from 'fecha';
2 | import { FrontendLocaleData } from './format_time';
3 |
4 | export function formatDate(dateObj: Date, locale: FrontendLocaleData, isoFormat?: boolean) {
5 | const isCurrentYear = dateObj.getFullYear() == new Date().getFullYear();
6 |
7 | const supportLocaleDateString = () => {
8 | try {
9 | new Date().toLocaleDateString('i');
10 | } catch (e) {
11 | return e.name === 'RangeError';
12 | }
13 | return false;
14 | };
15 |
16 | if (isoFormat) return format(dateObj, 'isoDate');
17 |
18 | if (isCurrentYear) {
19 | if (supportLocaleDateString()) {
20 | return new Intl.DateTimeFormat(locale.language, {
21 | month: 'long',
22 | day: 'numeric',
23 | }).format(dateObj);
24 | } else return format(dateObj, 'MMMM D');
25 | } else {
26 | if (supportLocaleDateString()) {
27 | return new Intl.DateTimeFormat(locale.language, {
28 | year: 'numeric',
29 | month: 'long',
30 | day: 'numeric',
31 | }).format(dateObj);
32 | } else return format(dateObj, 'MMMM D, YYYY');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/data/date-time/format_time.ts:
--------------------------------------------------------------------------------
1 | import { FrontendTranslationData } from 'custom-card-helpers';
2 | import { format } from 'fecha';
3 |
4 | export enum TimeFormat {
5 | language = 'language',
6 | system = 'system',
7 | am_pm = '12',
8 | twenty_four = '24',
9 | }
10 |
11 | export interface FrontendLocaleData extends FrontendTranslationData {
12 | time_format?: TimeFormat;
13 | }
14 |
15 | export const formatAmPm = (locale: FrontendLocaleData): boolean => {
16 | if (locale.time_format === TimeFormat.language || locale.time_format === TimeFormat.system) {
17 | const testLanguage = locale.time_format === TimeFormat.language ? locale.language : undefined;
18 | const test = new Date().toLocaleString(testLanguage);
19 | return test.includes('AM') || test.includes('PM');
20 | }
21 | return locale.time_format === TimeFormat.am_pm;
22 | };
23 |
24 | export function formatTime(
25 | dateObj: Date,
26 | locale: FrontendLocaleData,
27 | formatOption?: TimeFormat.am_pm | TimeFormat.twenty_four
28 | ) {
29 | const supportLocaleString = () => {
30 | try {
31 | new Date().toLocaleTimeString('i');
32 | } catch (e) {
33 | return (e as any).name === 'RangeError';
34 | }
35 | return false;
36 | };
37 |
38 | if (formatOption === TimeFormat.am_pm || (!formatOption && locale.time_format === TimeFormat.am_pm)) {
39 | return format(dateObj, 'h:mm A'); // '5:30 AM'
40 | }
41 | if (formatOption === TimeFormat.twenty_four || (!formatOption && locale.time_format === TimeFormat.twenty_four)) {
42 | return format(dateObj, 'shortTime'); // '05:30'
43 | }
44 |
45 | if (supportLocaleString()) {
46 | return dateObj.toLocaleTimeString(locale.language, {
47 | hour: 'numeric',
48 | minute: '2-digit',
49 | hour12: formatAmPm(locale),
50 | });
51 | }
52 | return formatAmPm(locale)
53 | ? formatTime(dateObj, locale, TimeFormat.am_pm)
54 | : formatTime(dateObj, locale, TimeFormat.twenty_four);
55 | }
56 |
--------------------------------------------------------------------------------
/src/data/date-time/format_weekday.ts:
--------------------------------------------------------------------------------
1 | import { FrontendTranslationData } from 'custom-card-helpers';
2 |
3 | export const weekdayArray = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
4 |
5 | export const formatWeekday = (date: Date | number, locale: FrontendTranslationData, short?: boolean): string => {
6 | const supportLocaleString = () => {
7 | try {
8 | new Date().toLocaleDateString('i');
9 | } catch (e) {
10 | return e.name === 'RangeError';
11 | }
12 | return false;
13 | };
14 |
15 | if (typeof date !== 'object') {
16 | let _date = new Date(2017, 1, 26);
17 | _date.setDate(_date.getDate() + date);
18 | date = _date;
19 | }
20 |
21 | if (supportLocaleString()) {
22 | return date.toLocaleDateString(locale.language, {
23 | weekday: short ? 'short' : 'long',
24 | });
25 | } else {
26 | const weekday = date.getDay();
27 | return short ? weekdayArray[weekday].substr(0, 3) : weekdayArray[weekday];
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/data/date-time/relative_time.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { ETimeEvent } from '../../types';
3 | import { parseRelativeTime, roundTime, stringToTime, timeToString } from './time';
4 |
5 | export const relToAbsTime = (timeStr: string, hass: HomeAssistant, options: { stepSize?: number } = {}) => {
6 | const res = parseRelativeTime(timeStr);
7 | if (!res) return timeStr;
8 |
9 | const sunEntity = hass.states['sun.sun'];
10 |
11 | const ts_ref =
12 | res.event == 'sunrise'
13 | ? stringToTime(sunEntity.attributes.next_rising, hass)
14 | : stringToTime(sunEntity.attributes.next_setting, hass);
15 |
16 | let ts = res.sign == '+' ? ts_ref + stringToTime(res.offset, hass) : ts_ref - stringToTime(res.offset, hass);
17 |
18 | if (options.stepSize) ts = roundTime(ts, options.stepSize);
19 |
20 | return timeToString(ts);
21 | };
22 |
23 | export const absToRelTime = (
24 | timeStr: string,
25 | event: ETimeEvent | undefined,
26 | hass: HomeAssistant,
27 | options: { stepSize?: number } = {}
28 | ) => {
29 | const res = parseRelativeTime(timeStr);
30 | if (res) timeStr = relToAbsTime(timeStr, hass);
31 |
32 | const ts = stringToTime(timeStr, hass);
33 |
34 | const sunEntity = hass.states['sun.sun'];
35 |
36 | const ts_sunrise = stringToTime(sunEntity.attributes.next_rising, hass);
37 | const ts_sunset = stringToTime(sunEntity.attributes.next_setting, hass);
38 |
39 | if (!event) event = Math.abs(ts - ts_sunrise) < Math.abs(ts - ts_sunset) ? ETimeEvent.Sunrise : ETimeEvent.Sunset;
40 |
41 | const ts_ref =
42 | event == ETimeEvent.Sunrise
43 | ? stringToTime(sunEntity.attributes.next_rising, hass)
44 | : stringToTime(sunEntity.attributes.next_setting, hass);
45 |
46 | let offset = ts - ts_ref;
47 | if (options.stepSize) offset = roundTime(offset, options.stepSize, { maxHours: 6 });
48 | return `${event}${offset > 0 ? '+' : '-'}${timeToString(Math.abs(offset))}`;
49 | };
50 |
--------------------------------------------------------------------------------
/src/data/date-time/start_of_week.ts:
--------------------------------------------------------------------------------
1 | export function startOfWeek(locale) {
2 | const parts = locale.match(
3 | /^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i
4 | );
5 |
6 | const language = parts[1];
7 | const region = parts[4];
8 |
9 | const regionSat = 'AEAFBHDJDZEGIQIRJOKWLYOMQASDSY'.match(/../g)!;
10 | const regionSun = 'AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW'.match(
11 | /../g
12 | )!;
13 | const languageSat = ['ar', 'arq', 'arz', 'fa'];
14 | const languageSun = 'amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu'.match(/../g)!;
15 |
16 | if (region) return regionSun.includes(region) ? 'sun' : regionSat.includes(region) ? 'sat' : 'mon';
17 | else return languageSun.includes(language) ? 'sun' : languageSat.includes(language) ? 'sat' : 'mon';
18 | }
19 |
--------------------------------------------------------------------------------
/src/data/date-time/string_to_date.ts:
--------------------------------------------------------------------------------
1 | export function stringToDate(dateTimeString?: string) {
2 | let date = new Date();
3 |
4 | const dateMatch = (dateTimeString || '').match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})/);
5 | if (dateMatch !== null) date.setFullYear(Number(dateMatch[1]), Number(dateMatch[2]) - 1, Number(dateMatch[3]));
6 |
7 | const timeMatch = (dateTimeString || '').match(/([0-9]{2}):([0-9]{2})(:([0-9]{2}))?$/);
8 | if (timeMatch !== null)
9 | date.setHours(
10 | Number(timeMatch[1]),
11 | Number(timeMatch[2]),
12 | timeMatch.length > 4 ? Number(timeMatch[4]) : date.getSeconds()
13 | );
14 |
15 | return date;
16 | }
17 |
--------------------------------------------------------------------------------
/src/data/date-time/time.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { ETimeEvent } from '../../types';
3 |
4 | export function stringToTime(string: string, hass: HomeAssistant) {
5 | if (string.match(/^([0-9:]+)$/)) {
6 | const parts = string.split(':').map(Number);
7 | return parts[0] * 3600 + parts[1] * 60 + (parts[2] || 0);
8 | }
9 | const res = parseRelativeTime(string);
10 | if (res) {
11 | const sunEntity = hass.states['sun.sun'];
12 | const ts_sunrise = stringToTime(sunEntity.attributes.next_rising, hass);
13 | const ts_sunset = stringToTime(sunEntity.attributes.next_setting, hass);
14 |
15 | let ts = res.event == 'sunrise' ? ts_sunrise : ts_sunset;
16 | ts = res.sign == '+' ? ts + stringToTime(res.offset, hass) : ts - stringToTime(res.offset, hass);
17 | return ts;
18 | }
19 | const ts = new Date(string);
20 | return ts.getHours() * 3600 + ts.getMinutes() * 60 + ts.getSeconds();
21 | }
22 |
23 | export function timeToString(time: number) {
24 | const hours = Math.floor(time / 3600);
25 | time -= hours * 3600;
26 | const minutes = Math.floor(time / 60);
27 | time -= minutes * 60;
28 | const seconds = Math.round(time);
29 | return (
30 | String(hours % 24).padStart(2, '0') +
31 | ':' +
32 | String(minutes).padStart(2, '0') +
33 | ':' +
34 | String(seconds).padStart(2, '0')
35 | );
36 | }
37 |
38 | export function roundTime(
39 | value: number,
40 | stepSize: number,
41 | options: { wrapAround?: boolean; maxHours?: number } = { wrapAround: true }
42 | ) {
43 | let hours = value >= 0 ? Math.floor(value / 3600) : Math.ceil(value / 3600);
44 | let minutes = Math.floor((value - hours * 3600) / 60);
45 |
46 | if (minutes % stepSize != 0) minutes = Math.round(minutes / stepSize) * stepSize;
47 |
48 | if (minutes >= 60) {
49 | hours++;
50 | minutes -= 60;
51 | } else if (minutes < 0) {
52 | hours--;
53 | minutes += 60;
54 | }
55 | if (options.wrapAround) {
56 | if (hours >= 24) hours -= 24;
57 | else if (hours < 0) hours += 24;
58 | }
59 | const time = hours * 3600 + minutes * 60;
60 | if (options.maxHours) {
61 | if (time > options.maxHours * 3600) return options.maxHours * 3600;
62 | if (time < -options.maxHours * 3600) return -options.maxHours * 3600;
63 | }
64 | return time;
65 | }
66 |
67 | export function parseRelativeTime(time: string) {
68 | const match = time.match(/^([a-z]+)([\+|-]{1})([0-9:]+)$/);
69 | if (!match) return false;
70 | return {
71 | event: match[1] as ETimeEvent,
72 | sign: match[2],
73 | offset: match[3],
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/data/date-time/weekday_to_list.ts:
--------------------------------------------------------------------------------
1 | import { WeekdayType } from '../../types';
2 |
3 | export function weekdayToList(weekday: WeekdayType) {
4 | let list = weekday
5 | .map(e => {
6 | switch (e) {
7 | case 'mon':
8 | return 1;
9 | case 'tue':
10 | return 2;
11 | case 'wed':
12 | return 3;
13 | case 'thu':
14 | return 4;
15 | case 'fri':
16 | return 5;
17 | case 'sat':
18 | return 6;
19 | case 'sun':
20 | return 7;
21 | default:
22 | return;
23 | }
24 | })
25 | .filter(e => e) as number[];
26 | return list;
27 | }
28 |
--------------------------------------------------------------------------------
/src/data/date-time/weekday_type.ts:
--------------------------------------------------------------------------------
1 | import { WeekdayType, EDayType } from '../../types';
2 |
3 | export function weekdayType(weekday: WeekdayType) {
4 | if (weekday.includes('daily')) return EDayType.Daily;
5 | if (weekday.includes('workday')) return EDayType.Workday;
6 | if (weekday.includes('weekend')) return EDayType.Weekend;
7 | return EDayType.Custom;
8 | }
9 |
--------------------------------------------------------------------------------
/src/data/entities/compute_entities.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { CardConfig } from '../../types';
3 | import { entityFilter } from './entity_filter';
4 | import { computeActions } from '../actions/compute_actions';
5 | import { computeStates } from '../compute_states';
6 |
7 | export function computeEntities(
8 | hass: HomeAssistant,
9 | config: CardConfig,
10 | options: { filterActions: boolean; filterStates: boolean } = { filterActions: true, filterStates: false }
11 | ) {
12 | let entities = Object.keys(hass.states).filter(e => entityFilter(e, config));
13 |
14 | if ('notify' in hass.services) {
15 | entities = [
16 | ...entities,
17 | ...Object.keys(hass.services['notify'])
18 | .map(e => `notify.${e}`)
19 | .filter(e => entityFilter(e, config)),
20 | ];
21 | }
22 |
23 | if (options.filterActions && options.filterStates)
24 | entities = entities.filter(e => computeActions(e, hass, config).length || computeStates(e, hass, config));
25 | else if (options.filterActions) entities = entities.filter(e => computeActions(e, hass, config).length);
26 | else if (options.filterStates) entities = entities.filter(e => computeStates(e, hass, config));
27 |
28 | return entities;
29 | }
30 |
--------------------------------------------------------------------------------
/src/data/entities/compute_supported_features.ts:
--------------------------------------------------------------------------------
1 | import { HassEntity } from 'home-assistant-js-websocket';
2 | import { computeDomain } from 'custom-card-helpers';
3 | import { unique } from '../../helpers';
4 |
5 | export const colorModesToSupportedFeatures = (colorModes: any) => {
6 | if (!colorModes || !Array.isArray(colorModes)) return 0;
7 | let features: number[] = colorModes.map(mode => {
8 | switch (mode) {
9 | case 'brightness':
10 | case 'color_temp':
11 | case 'hs':
12 | case 'xy':
13 | case 'rgb':
14 | case 'rgbw':
15 | case 'rgbww':
16 | return 1;
17 | case 'unknown':
18 | case 'onoff':
19 | case 'white':
20 | default:
21 | return 0;
22 | }
23 | });
24 | features = unique(features);
25 | return features.reduce((acc, val) => acc | val, 0);
26 | };
27 |
28 | export const computeSupportedFeatures = (stateObj: HassEntity | undefined) => {
29 | if (!stateObj) return 0;
30 | const domain = computeDomain(stateObj.entity_id);
31 |
32 | switch (domain) {
33 | case 'light':
34 | return colorModesToSupportedFeatures(stateObj.attributes.supported_color_modes);
35 | default:
36 | return stateObj.attributes.supported_features || 0;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/data/entities/entity_filter.ts:
--------------------------------------------------------------------------------
1 | import { Dictionary } from '../../types';
2 | import { matchPattern } from '../match_pattern';
3 |
4 | type FilterType = { include?: string[]; exclude?: string[]; customize?: Dictionary; groups?: FilterType[] };
5 |
6 | export function entityFilter(entity_id: string, config: FilterType) {
7 | const applyFilter = (value: string, filter: FilterType) => {
8 | return (
9 | ((filter.include || []).some(e => matchPattern(e, value)) ||
10 | Object.keys(filter.customize || {}).some(e => matchPattern(e, value))) &&
11 | !(filter.exclude || []).some(e => matchPattern(e, value))
12 | );
13 | };
14 | return config.groups?.some(group => applyFilter(entity_id, group)) || applyFilter(entity_id, config);
15 | }
16 |
--------------------------------------------------------------------------------
/src/data/entities/parse_entity.ts:
--------------------------------------------------------------------------------
1 | import { CardConfig, EntityElement } from '../../types';
2 | import { standardIcon } from '../../standard-configuration/standardIcon';
3 | import { matchPattern } from '../match_pattern';
4 | import { pick } from '../../helpers';
5 | import { HomeAssistant, computeEntity, computeDomain } from 'custom-card-helpers';
6 | import { DeadEntityIcon, DeadEntityName, DefaultEntityIcon, NotifyDomain } from '../../const';
7 |
8 | export function parseEntity(entity_id: string, hass: HomeAssistant, config: Partial) {
9 | const stateObj = entity_id in hass.states ? hass.states[entity_id] : undefined;
10 |
11 | let entity: EntityElement = {
12 | id: entity_id,
13 | name: stateObj ? stateObj.attributes.friendly_name || computeEntity(entity_id) : DeadEntityName,
14 | icon: stateObj ? stateObj.attributes.icon : DeadEntityIcon,
15 | };
16 |
17 | if (!stateObj && computeDomain(entity_id) == NotifyDomain) {
18 | let name = computeEntity(entity_id);
19 | let icon = standardIcon(entity_id, hass);
20 | if (name.includes('mobile_app_')) {
21 | name = name.split('mobile_app_').pop()!;
22 | if (`device_tracker.${name}` in hass.states) {
23 | const deviceTracker = hass.states[`device_tracker.${name}`];
24 | name = deviceTracker.attributes.friendly_name || name;
25 | icon = 'hass:cellphone-text';
26 | }
27 | }
28 | entity = {
29 | ...entity,
30 | name: name,
31 | icon: icon,
32 | };
33 | }
34 |
35 | if ((config.standard_configuration === undefined || config.standard_configuration) && !entity.icon) {
36 | entity = { ...entity, icon: standardIcon(entity_id, hass) };
37 | } else if (!entity.icon) {
38 | entity = { ...entity, icon: DefaultEntityIcon };
39 | }
40 |
41 | if (config.customize) {
42 | const customize = Object.entries(config.customize)
43 | .filter(([pattern]) => matchPattern(pattern, entity.id))
44 | .sort((a, b) => b[0].length - a[0].length)
45 | .map(([, v]) => v)
46 | .forEach(el => {
47 | entity = { ...entity, ...pick(el, ['name', 'icon']) };
48 | });
49 | }
50 |
51 | return entity;
52 | }
53 |
--------------------------------------------------------------------------------
/src/data/entity_group.ts:
--------------------------------------------------------------------------------
1 | import { Group, CardConfig } from '../types';
2 | import { DefaultGroupIcon } from '../const';
3 | import { computeDomain, HomeAssistant } from 'custom-card-helpers';
4 | import { domainIcons } from '../standard-configuration/standardIcon';
5 | import { entityFilter } from './entities/entity_filter';
6 | import { standardGroupNames } from '../standard-configuration/group_name';
7 |
8 | export function entityGroups(entities: string[], config: Partial, hass: HomeAssistant) {
9 | let groups: Group[] = [];
10 |
11 | //create groups from user config
12 | if (config.groups) {
13 | config.groups.forEach(el => {
14 | if (!entities.find(e => entityFilter(e, el))) return;
15 | groups = [
16 | ...groups,
17 | {
18 | name: el.name,
19 | icon: el.icon || DefaultGroupIcon,
20 | entities: entities.filter(e => entityFilter(e, el)),
21 | },
22 | ];
23 | });
24 | }
25 |
26 | const ungroupedEntities = entities.filter(e => !groups.some(g => g.entities.includes(e)));
27 | const domains = ungroupedEntities.map(computeDomain).filter((v, k, arr) => arr.indexOf(v) === k);
28 |
29 | //automatically create groups for ungrouped entities
30 | domains.forEach(domain => {
31 | groups = [
32 | ...groups,
33 | {
34 | name: standardGroupNames(domain, hass),
35 | icon:
36 | (config.standard_configuration === undefined || config.standard_configuration) && domain in domainIcons
37 | ? domainIcons[domain]
38 | : DefaultGroupIcon,
39 | entities: ungroupedEntities.filter(e => entityFilter(e, { include: [domain], exclude: [] })),
40 | },
41 | ];
42 | });
43 | return groups;
44 | }
45 |
--------------------------------------------------------------------------------
/src/data/item_display/compute_days_display.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { getLocale } from '../../helpers';
3 | import { localize } from '../../localize/localize';
4 | import { WeekdayType, EDayType } from '../../types';
5 | import { formatWeekday } from '../date-time/format_weekday';
6 | import { weekdayToList } from '../date-time/weekday_to_list';
7 | import { weekdayType } from '../date-time/weekday_type';
8 |
9 | export const computeDaysDisplay = (weekdays: WeekdayType, hass: HomeAssistant) => {
10 | function findSequence(list: number[]): number[] {
11 | const len: number[] = [];
12 | for (let i = 0; i < list.length - 1; i++) {
13 | let j = i + 1;
14 | while (list[j] - list[j - 1] == 1) j++;
15 | len.push(j - i);
16 | }
17 | return len;
18 | }
19 |
20 | if (!hass) return '';
21 | switch (weekdayType(weekdays)) {
22 | case EDayType.Daily:
23 | return localize('ui.components.date.day_types_long.daily', getLocale(hass));
24 | case EDayType.Workday:
25 | return localize('ui.components.date.day_types_long.workdays', getLocale(hass));
26 | case EDayType.Weekend:
27 | return localize('ui.components.date.day_types_long.weekend', getLocale(hass));
28 | case EDayType.Custom:
29 | const list = weekdayToList(weekdays);
30 | const seq = findSequence(list);
31 | const len = Math.max(...seq);
32 | if (list.length == 6) {
33 | const missing = [1, 2, 3, 4, 5, 6, 7].filter(e => !list.includes(e));
34 | return localize(
35 | 'ui.components.date.repeated_days_except',
36 | getLocale(hass),
37 | '{excludedDays}',
38 | formatWeekday(missing.pop()!, getLocale(hass))
39 | );
40 | }
41 | const dayNames = list.map(e => formatWeekday(e, getLocale(hass)));
42 | if (list.length >= 3 && len >= 3) {
43 | const start = seq.reduce((obj, e, i) => (e == len ? i : obj), 0);
44 | dayNames.splice(
45 | start,
46 | len,
47 | localize(
48 | 'ui.components.date.days_range',
49 | getLocale(hass),
50 | ['{startDay}', '{endDay}'],
51 | [dayNames[start], dayNames[start + len - 1]]
52 | )
53 | );
54 | }
55 | const daysString =
56 | dayNames.length > 1
57 | ? `${dayNames.slice(0, -1).join(', ')} ${hass.localize('ui.common.and')} ${dayNames.pop()}`
58 | : `${dayNames.pop()}`;
59 | return localize('ui.components.date.repeated_days', getLocale(hass), '{days}', daysString);
60 | default:
61 | return '';
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/data/item_display/compute_schedule_display.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, HomeAssistant } from 'custom-card-helpers';
2 | import { AsArray, capitalize, getLocale, PrettyPrintName, unique } from '../../helpers';
3 | import { localize } from '../../localize/localize';
4 | import { standardIcon } from '../../standard-configuration/standardIcon';
5 | import { Action, CardConfig, Schedule } from '../../types';
6 | import { compareActions } from '../actions/compare_actions';
7 | import { computeActions } from '../actions/compute_actions';
8 | import { computeActionDisplay } from '../actions/compute_action_display';
9 | import { importAction } from '../actions/import_action';
10 | import { parseEntity } from '../entities/parse_entity';
11 | import { computeDaysDisplay } from './compute_days_display';
12 | import { computeTimeDisplay } from './compute_time_display';
13 |
14 | export const computeScheduleHeader = (schedule: Schedule, config: CardConfig, hass: HomeAssistant) => {
15 | const primaryInfo = AsArray(
16 | !config.display_options || !config.display_options.primary_info
17 | ? '{entity}: {action}'
18 | : config.display_options.primary_info
19 | );
20 | return primaryInfo.map(e => computeItemDisplay(e, schedule, config, hass));
21 | };
22 |
23 | export const computeScheduleInfo = (schedule: Schedule, config: CardConfig, hass: HomeAssistant) => {
24 | const primaryInfo = AsArray(
25 | !config.display_options || !config.display_options.secondary_info
26 | ? ['relative-time', 'additional-tasks']
27 | : config.display_options.secondary_info
28 | );
29 | return primaryInfo.map(e => computeItemDisplay(e, schedule, config, hass));
30 | };
31 |
32 | export const computeScheduleIcon = (schedule: Schedule, config: CardConfig, hass: HomeAssistant) => {
33 | if (config.display_options && config.display_options.icon && config.display_options.icon == 'entity') {
34 | const entities = computeEntities(schedule, config, hass);
35 | return unique(entities.map(e => e.icon)).length == 1 ? entities[0].icon : standardIcon(entities[0].id, hass);
36 | } else {
37 | const action = computeAction(schedule, config, hass);
38 | return action.icon;
39 | }
40 | };
41 |
42 | const computeItemDisplay = (
43 | displayItem: string,
44 | schedule: Schedule,
45 | config: CardConfig,
46 | hass: HomeAssistant
47 | ): string => {
48 | switch (displayItem) {
49 | case 'default':
50 | return (
51 | computeItemDisplay('name', schedule, config, hass) ||
52 | `${computeItemDisplay('entity', schedule, config, hass)}: ${computeItemDisplay(
53 | 'action',
54 | schedule,
55 | config,
56 | hass
57 | )}`
58 | );
59 | case 'name':
60 | return schedule.name || '';
61 | case 'relative-time':
62 | return ``;
63 | case 'additional-tasks':
64 | return schedule.timeslots.length > 1
65 | ? '+' +
66 | localize(
67 | 'ui.panel.overview.additional_tasks',
68 | getLocale(hass),
69 | '{number}',
70 | String(schedule.timeslots.length - 1)
71 | )
72 | : '';
73 | case 'time':
74 | return capitalize(computeTimeDisplay(schedule.timeslots[schedule.next_entries[0]], hass));
75 | case 'days':
76 | return capitalize(computeDaysDisplay(schedule.weekdays, hass));
77 | case 'entity':
78 | const entities = computeEntities(schedule, config, hass);
79 | const entityDomains = unique(entities.map(e => computeDomain(e.id)));
80 |
81 | return entities.length == 1
82 | ? capitalize(PrettyPrintName(entities[0].name || ''))
83 | : entityDomains.length == 1
84 | ? `${entities.length}x ${localize(`domains.${entityDomains[0]}`, getLocale(hass)) || entityDomains[0]}`
85 | : `${entities.length}x entities`;
86 | case 'action':
87 | const action = computeAction(schedule, config, hass);
88 | return capitalize(computeActionDisplay(action));
89 | case 'tags':
90 | return (schedule.tags || []).map(e => `${e}`).join('');
91 | default:
92 | const regex = /\{([^\}]+)\}/;
93 | let res;
94 | while ((res = regex.exec(displayItem))) {
95 | displayItem = displayItem.replace(res[0], String(computeItemDisplay(String(res[1]), schedule, config, hass)));
96 | }
97 | return displayItem;
98 | }
99 | };
100 |
101 | export const computeAction = (schedule: Schedule, config: CardConfig, hass: HomeAssistant) => {
102 | const nextEntry = schedule.timeslots[schedule.next_entries[0]];
103 |
104 | const match = computeActions(nextEntry.actions[0].entity_id || nextEntry.actions[0].service, hass, config)
105 | .filter(e => compareActions(e, nextEntry.actions[0], true))
106 | .reduce((_acc: Action | undefined, e) => e, undefined);
107 |
108 | return match
109 | ? {
110 | ...match,
111 | service_data: {
112 | ...match.service_data,
113 | ...Object.entries(nextEntry.actions[0].service_data || {})
114 | .filter(([k]) => Object.keys(match.variables || {}).includes(k))
115 | .reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {}),
116 | },
117 | }
118 | : importAction(nextEntry.actions[0], hass);
119 | };
120 |
121 | export const computeEntities = (schedule: Schedule, config: CardConfig, hass: HomeAssistant) => {
122 | const nextEntry = schedule.timeslots[schedule.next_entries[0]];
123 | const entities = unique(nextEntry.actions.map(e => e.entity_id || e.service)).map(e => parseEntity(e, hass, config));
124 | return entities;
125 | };
126 |
--------------------------------------------------------------------------------
/src/data/item_display/compute_time_display.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant, TimeFormat } from 'custom-card-helpers';
2 | import { getLocale } from '../../helpers';
3 | import { localize } from '../../localize/localize';
4 | import { ETimeEvent, Timeslot } from '../../types';
5 | import { formatTime } from '../date-time/format_time';
6 | import { stringToDate } from '../date-time/string_to_date';
7 | import { parseRelativeTime, stringToTime } from '../date-time/time';
8 |
9 | export const computeTimeDisplay = (entry: Timeslot, hass: HomeAssistant) => {
10 | const computeRelativeTimeString = (timeString: string) => {
11 | const res = parseRelativeTime(timeString);
12 | if (!res) return timeString;
13 |
14 | const eventString =
15 | res.event == ETimeEvent.Sunrise
16 | ? getLocale(hass).language == 'de'
17 | ? hass.localize('ui.panel.config.automation.editor.conditions.type.sun.sunrise')
18 | : hass.localize('ui.panel.config.automation.editor.conditions.type.sun.sunrise').toLowerCase()
19 | : getLocale(hass).language == 'de'
20 | ? hass.localize('ui.panel.config.automation.editor.conditions.type.sun.sunset')
21 | : hass.localize('ui.panel.config.automation.editor.conditions.type.sun.sunset').toLowerCase();
22 | if (Math.abs(stringToTime(res.offset, hass)) < 5 * 60)
23 | return localize('ui.components.time.at_sun_event', getLocale(hass), '{sunEvent}', eventString);
24 |
25 | const signString =
26 | res.sign == '-'
27 | ? hass
28 | .localize('ui.panel.config.automation.editor.conditions.type.sun.before')
29 | .replace(/[^a-z]/gi, '')
30 | .toLowerCase()
31 | : hass
32 | .localize('ui.panel.config.automation.editor.conditions.type.sun.after')
33 | .replace(/[^a-z]/gi, '')
34 | .toLowerCase();
35 |
36 | const timeStr = formatTime(stringToDate(res.offset), getLocale(hass), TimeFormat.twenty_four);
37 |
38 | return `${timeStr} ${signString} ${eventString}`;
39 | };
40 |
41 | if (!entry.stop) {
42 | const timeString = entry.start;
43 | if (parseRelativeTime(timeString)) return computeRelativeTimeString(timeString);
44 | else {
45 | const time = stringToDate(timeString);
46 | return localize('ui.components.time.absolute', getLocale(hass), '{time}', formatTime(time, getLocale(hass)));
47 | }
48 | } else {
49 | const start = parseRelativeTime(entry.start)
50 | ? computeRelativeTimeString(entry.start)
51 | : formatTime(stringToDate(entry.start), getLocale(hass));
52 | const end = parseRelativeTime(entry.stop)
53 | ? computeRelativeTimeString(entry.stop)
54 | : formatTime(stringToDate(entry.stop), getLocale(hass));
55 | return localize('ui.components.time.interval', getLocale(hass), ['{startTime}', '{endTime}'], [start, end]);
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/src/data/match_pattern.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain } from 'custom-card-helpers';
2 |
3 | export function matchPattern(pattern: string, value: string) {
4 | let res = false;
5 | if (pattern.match(/^[a-z0-9_\.]+$/)) {
6 | res = !pattern.includes('.') && value.includes('.') ? pattern == computeDomain(value) : pattern == value;
7 | } else {
8 | try {
9 | if ((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) {
10 | if (!pattern.startsWith('/')) {
11 | pattern = pattern.replace(/\./g, '.').replace(/\*/g, '.*');
12 | pattern = `/^${pattern}$/`;
13 | }
14 | const regex = new RegExp(pattern.slice(1, -1));
15 | res = regex.test(value);
16 | }
17 | } catch (e) {}
18 | }
19 | return res;
20 | }
21 |
--------------------------------------------------------------------------------
/src/data/variables/compute_merged_variable.ts:
--------------------------------------------------------------------------------
1 | import { LevelVariable, ListVariable, TextVariable, EVariableType } from '../../types';
2 | import { levelVariable } from './level_variable';
3 | import { listVariable } from './list_variable';
4 | import { textVariable } from './text_variable';
5 | import { isDefined, unique } from '../../helpers';
6 | import { computeVariables } from './compute_variables';
7 |
8 | export function computeMergedVariable(
9 | ...variables: Partial[]
10 | ): LevelVariable | ListVariable | TextVariable | undefined {
11 | const types = unique(variables.map(e => e.type).filter(isDefined));
12 | if (!types.length) {
13 | variables = Object.values(computeVariables(Object.assign({}, ...variables))!);
14 | return computeMergedVariable(...variables);
15 | }
16 |
17 | if (types.length > 1) return undefined;
18 |
19 | if (types[0] == EVariableType.Level) {
20 | return levelVariable(...(variables as LevelVariable[]));
21 | } else if (types[0] == EVariableType.List) {
22 | return listVariable(...(variables as ListVariable[]));
23 | } else return textVariable(...(variables as TextVariable[]));
24 | }
25 |
--------------------------------------------------------------------------------
/src/data/variables/compute_variables.ts:
--------------------------------------------------------------------------------
1 | import { LevelVariable, ListVariable, TextVariable, Dictionary, VariableDictionary } from '../../types';
2 | import { listVariable } from './list_variable';
3 | import { levelVariable } from './level_variable';
4 | import { textVariable } from './text_variable';
5 |
6 | export function computeVariables(
7 | variables?: Dictionary>
8 | ): VariableDictionary | undefined {
9 | if (!Object.keys(variables || {}).length) return undefined;
10 |
11 | return Object.entries(variables!)
12 | .map(([field, variable]) => {
13 | if ('options' in variable) {
14 | return [field, listVariable(variable as ListVariable)];
15 | } else if ('min' in variable || 'max' in variable) {
16 | return [field, levelVariable(variable as LevelVariable)];
17 | } else {
18 | return [field, textVariable(variable as TextVariable)];
19 | }
20 | })
21 | .reduce((obj, [key, val]) => (val ? Object.assign(obj, { [key as string]: val }) : obj), {});
22 | }
23 |
--------------------------------------------------------------------------------
/src/data/variables/level_variable.ts:
--------------------------------------------------------------------------------
1 | import { isDefined, unique } from '../../helpers';
2 | import { LevelVariable, EVariableType } from '../../types';
3 |
4 | export function levelVariable(...config: Partial[]) {
5 | //factory function to create LevelVariable from configuration
6 |
7 | const min = config.map(e => e.min).filter(isDefined);
8 | const max = config.map(e => e.max).filter(isDefined);
9 | const step = config.map(e => e.step).filter(isDefined);
10 | const scale_factor = unique(config.map(e => e.scale_factor).filter(isDefined));
11 | const optional = config.map(e => e.optional).filter(isDefined);
12 | const unit = config.map(e => e.unit).filter(isDefined);
13 | const name = config.map(e => e.name).filter(isDefined);
14 |
15 | const stepSize = step.length ? Math.max(...step) : 1;
16 | const round = (val: number) => {
17 | val = Math.round(val / stepSize) * stepSize;
18 | return parseFloat(val.toPrecision(12));
19 | };
20 |
21 | const variable: LevelVariable = {
22 | type: EVariableType.Level,
23 | min: round(min.length ? Math.min(...min) : 0),
24 | max: round(max.length ? Math.max(...max) : 255),
25 | step: stepSize,
26 | scale_factor: scale_factor.length == 1 ? scale_factor[0] : 1,
27 | optional: (optional.length && optional.every(e => e)) || false,
28 | unit: unit.length ? unit.reduce((_acc, val) => val) : '',
29 | name: name.length ? name.reduce((_acc, val) => val) : undefined,
30 | };
31 | return variable;
32 | }
33 |
34 | export function levelVariableDisplay(value: any, variable: LevelVariable): string {
35 | let val = Number(value);
36 | if (isNaN(val)) return '';
37 |
38 | if (variable.scale_factor != 1) {
39 | val = val / variable.scale_factor;
40 | val = Math.round(val / variable.step) * variable.step;
41 | val = parseFloat(val.toPrecision(12));
42 | if (val > variable.max) val = variable.max;
43 | else if (val < variable.min) val = variable.min;
44 | }
45 | return `${val}${variable.unit}`;
46 | }
47 |
--------------------------------------------------------------------------------
/src/data/variables/list_variable.ts:
--------------------------------------------------------------------------------
1 | import { isDefined, flatten } from '../../helpers';
2 | import { ListVariableOption, ListVariable, EVariableType, AtLeast } from '../../types';
3 |
4 | export function listVariable(...config: AtLeast[]) {
5 | //factory function to create ListVariable from configuration
6 |
7 | const commonOptions = config[0].options
8 | .map(e => e.value)
9 | .filter(option => config.map(e => e.options).every(list => list.map(e => e.value).includes(option)));
10 |
11 | const options: ListVariableOption[] = commonOptions.map(val => {
12 | const name = config
13 | .map(e => e.options.find(o => o.value == val))
14 | .filter(isDefined)
15 | .map(e => e.name)
16 | .filter(isDefined);
17 | const icon = config
18 | .map(e => e.options.find(o => o.value == val))
19 | .filter(isDefined)
20 | .map(e => e.icon)
21 | .filter(isDefined);
22 |
23 | let item: ListVariableOption = {
24 | value: val,
25 | name: name.length ? name.reduce((_acc, val) => val) : undefined,
26 | icon: icon.length ? icon.reduce((_acc, val) => val) : undefined,
27 | };
28 | return item;
29 | });
30 |
31 | const name = config.map(e => e.name).filter(isDefined);
32 |
33 | const variable: ListVariable = {
34 | type: EVariableType.List,
35 | name: name.length ? name.reduce((_acc, val) => val) : undefined,
36 | options: options,
37 | };
38 | return variable;
39 | }
40 |
41 | export function listVariableDisplay(value: any, variable: ListVariable): string {
42 | const option = variable.options.find(e => e.value == value);
43 | return option ? option.name || option.value : '';
44 | }
45 |
--------------------------------------------------------------------------------
/src/data/variables/text_variable.ts:
--------------------------------------------------------------------------------
1 | import { isDefined } from '../../helpers';
2 | import { EVariableType, TextVariable } from '../../types';
3 |
4 | export function textVariable(...config: Partial[]) {
5 | //factory function to create ListVariable from configuration
6 |
7 | const name = config.map(e => e.name).filter(isDefined);
8 |
9 | const variable: TextVariable = {
10 | type: EVariableType.Text,
11 | name: name.length ? name.reduce((_acc, val) => val) : undefined,
12 | multiline: config.some(e => e.multiline),
13 | };
14 | return variable;
15 | }
16 |
17 | export function textVariableDisplay(value: any, _variable: TextVariable): string {
18 | return String(value);
19 | }
20 |
--------------------------------------------------------------------------------
/src/data/websockets.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant, fireEvent } from 'custom-card-helpers';
2 | import { Schedule, ScheduleConfig, TagEntry } from '../types';
3 | import { html, TemplateResult } from 'lit';
4 | import { DialogParams } from '../components/generic-dialog';
5 | import { ProvideHassElement, showDialog } from './custom_dialog';
6 |
7 | export const fetchSchedules = (hass: HomeAssistant): Promise =>
8 | hass.callWS({
9 | type: 'scheduler',
10 | });
11 |
12 | export const fetchScheduleItem = (hass: HomeAssistant, schedule_id: string): Promise =>
13 | hass.callWS({
14 | type: 'scheduler/item',
15 | schedule_id: schedule_id,
16 | });
17 |
18 | export const saveSchedule = (hass: HomeAssistant, config: ScheduleConfig): Promise => {
19 | return hass.callApi('POST', 'scheduler/add', config);
20 | };
21 |
22 | export const editSchedule = (
23 | hass: HomeAssistant,
24 | config: ScheduleConfig & { schedule_id: string }
25 | ): Promise => {
26 | return hass.callApi('POST', 'scheduler/edit', config);
27 | };
28 |
29 | export const deleteSchedule = (hass: HomeAssistant, schedule_id: string): Promise => {
30 | return hass.callApi('POST', 'scheduler/remove', { schedule_id: schedule_id });
31 | };
32 |
33 | export const fetchTags = (hass: HomeAssistant): Promise =>
34 | hass.callWS({
35 | type: 'scheduler/tags',
36 | });
37 |
38 | export function showErrorDialog(
39 | target: HTMLElement & ProvideHassElement,
40 | error: string | TemplateResult,
41 | hass: HomeAssistant,
42 | useAlternativeDialog?: boolean
43 | ) {
44 | const params: DialogParams = {
45 | title: hass.localize('state_badge.default.error'),
46 | description: error,
47 | primaryButtonLabel: hass.localize('ui.common.ok'),
48 | confirm: () => { },
49 | cancel: () => { },
50 | };
51 | showDialog(
52 | target,
53 | {
54 | dialogTag: 'generic-dialog',
55 | dialogImport: () => import('../components/generic-dialog'),
56 | dialogParams: params,
57 | },
58 | useAlternativeDialog
59 | );
60 | }
61 |
62 | export function handleError(
63 | err: { body: { message: string }; error: string },
64 | el: HTMLElement & ProvideHassElement,
65 | hass: HomeAssistant,
66 | useAlternativeDialog?: boolean
67 | ) {
68 | const errorMessage = html`
69 | Something went wrong!
70 | ${err.body.message}
71 | ${err.error}
72 | Please report the bug.
73 | `;
74 | showErrorDialog(el, errorMessage, hass, useAlternativeDialog);
75 | }
76 |
--------------------------------------------------------------------------------
/src/load-ha-form.js:
--------------------------------------------------------------------------------
1 |
2 | export const loadHaForm = async () => {
3 | if (customElements.get("ha-checkbox") && customElements.get("ha-slider") && customElements.get("ha-combo-box")) return;
4 |
5 | await customElements.whenDefined("partial-panel-resolver");
6 | const ppr = document.createElement('partial-panel-resolver');
7 | ppr.hass = {
8 | panels: [{
9 | url_path: "tmp",
10 | component_name: "config",
11 | }]
12 | };
13 | ppr._updateRoutes();
14 | await ppr.routerOptions.routes.tmp.load();
15 |
16 | await customElements.whenDefined("ha-panel-config");
17 | const cpr = document.createElement("ha-panel-config");
18 | await cpr.routerOptions.routes.automation.load();
19 | }
--------------------------------------------------------------------------------
/src/localize/languages/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} na {value}",
5 | "action_with_parameter": "{action} s {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "nastavit teplotu[ na {temperature}]",
9 | "set_temperature_hvac_mode_heat": "topení[ na {temperature}]",
10 | "set_temperature_hvac_mode_cool": "chlazení[ na {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "topení/chlazení[ na {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "topení/chlazení[ na {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "automatika[ na {temperature}]",
14 | "set_hvac_mode": "nastavit režim[ na {hvac_mode}]",
15 | "set_preset_mode": "nastavit předvolbu[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "zavřít",
20 | "open_cover": "otevřít",
21 | "set_cover_position": "nastavit polohu[ na {position}]",
22 | "set_cover_tilt_position": "set tilt position[ to {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "nastavit rychlost[ na {speed}]",
26 | "set_direction": "nastavit směr[ na {direction}]",
27 | "oscillate": "nastavit oscilaci[ na {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "nastavit vlhkost[ na {humidity}]",
31 | "set_mode": "nastavit režim[ na {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "nastavit hodnotu[ na {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "vybrat možnost[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "vybrat možnost[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "zapnout[ na {brightness} jas]"
44 | },
45 | "media_player": {
46 | "select_source": "vybrat zdroj[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "spustit"
53 | },
54 | "vacuum": {
55 | "start_pause": "start / pauza"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "nastavit režim[ na {operation_mode}]",
59 | "set_away_mode": "vypnout režim"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "poplašný ovládací panel",
64 | "binary_sensor": "binary sensors",
65 | "climate": "klima",
66 | "cover": "rolety",
67 | "fan": "ventilátory",
68 | "group": "skupiny",
69 | "humidifier": "zvlhčovače",
70 | "input_boolean": "input boolean",
71 | "input_number": "input number",
72 | "input_select": "input select",
73 | "lawn_mower": "lawn mower",
74 | "light": "světla",
75 | "lock": "zámky",
76 | "media_player": "média přehrávače",
77 | "notify": "notification",
78 | "switch": "spínače",
79 | "vacuum": "vysavače",
80 | "water_heater": "ohřívače vody"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "denně",
87 | "workdays": "pracovní dny",
88 | "weekend": "víkendy"
89 | },
90 | "day_types_long": {
91 | "daily": "každý den",
92 | "workdays": "v pracovní dny",
93 | "weekend": "o víkendu"
94 | },
95 | "days": "dnů",
96 | "tomorrow": "zítra",
97 | "repeated_days": "každý {days}",
98 | "repeated_days_except": "každý den kromě {excludedDays}",
99 | "days_range": "od {startDay} do {endDay}",
100 | "next_week_day": "příští {weekday}"
101 | },
102 | "time": {
103 | "absolute": "od {time}",
104 | "interval": "od {startTime} do {endTime}",
105 | "at_midnight": "od půlnoc",
106 | "at_noon": "od poledne",
107 | "at_sun_event": "na {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Dokončete úpravy",
113 | "description": "Plán, který byl změněn, je aktuálně zakázán, měl by být povolen?"
114 | },
115 | "confirm_delete": {
116 | "title": "Odebrat entitu?",
117 | "description": "Opravdu chcete tuto entitu odebrat?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Aktualizovat plán",
121 | "description": "Některá nastavení budou touto změnou ztracena. Chceš pokračovat?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Plánovač",
127 | "new_schedule": "Nový plán",
128 | "default_name": "Plán #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Nejsou žádné položky k zobrazení",
132 | "backend_error": "Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",
133 | "excluded_items": "{number} vyloučeno {if number is 1} položka {else} položek",
134 | "hide_excluded": "skrýt vyloučené položky",
135 | "additional_tasks": "{number} a více {if number is 1} úkol {else} úkolů"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Nejsou definovány žádné skupiny",
139 | "no_group_selected": "Nejprve vyberte skupinu",
140 | "no_entities_for_group": "V této skupině nejsou žádné entity",
141 | "no_entity_selected": "Nejprve vyberte entitu",
142 | "no_actions_for_entity": "Pro tuto entitu neexistují žádné akce",
143 | "make_scheme": "vytvořit schéma",
144 | "multiple": "Multiple"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Nejprve vyberte časový úsek",
148 | "time_scheme": "Schéma",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "je",
153 | "unequal_to": "není",
154 | "all": "Vše",
155 | "any": "žádný",
156 | "no_conditions_defined": "Nejsou definovány žádné podmínky",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "chování po spuštění",
161 | "period": "období"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/et.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} {value} jaoks",
5 | "action_with_parameter": "{action} väärtusega {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "vali temperatuur [{temperature}]",
9 | "set_temperature_hvac_mode_heat": "küte[ @ {temperature}]",
10 | "set_temperature_hvac_mode_cool": "jahutus [ @ {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "küte/jahutus[ @ {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "küte/jahutus[ @ {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "automaatne[ @ {temperature}]",
14 | "set_hvac_mode": "vali režiim [{hvac_mode}]",
15 | "set_preset_mode": "eelseade[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "sulge",
20 | "open_cover": "ava",
21 | "set_cover_position": "sea asendisse[{position}]",
22 | "set_cover_tilt_position": "sea ribide kalle [ asendisse {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "vali kiirus[ @ {speed}]",
26 | "set_direction": "vali suund[ @ {direction}]",
27 | "oscillate": "vali hajutus[ @ {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "sea niiskus[ {humidity}]",
31 | "set_mode": "vali režiim [{mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "vali väärtus[ {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "valik[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "valik[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "lülita sisse[ heledusega {brightness}]"
44 | },
45 | "media_player": {
46 | "select_source": "vali sisend[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "käivita"
53 | },
54 | "vacuum": {
55 | "start_pause": "alusta/ootele"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "vali režiim [{operation_mode}]",
59 | "set_away_mode": "kodust ära"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "valvepaneel",
64 | "binary_sensor": "binary sensors",
65 | "climate": "kliimaseade",
66 | "cover": "aknakatted",
67 | "fan": "ventilaatorid",
68 | "group": "grupid",
69 | "humidifier": "niisutajad",
70 | "input_boolean": "tõeväärtus",
71 | "input_number": "numbriline valik",
72 | "input_select": "valikmenüü",
73 | "lawn_mower": "lawn mower",
74 | "light": "valgustid",
75 | "lock": "lukud",
76 | "media_player": "meediamängijad",
77 | "notify": "notification",
78 | "switch": "lülitid",
79 | "vacuum": "tolmuimejad",
80 | "water_heater": "veeboilerid"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "iga päev",
87 | "workdays": "tööpäevadel",
88 | "weekend": "nädalavahetusel"
89 | },
90 | "day_types_long": {
91 | "daily": "iga päev",
92 | "workdays": "tööpäevadel",
93 | "weekend": "nädalavahetusel"
94 | },
95 | "days": "päeva",
96 | "tomorrow": "homme",
97 | "repeated_days": "iga {days} järel",
98 | "repeated_days_except": "iga päev aga mitte {excludedDays}",
99 | "days_range": "{startDay} kuni {endDay}",
100 | "next_week_day": "järgmisel {weekday}"
101 | },
102 | "time": {
103 | "absolute": "{time}",
104 | "interval": "{startTime} kuni {endTime}",
105 | "at_midnight": "keskööl",
106 | "at_noon": "keskpäeval",
107 | "at_sun_event": "{sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Viige muudatused lõpule",
113 | "description": "Muudetud ajakava on praegu keelatud, kas see peaks olema lubatud?"
114 | },
115 | "confirm_delete": {
116 | "title": "Kas eemaldan olemi?",
117 | "description": "Oled kindel, et soovid selle olemi eemaldada?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Muutke ajakava",
121 | "description": "Selle muudatusega lähevad mõned seaded kaotsi. Kas soovite jätkata?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Ajastaja",
127 | "new_schedule": "Uus ajakava",
128 | "default_name": "Ajakava #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Ajastused puuduvad",
132 | "backend_error": "Ajastaja sidumine puudub. Sidumine tuleb luua enne selle kaardi kasutamist.",
133 | "excluded_items": "välja on jäetud {number} {if number is 1} ajastus {else} ajastust",
134 | "hide_excluded": "peida välja jäetud ajastused",
135 | "additional_tasks": "veel {number} {if number is 1} ajastus {else} ajastust"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Gruppe pole valitud",
139 | "no_group_selected": "Vali alustuseks grupid",
140 | "no_entities_for_group": "Selles grupis puuduvad olemid",
141 | "no_entity_selected": "Vali alustuseks olem",
142 | "no_actions_for_entity": "Selle olemi jaoks pole tegevusi",
143 | "make_scheme": "loo skeem",
144 | "multiple": "Mitu"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Alustuseks vali ajavahemik",
148 | "time_scheme": "Kkeem",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "võrdub",
153 | "unequal_to": "ei võrdu",
154 | "all": "kõik",
155 | "any": "iga",
156 | "no_conditions_defined": "Tingimusi pole määratud",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "toiming peale käivitumist",
161 | "period": "periood"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/he.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} ל {value}",
5 | "action_with_parameter": "{action} עם {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "קבע טמפרטורה[ ל {temperature}]",
9 | "set_temperature_hvac_mode_heat": "חימום[ ל {temperature}]",
10 | "set_temperature_hvac_mode_cool": "קירור[ ל {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "חימום/קירור[ ל {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "חימום/קירור[ ל {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "אוטומטי[ ל {temperature}]",
14 | "set_hvac_mode": "קבע מצב עבודה[ ל {hvac_mode}]",
15 | "set_preset_mode": "קבע הגדרה[ ל {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "סגירה",
20 | "open_cover": "פתיחה",
21 | "set_cover_position": "קבע מיקום[ ל {position}]",
22 | "set_cover_tilt_position": "קבע הטיה[ ל {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "קבע מהירות[ ל {speed}]",
26 | "set_direction": "קבע כיוון[ ל {direction}]",
27 | "oscillate": "קבע תנודה[ ל {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "קבע לחות[ ל {humidity}]",
31 | "set_mode": "קבע מצב עבודה[ ל {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "קבע ערך[ ל {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "בחר אפשרות[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "בחר אפשרות[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "הדלקה[ בעוצמה של {brightness}]"
44 | },
45 | "media_player": {
46 | "select_source": "select source[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "בצע"
53 | },
54 | "vacuum": {
55 | "start_pause": "התחל / הפסק"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "קבע מצב עבודה[ ל {operation_mode}]",
59 | "set_away_mode": "קבע מצב מוץ לבית"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "בקרת אזעקה",
64 | "binary_sensor": "binary sensors",
65 | "climate": "מזג אוויר",
66 | "cover": "תריסים",
67 | "fan": "מאווררים",
68 | "group": "קבוצות יישויות",
69 | "humidifier": "מכשירי אדים",
70 | "input_boolean": "כניסה בוליאנית",
71 | "input_number": "כניסה מספרית",
72 | "input_select": "בחירת כניסה",
73 | "lawn_mower": "lawn mower",
74 | "light": "תאורה",
75 | "lock": "מנעולים",
76 | "media_player": "נגני מדיה",
77 | "notify": "notification",
78 | "switch": "מפסקים",
79 | "vacuum": "שואבי אבק",
80 | "water_heater": "מחממי מים"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "כל יום",
87 | "workdays": "ימי חול",
88 | "weekend": "סוף שבוע"
89 | },
90 | "day_types_long": {
91 | "daily": "כל יום",
92 | "workdays": "בימי חול",
93 | "weekend": "בסוף השבוע"
94 | },
95 | "days": "ימים",
96 | "tomorrow": "מחר",
97 | "repeated_days": "בכל {days}",
98 | "repeated_days_except": "בכל יום פרט ל {excludedDays}",
99 | "days_range": "מ- {startDay} ועד- {endDay}",
100 | "next_week_day": "הבא {weekday}"
101 | },
102 | "time": {
103 | "absolute": "בשעה {time}",
104 | "interval": "משעה {startTime} עד שעה {endTime}",
105 | "at_midnight": "בחצות הלילה",
106 | "at_noon": "בחצות היום",
107 | "at_sun_event": "ב {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "השלם את השינויים",
113 | "description": "לוח הזמנים ששונה מושבת כעת, האם צריך להפעיל אותו?"
114 | },
115 | "confirm_delete": {
116 | "title": "להסיר את הישות?",
117 | "description": "האם בוודאות ברצונך להסיר ישות זו?"
118 | },
119 | "confirm_migrate": {
120 | "title": "שנה את לוח הזמנים",
121 | "description": "חלק מההגדרות יאבדו על ידי פעולה זו. האם אתה רוצה להמשיך?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "לוח זמנים",
127 | "new_schedule": "לוח זמנים חדש",
128 | "default_name": "לוח זמנים #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "אין פריטים להצגה",
132 | "backend_error": "אין אפשרות להתחבר לרכיב התזמונים. נדרש להתקין את הרכיב באינטגרציה לפני השימוש בכרטיס.",
133 | "excluded_items": "{number} לא נכלל {if number is 1} פריט {else} פריטים",
134 | "hide_excluded": "הסתר פריטים לא כלולים",
135 | "additional_tasks": "{number} נוסף {if number is 1} משימה {else} משימות"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "לא הוגדרו קבוצות",
139 | "no_group_selected": "בחר קבוצה תחילה",
140 | "no_entities_for_group": "אין יישויות בקבוצה זו",
141 | "no_entity_selected": "תחילה בחר יישות",
142 | "no_actions_for_entity": "אין פעולות עבור יישות זאת",
143 | "make_scheme": "בנה סכימה",
144 | "multiple": "מספר יישויות"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "בחר משבצת זמן קודם",
148 | "time_scheme": "סכימה",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "שווה ל",
153 | "unequal_to": "שונה מ",
154 | "all": "כל התנאים",
155 | "any": "אחד מהתנאים",
156 | "no_conditions_defined": "לא הוגדרו תנאים",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "התנהגות לאחר הפעלה",
161 | "period": "פרק זמן"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/hu.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} to {value}",
5 | "action_with_parameter": "{action} with {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "hőmérséklet[ to {temperature}]",
9 | "set_temperature_hvac_mode_heat": "melegíteni[ to {temperature}]",
10 | "set_temperature_hvac_mode_cool": "hűtés[ to {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "melegíteni/hűtés[ to {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "melegíteni/hűtés[ to {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "automatikus[ to {temperature}]",
14 | "set_hvac_mode": "mód beállítása[ to {hvac_mode}]",
15 | "set_preset_mode": "preset beállítása[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "zárás",
20 | "open_cover": "nyitás",
21 | "set_cover_position": "változtass pozíciót[ to {position}]",
22 | "set_cover_tilt_position": "set tilt position[ to {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "set speed[ to {speed}]",
26 | "set_direction": "set direction[ to {direction}]",
27 | "oscillate": "set oscillation[ to {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "set humidity[ to {humidity}]",
31 | "set_mode": "mód beállítása[ to {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "érték beállítása[ to {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "opció kiválasztása[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "opció kiválasztása[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "bekapcsolás[ with {brightness} brightness]"
44 | },
45 | "media_player": {
46 | "select_source": "forrás kiválasztása[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "kezdés"
53 | },
54 | "vacuum": {
55 | "start_pause": "start / pause"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "mód beállítása[ to {operation_mode}]",
59 | "set_away_mode": "set away mode"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "alarm control panel",
64 | "binary_sensor": "binary sensors",
65 | "climate": "termosztát",
66 | "cover": "redőny",
67 | "fan": "ventilátor",
68 | "group": "csoportok",
69 | "humidifier": "humifiers",
70 | "input_boolean": "logikai bemenet",
71 | "input_number": "szám bemenet",
72 | "input_select": "legördülő bemenet",
73 | "lawn_mower": "lawn mower",
74 | "light": "lámpa",
75 | "lock": "locks",
76 | "media_player": "lejátszó",
77 | "notify": "notification",
78 | "switch": "kapcsoló",
79 | "vacuum": "pórszívó",
80 | "water_heater": "water heaters"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "minden nap",
87 | "workdays": "munkanapokon",
88 | "weekend": "hétvégén"
89 | },
90 | "day_types_long": {
91 | "daily": "minden nap",
92 | "workdays": "munkanapokon",
93 | "weekend": "hétvégén"
94 | },
95 | "days": "Napokon",
96 | "tomorrow": "tomorrow",
97 | "repeated_days": "every {days}",
98 | "repeated_days_except": "every day except {excludedDays}",
99 | "days_range": "from {startDay} to {endDay}",
100 | "next_week_day": "következő {weekday}"
101 | },
102 | "time": {
103 | "absolute": "{time}-kor",
104 | "interval": "{startTime} - {endTime}",
105 | "at_midnight": "éjfélkor",
106 | "at_noon": "délben",
107 | "at_sun_event": "{sunEvent}kor"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Végezze el a módosításokat",
113 | "description": "A módosított ütemezés jelenleg le van tiltva, engedélyezni kell?"
114 | },
115 | "confirm_delete": {
116 | "title": "Biztos benne, hogy eltávolítja az entitást?",
117 | "description": "Biztos benne, hogy el szeretné távolítani ezt az entitást?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Ütemezés módosítása",
121 | "description": "Ezzel a művelettel bizonyos beállítások elvesznek. Akarod folytatni?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Időzítések",
127 | "new_schedule": "Új ütemezés",
128 | "default_name": "Ütemterv #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Nincs megjeleníthető elem",
132 | "backend_error": "Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",
133 | "excluded_items": "{number} excluded {if number is 1} item {else} items",
134 | "hide_excluded": "hide excluded items",
135 | "additional_tasks": "még {number} feladat"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Nincsenek deffiniált csoportok",
139 | "no_group_selected": "Előbb egy csoportot szükséges választani",
140 | "no_entities_for_group": "Ebben a csoportban nem találhatók entitások",
141 | "no_entity_selected": "Előbb egy entitást szükséges választani",
142 | "no_actions_for_entity": "Ehhez az entitáshoz nem tartoznak műveletek",
143 | "make_scheme": "make scheme",
144 | "multiple": "Multiple"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Select a timeslot first",
148 | "time_scheme": "Scheme",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "is",
153 | "unequal_to": "is not",
154 | "all": "all",
155 | "any": "any",
156 | "no_conditions_defined": "There are no conditions defined",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "behaviour after triggering",
161 | "period": "időszak"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/no.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} til {value}",
5 | "action_with_parameter": "{action} med {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "sett temperatur[ til {temperature}]",
9 | "set_temperature_hvac_mode_heat": "oppvarming[ til {temperature}]",
10 | "set_temperature_hvac_mode_cool": "kjøling[ til {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "oppvarming/kjøling[ til {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "oppvarming/kjøling[ til {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "auto[ til {temperature}]",
14 | "set_hvac_mode": "sett modus[ til {hvac_mode}]",
15 | "set_preset_mode": "sett forhåndsvalg[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "lukk",
20 | "open_cover": "åpne",
21 | "set_cover_position": "sett posisjon[ til {position}]",
22 | "set_cover_tilt_position": "sett vippestilling[ til {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "sett hastighet[ til {speed}]",
26 | "set_direction": "sett retning[ til {direction}]",
27 | "oscillate": "sett svingning[ til {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "sett luftfuktighet[ til {humidity}]",
31 | "set_mode": "sett modus[ til {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "sett verdi[ til {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "velg[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "velg[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "slå på[ med {brightness} lysstyrke]"
44 | },
45 | "media_player": {
46 | "select_source": "velg kilde[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notifikasjon"
50 | },
51 | "script": {
52 | "script": "utfør"
53 | },
54 | "vacuum": {
55 | "start_pause": "start / pause"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "sett modus[ til {operation_mode}]",
59 | "set_away_mode": "sett bortemodus"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "alarmpanel",
64 | "binary_sensor": "binary sensors",
65 | "climate": "klima",
66 | "cover": "solskjerming",
67 | "fan": "vifter",
68 | "group": "grupper",
69 | "humidifier": "luftfuktere",
70 | "input_boolean": "input boolsk",
71 | "input_number": "input nummer",
72 | "input_select": "input valg",
73 | "lawn_mower": "lawn mower",
74 | "light": "lys",
75 | "lock": "låser",
76 | "media_player": "mediaspillere",
77 | "notify": "notification",
78 | "switch": "brytere",
79 | "vacuum": "støvsugere",
80 | "water_heater": "varmtvannsberedere"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "hver dag",
87 | "workdays": "ukedager",
88 | "weekend": "helg"
89 | },
90 | "day_types_long": {
91 | "daily": "hver dag",
92 | "workdays": "ukedager",
93 | "weekend": "helg"
94 | },
95 | "days": "Dager",
96 | "tomorrow": "imorgen",
97 | "repeated_days": "hver {days}",
98 | "repeated_days_except": "hver dag unntatt {excludedDays}",
99 | "days_range": "fra {startDay} til {endDay}",
100 | "next_week_day": "neste {weekday}"
101 | },
102 | "time": {
103 | "absolute": "kl. {time}",
104 | "interval": "fra {startTime} til {endTime}",
105 | "at_midnight": "ved midnatt",
106 | "at_noon": "kl. 12.00",
107 | "at_sun_event": "ved {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Fullfør endringene",
113 | "description": "Tidsplanen som er endret er for øyeblikket deaktivert, bør den være aktivert?"
114 | },
115 | "confirm_delete": {
116 | "title": "Vil du fjerne entiteten?",
117 | "description": "Er du sikker på at du vil fjerne denne entiteten?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Endre tidsplanen",
121 | "description": "Noen innstillinger vil gå tapt ved denne handlingen. Vil du fortsette?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Tidsplan",
127 | "new_schedule": "Ny tidsplan",
128 | "default_name": "Tidsplan #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Det er ingen definerte tidsplaner å vise",
132 | "backend_error": "Kunne ikke koble til tidsplankomponenten. Den må installeres som en integrasjon før dette kortet kan benyttes.",
133 | "excluded_items": "{number} ekskludert {if number is 1} element {else} elementer",
134 | "hide_excluded": "skjul ekskluderte elementer",
135 | "additional_tasks": "{number} flere {if number is 1} oppgaver {else} oppgaver"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Ingen grupper definert",
139 | "no_group_selected": "Velg en gruppe først",
140 | "no_entities_for_group": "Det finnes ingen entiteter i denne gruppen",
141 | "no_entity_selected": "Velg en entitet først",
142 | "no_actions_for_entity": "Det finnes ingen handlinger for denne entiteten",
143 | "make_scheme": "lag tidsplan",
144 | "multiple": "Flere"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Velg tidsluke først",
148 | "time_scheme": "Tidsplan",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "er",
153 | "unequal_to": "er ikke",
154 | "all": "alle",
155 | "any": "any",
156 | "no_conditions_defined": "Ingen vilkår definert",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "oppførsel etter fullføring",
161 | "period": "periode"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} na {value}",
5 | "action_with_parameter": "{action} z {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "ustaw temperaturę[ na {temperature}]",
9 | "set_temperature_hvac_mode_heat": "grzej[ do {temperature}]",
10 | "set_temperature_hvac_mode_cool": "chłodź[ do {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "grzej/chłodź[ do {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "grzej/chłodź[ do {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "automatyczny[ do {temperature}]",
14 | "set_hvac_mode": "ustaw tryb[ na {hvac_mode}]",
15 | "set_preset_mode": "ustaw preset[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "zamknij",
20 | "open_cover": "otwórz",
21 | "set_cover_position": "ustaw pozycję[ na {position}]",
22 | "set_cover_tilt_position": "ustaw pozycję lameli[ na {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "ustaw prędkość[ na {speed}]",
26 | "set_direction": "ustaw kierunek[ na {direction}]",
27 | "oscillate": "ustaw oscylacje[ na {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "ustaw wilgotność[ na {humidity}]",
31 | "set_mode": "ustaw tryb[ na {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "ustaw wartość[ na {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "wybierz opcję[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "wybierz opcję[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "zapal[ z jasnością {brightness}]"
44 | },
45 | "media_player": {
46 | "select_source": "wybierz źródło[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "wykonaj"
53 | },
54 | "vacuum": {
55 | "start_pause": "start / pauza"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "ustaw tryb[ na {operation_mode}]",
59 | "set_away_mode": "ustaw tryb nieobecności"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "panel kontrolny alarmu",
64 | "binary_sensor": "binary sensors",
65 | "climate": "klimatyzacja",
66 | "cover": "rolety",
67 | "fan": "wentylatory",
68 | "group": "grupy",
69 | "humidifier": "nawilżacze",
70 | "input_boolean": "wejście logiczne",
71 | "input_number": "wejście liczbowe",
72 | "input_select": "wybór wejścia",
73 | "lawn_mower": "lawn mower",
74 | "light": "światła",
75 | "lock": "zamki",
76 | "media_player": "odtwarzacze",
77 | "notify": "notification",
78 | "switch": "przełączniki",
79 | "vacuum": "odkurzacze",
80 | "water_heater": "podgrzewacze wody"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "codziennie",
87 | "workdays": "robocze",
88 | "weekend": "weekendy"
89 | },
90 | "day_types_long": {
91 | "daily": "codziennie",
92 | "workdays": "w dni robocze",
93 | "weekend": "podczas weekendu"
94 | },
95 | "days": "dni",
96 | "tomorrow": "jutro",
97 | "repeated_days": "co {days} dni",
98 | "repeated_days_except": "coddziennie z wyjątkiem {excludedDays}",
99 | "days_range": "od {startDay} do {endDay}",
100 | "next_week_day": "następna {weekday}"
101 | },
102 | "time": {
103 | "absolute": "o {time}",
104 | "interval": "od {startTime} do {endTime}",
105 | "at_midnight": "o północ",
106 | "at_noon": "o południe",
107 | "at_sun_event": "o {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Zakończ modyfikacje",
113 | "description": "Zmieniony harmonogram jest obecnie wyłączony, czy powinien być włączony?"
114 | },
115 | "confirm_delete": {
116 | "title": "Usunąć encję?",
117 | "description": "Czy na pewno chcesz usunąć tę encję?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Zmodyfikuj harmonogram",
121 | "description": "Ta czynność spowoduje utratę niektórych ustawień. Czy chcesz kontynuować?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Harmonogram",
127 | "new_schedule": "Nowy harmonogram",
128 | "default_name": "Harmonogram #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Nie ma elementów do pokazania",
132 | "backend_error": "Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",
133 | "excluded_items": "{number} wykluczona {if number is 1} pozycja {else} pozycje",
134 | "hide_excluded": "ukryj wykluczone pozycje",
135 | "additional_tasks": "{number} dodatkowe {if number is 1} zadanie {else} zadań(nia)"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Nie ma zdefiniowanych grup",
139 | "no_group_selected": "Najpierw wybierz grupę",
140 | "no_entities_for_group": "Nie ma encji w tej grupie",
141 | "no_entity_selected": "Najpierw wybierz encję",
142 | "no_actions_for_entity": "Nie ma akcji dla tej encji",
143 | "make_scheme": "stwórz schemat",
144 | "multiple": "Multiple"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Najpierw wybierz przedział czasowy",
148 | "time_scheme": "Schemat",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "jest równe ",
153 | "unequal_to": "nie jest równe",
154 | "all": "wszystkie",
155 | "any": "dowolny",
156 | "no_conditions_defined": "Nie ma zdefiniowanych warunków",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "zachowanie po zakończeniu",
161 | "period": "okres"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} к {value}",
5 | "action_with_parameter": "{action} с {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "установить температуру[ {temperature}]",
9 | "set_temperature_hvac_mode_heat": "обогрев[ {temperature}]",
10 | "set_temperature_hvac_mode_cool": "охлаждение[ {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "обогрев/охлаждение[ {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "обогрев/охлаждение[ {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "aвто[ {temperature}]",
14 | "set_hvac_mode": "установить режим[ {hvac_mode}]",
15 | "set_preset_mode": "выбрать набор настроек[ {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "закрыть",
20 | "open_cover": "открыть",
21 | "set_cover_position": "установить позицию[ {position}]",
22 | "set_cover_tilt_position": "установить наклон[ {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "установить скорость[ {speed}]",
26 | "set_direction": "установить направление[ {direction}]",
27 | "oscillate": "установить колебание[ {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "установить влажность[ {humidity}]",
31 | "set_mode": "установить режим[ {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "установить значение[ в {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "установить опцию[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "установить опцию[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "включить[ с {brightness} яркостью]"
44 | },
45 | "media_player": {
46 | "select_source": "выбрать источник[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "послать сообщение"
50 | },
51 | "script": {
52 | "script": "запустить"
53 | },
54 | "vacuum": {
55 | "start_pause": "старт / пауза"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "установить режим[ {operation_mode}]",
59 | "set_away_mode": "установить режим вне дома"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "панель управления сигнализацией",
64 | "binary_sensor": "binary sensors",
65 | "climate": "климат",
66 | "cover": "жалюзи",
67 | "fan": "вентиляторы",
68 | "group": "группы",
69 | "humidifier": "увлажнители",
70 | "input_boolean": "логические",
71 | "input_number": "числовые",
72 | "input_select": "списки",
73 | "lawn_mower": "lawn mower",
74 | "light": "освещение",
75 | "lock": "замки",
76 | "media_player": "медиа-плееры",
77 | "notify": "notification",
78 | "switch": "розетки",
79 | "vacuum": "пылесосы",
80 | "water_heater": "подогреватели воды"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "ежедневно",
87 | "workdays": "рабочие дни",
88 | "weekend": "выходные"
89 | },
90 | "day_types_long": {
91 | "daily": "каждый день",
92 | "workdays": "по рабочим дням",
93 | "weekend": "в выходные"
94 | },
95 | "days": "дни",
96 | "tomorrow": "завтра",
97 | "repeated_days": "каждый {days}",
98 | "repeated_days_except": "каждый день кроме {excludedDays}",
99 | "days_range": "с {startDay} до {endDay}",
100 | "next_week_day": "в следующую {weekday}"
101 | },
102 | "time": {
103 | "absolute": "в {time}",
104 | "interval": "с {startTime} до {endTime}",
105 | "at_midnight": "в полночь",
106 | "at_noon": "в полдень",
107 | "at_sun_event": "в {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Завершите модификации",
113 | "description": "Расписание, которое было изменено, в настоящее время отключено, следует ли его включить?"
114 | },
115 | "confirm_delete": {
116 | "title": "Удалить объект?",
117 | "description": "Вы уверены, что хотите удалить этот объект?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Изменить расписание",
121 | "description": "При этом некоторые настройки будут потеряны. Вы хотите продолжить?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Планировщик",
127 | "new_schedule": "Новое расписание",
128 | "default_name": "Расписание #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Отсутствуют элементы",
132 | "backend_error": "Нет соединенияс scheduler component. Scheduler component должен быть установлен до применения этой карты.",
133 | "excluded_items": "{number} исключено {if number is 1} элемент {else} элементов",
134 | "hide_excluded": "скрыть исключенные элементы",
135 | "additional_tasks": "{number} больше {if number is 1} задача {else} задач"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Не определены группы",
139 | "no_group_selected": "Выберите группу",
140 | "no_entities_for_group": "Отсутствуют элементы в группе",
141 | "no_entity_selected": "Выберите элемент",
142 | "no_actions_for_entity": "Нет действий для этого элемента",
143 | "make_scheme": "создать схему",
144 | "multiple": "Множественный"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Выберите временной слот",
148 | "time_scheme": "Cхему",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "равно",
153 | "unequal_to": "не равно",
154 | "all": "все",
155 | "any": "любое",
156 | "no_conditions_defined": "Не определены условия",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "поведение после срабатывания",
161 | "period": "период"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/uk.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} до {value}",
5 | "action_with_parameter": "{action} з {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "встановити температуру[ to {temperature}]",
9 | "set_temperature_hvac_mode_heat": "нагрів[ to {temperature}]",
10 | "set_temperature_hvac_mode_cool": "охолодження[ to {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "нагрів/охолодження[ to {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "нагрів/охолодження[ to {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "aвтоматичний[ to {temperature}]",
14 | "set_hvac_mode": "встановити режим[ to {hvac_mode}]",
15 | "set_preset_mode": "вибрати пресет[ to {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "закрити",
20 | "open_cover": "відкрити",
21 | "set_cover_position": "встановити позицію[ to {position}]",
22 | "set_cover_tilt_position": "set tilt position[ to {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "встановити швидкість[ to {speed}]",
26 | "set_direction": "встановити напрямок[ to {direction}]",
27 | "oscillate": "встановити коливання[ to {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "встановити вологість[ to {humidity}]",
31 | "set_mode": "встановити режим[ to {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "встановити значення[ to {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "встановити опцію[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "встановити опцію[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "увімкнути[ з {brightness} якскравістю]"
44 | },
45 | "media_player": {
46 | "select_source": "вибрати джерело[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "send notification"
50 | },
51 | "script": {
52 | "script": "виконати"
53 | },
54 | "vacuum": {
55 | "start_pause": "старт / пауза"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "встановити режим[ to {operation_mode}]",
59 | "set_away_mode": "встановити режим Не вдома"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "панель керування сигналізацією",
64 | "binary_sensor": "binary sensors",
65 | "climate": "клімат",
66 | "cover": "жалюзі",
67 | "fan": "вентилятори",
68 | "group": "групи",
69 | "humidifier": "зволожувачі",
70 | "input_boolean": "логічні",
71 | "input_number": "числові",
72 | "input_select": "списки",
73 | "lawn_mower": "lawn mower",
74 | "light": "освітлення",
75 | "lock": "замки",
76 | "media_player": "медіаплеєри",
77 | "notify": "notification",
78 | "switch": "вимикачі",
79 | "vacuum": "пилососи",
80 | "water_heater": "водонагрівачі"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "щоденно",
87 | "workdays": "робочі дні",
88 | "weekend": "вихідні"
89 | },
90 | "day_types_long": {
91 | "daily": "кожного дня",
92 | "workdays": "в робочі дні",
93 | "weekend": "по вихідних"
94 | },
95 | "days": "дні",
96 | "tomorrow": "завтра",
97 | "repeated_days": "кожні {days}",
98 | "repeated_days_except": "кожного дня окрім {excludedDays}",
99 | "days_range": "з {startDay} до {endDay}",
100 | "next_week_day": "наступної {weekday}"
101 | },
102 | "time": {
103 | "absolute": "о {time}",
104 | "interval": "з {startTime} до {endTime}",
105 | "at_midnight": "опівночі",
106 | "at_noon": "опівдні",
107 | "at_sun_event": "о {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "Завершіть зміни",
113 | "description": "Розклад, який було змінено, наразі вимкнено, чи потрібно його ввімкнути?"
114 | },
115 | "confirm_delete": {
116 | "title": "Видалити сутність?",
117 | "description": "Ви дійсно бажаєте видалити цю сутність?"
118 | },
119 | "confirm_migrate": {
120 | "title": "Змінити розклад",
121 | "description": "У результаті цієї дії деякі налаштування буде втрачено. Ви бажаєте продовжити?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "Планувальник",
127 | "new_schedule": "Новий розклад",
128 | "default_name": "Розклад #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "Елементи відсутні",
132 | "backend_error": "Не вдалося підключитися до компонента планувальника. Перш ніж використовувати цю карту, її потрібно встановити як інтеграцію.",
133 | "excluded_items": "{number} виключено {if number is 1} елемент {else} елементів",
134 | "hide_excluded": "сховати виключені елементи",
135 | "additional_tasks": "{number} більше {if number is 1} завдання {else} завдань"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "Немає визначених груп",
139 | "no_group_selected": "Спершу виберіть групу",
140 | "no_entities_for_group": "В даній групі відсутні елементи",
141 | "no_entity_selected": "Спершу виберіть елемент",
142 | "no_actions_for_entity": "Немає дій для цього елемента",
143 | "make_scheme": "створити схему",
144 | "multiple": "Декілька"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "Спершу виберіть часовий проміжок",
148 | "time_scheme": "Cхему",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "дорівнює",
153 | "unequal_to": "не рівне",
154 | "all": "всі",
155 | "any": "будь-яке",
156 | "no_conditions_defined": "Не визначені умови",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "поведінка після спрацювання",
161 | "period": "період"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/languages/zh-Hans.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "generic": {
4 | "parameter_to_value": "{parameter} 至 {value}",
5 | "action_with_parameter": "{action} 使用 {parameter}"
6 | },
7 | "climate": {
8 | "set_temperature": "设定温度[ 至 {temperature}]",
9 | "set_temperature_hvac_mode_heat": "制热模式[ 至 {temperature}]",
10 | "set_temperature_hvac_mode_cool": "制冷模式[ 至 {temperature}]",
11 | "set_temperature_hvac_mode_heat_cool": "制热模式/制冷模式[ 至 {temperature}]",
12 | "set_temperature_hvac_mode_heat_cool_range": "制热模式/制冷模式[ 至 {target_temp_low} - {target_temp_high}]",
13 | "set_temperature_hvac_mode_auto": "自动[ 至 {temperature}]",
14 | "set_hvac_mode": "设定模式[ 为 {hvac_mode}]",
15 | "set_preset_mode": "设定预设模式[ 为 {preset_mode}]",
16 | "set_fan_mode": "set fan mode[ to {fan_mode}]"
17 | },
18 | "cover": {
19 | "close_cover": "关闭",
20 | "open_cover": "打开",
21 | "set_cover_position": "设定位置[ 至 {position}]",
22 | "set_cover_tilt_position": "设定倾斜位置[ 至 {tilt_position}]"
23 | },
24 | "fan": {
25 | "set_speed": "设定风速[ 至 {speed}]",
26 | "set_direction": "设定方向[ 至 {direction}]",
27 | "oscillate": "设置摇摆[ 至 {oscillate}]"
28 | },
29 | "humidifier": {
30 | "set_humidity": "设定湿度[ 至 {humidity}]",
31 | "set_mode": "设定模式[ 为 {mode}]"
32 | },
33 | "input_number": {
34 | "set_value": "设定数值[ 至 {value}]"
35 | },
36 | "input_select": {
37 | "select_option": "选择选项[ {option}]"
38 | },
39 | "select": {
40 | "select_option": "选择选项[ {option}]"
41 | },
42 | "light": {
43 | "turn_on": "打开[ 并设定亮度为 {brightness}]"
44 | },
45 | "media_player": {
46 | "select_source": "选择播放源[ {source}]"
47 | },
48 | "notify": {
49 | "notify": "发送通知"
50 | },
51 | "script": {
52 | "script": "执行"
53 | },
54 | "vacuum": {
55 | "start_pause": "开始 / 暂停"
56 | },
57 | "water_heater": {
58 | "set_operation_mode": "设定模式[ 为 {operation_mode}]",
59 | "set_away_mode": "设定离开模式"
60 | }
61 | },
62 | "domains": {
63 | "alarm_control_panel": "警戒控制面板",
64 | "binary_sensor": "binary sensors",
65 | "climate": "空调/地暖",
66 | "cover": "窗帘",
67 | "fan": "风扇/空气净化器",
68 | "group": "实体组",
69 | "humidifier": "空气加湿器",
70 | "input_boolean": "输入二元选择器",
71 | "input_number": "输入数值",
72 | "input_select": "输入选择",
73 | "lawn_mower": "lawn mower",
74 | "light": "灯具",
75 | "lock": "门锁",
76 | "media_player": "媒体播放器",
77 | "notify": "notification",
78 | "switch": "开关",
79 | "vacuum": "扫地机/吸尘器",
80 | "water_heater": "热水器"
81 | },
82 | "ui": {
83 | "components": {
84 | "date": {
85 | "day_types_short": {
86 | "daily": "每日",
87 | "workdays": "工作日",
88 | "weekend": "周末"
89 | },
90 | "day_types_long": {
91 | "daily": "每一天",
92 | "workdays": "在工作日",
93 | "weekend": "在周末"
94 | },
95 | "days": "天",
96 | "tomorrow": "明天",
97 | "repeated_days": "每 {days}",
98 | "repeated_days_except": "每天,除了 {excludedDays}",
99 | "days_range": "从 {startDay} 至 {endDay}",
100 | "next_week_day": "下{weekday}"
101 | },
102 | "time": {
103 | "absolute": "在 {time}",
104 | "interval": "从 {startTime} 至 {endTime}",
105 | "at_midnight": "在午夜",
106 | "at_noon": "在正午",
107 | "at_sun_event": "在 {sunEvent}"
108 | }
109 | },
110 | "dialog": {
111 | "enable_schedule": {
112 | "title": "完成修改",
113 | "description": "已更改的计划当前已禁用,是否应该启用?"
114 | },
115 | "confirm_delete": {
116 | "title": "是否删除实体?",
117 | "description": "您确定要删除此实体吗?"
118 | },
119 | "confirm_migrate": {
120 | "title": "修改时间表",
121 | "description": "此操作将丢失某些设置。 你想继续吗?"
122 | }
123 | },
124 | "panel": {
125 | "common": {
126 | "title": "计划任务",
127 | "new_schedule": "新时间表",
128 | "default_name": "日程 #{id}"
129 | },
130 | "overview": {
131 | "no_entries": "无事项",
132 | "backend_error": "计划任务组件关联失败。本卡片使用前,需先安装计划任务组件和集成.",
133 | "excluded_items": "{number} 除外 {if number is 1} 事项 {else} 事项",
134 | "hide_excluded": "隐藏除外事项",
135 | "additional_tasks": "{number} 更多 {if number is 1} 任务 {else} 任务"
136 | },
137 | "entity_picker": {
138 | "no_groups_defined": "未添加需执行计划任务的群组",
139 | "no_group_selected": "请选择群组",
140 | "no_entities_for_group": "群组不含实体",
141 | "no_entity_selected": "请选择实体",
142 | "no_actions_for_entity": "该实体不含可执行的动作",
143 | "make_scheme": "制定计划",
144 | "multiple": "多选"
145 | },
146 | "time_picker": {
147 | "no_timeslot_selected": "请选择时间段",
148 | "time_scheme": "议程",
149 | "time_input_mode": "Time control mode"
150 | },
151 | "conditions": {
152 | "equal_to": "是",
153 | "unequal_to": "非",
154 | "all": "全部",
155 | "any": "任一",
156 | "no_conditions_defined": "未定义条件",
157 | "track_conditions": "Re-evaluate when conditions change"
158 | },
159 | "options": {
160 | "repeat_type": "触发后的行为",
161 | "period": "时期"
162 | }
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/src/localize/localize.ts:
--------------------------------------------------------------------------------
1 | import * as cs from './languages/cs.json';
2 | import * as de from './languages/de.json';
3 | import * as en from './languages/en.json';
4 | import * as es from './languages/es.json';
5 | import * as et from './languages/et.json';
6 | import * as fi from './languages/fi.json';
7 | import * as fr from './languages/fr.json';
8 | import * as he from './languages/he.json';
9 | import * as hu from './languages/hu.json';
10 | import * as it from './languages/it.json';
11 | import * as lv from './languages/lv.json';
12 | import * as nl from './languages/nl.json';
13 | import * as no from './languages/no.json';
14 | import * as pl from './languages/pl.json';
15 | import * as pt from './languages/pt.json';
16 | import * as pt_br from './languages/pt-BR.json';
17 | import * as ro from './languages/ro.json';
18 | import * as ru from './languages/ru.json';
19 | import * as sk from './languages/sk.json';
20 | import * as sl from './languages/sl.json';
21 | import * as uk from './languages/uk.json';
22 | import * as zh_Hans from './languages/zh-Hans.json';
23 | import { FrontendTranslationData } from 'custom-card-helpers';
24 |
25 | const languages: any = {
26 | cs: cs,
27 | de: de,
28 | en: en,
29 | es: es,
30 | et: et,
31 | es_419: es,
32 | fi: fi,
33 | fr: fr,
34 | he: he,
35 | hu: hu,
36 | it: it,
37 | lv: lv,
38 | nb: no,
39 | nl: nl,
40 | nn: no,
41 | no: no,
42 | pl: pl,
43 | pt: pt,
44 | 'pt-BR': pt_br,
45 | ro: ro,
46 | sk: sk,
47 | sl: sl,
48 | ru: ru,
49 | uk: uk,
50 | 'zh-Hans': zh_Hans,
51 | };
52 |
53 | export function localize(
54 | string: string,
55 | locale: FrontendTranslationData,
56 | search: string | (string | number)[] | number = '',
57 | replace: string | (string | number)[] | number = ''
58 | ) {
59 | let translated: string;
60 | try {
61 | if (locale.language == 'test') return 'TRANSLATED';
62 | translated = string.split('.').reduce((o, i) => o[i], languages[locale.language]);
63 | if (!translated) translated = string.split('.').reduce((o, i) => o[i], languages['en']);
64 | } catch (e) {
65 | try {
66 | translated = string.split('.').reduce((o, i) => o[i], languages['en']);
67 | } catch (e) {
68 | translated = '';
69 | }
70 | }
71 |
72 | if (search !== '' && replace !== '' && translated) {
73 | if (!Array.isArray(search)) search = [search];
74 | if (!Array.isArray(replace)) replace = [replace];
75 | for (let i = 0; i < (search as string[]).length; i++) {
76 | translated = translated.replace(String(search[i]), String(replace[i]));
77 | const res = translated.match(/\{if ([a-z]+) is ([^\}]+)\}\ ?([^\{]+)\ ?\{else\}\ ?([^\{]+)/i);
78 | if (res && String(search[i]).replace(/[\{\}']+/g, '') == res[1]) {
79 | const is_match = String(replace[i]) == res[2];
80 | if (is_match) translated = translated.replace(res[0], res[3]);
81 | else translated = translated.replace(res[0], res[4]);
82 | }
83 | }
84 | }
85 |
86 | // if (!translated) {
87 | // console.log(`missing translation for ${string}`);
88 | // }
89 | return translated;
90 | }
91 |
--------------------------------------------------------------------------------
/src/standard-configuration/action_icons.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeEntity } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { DefaultActionIcon } from '../const';
4 |
5 | type IconItem = string | ((action: string, stateObj?: HassEntity) => string);
6 |
7 | type IconList = Record>;
8 |
9 | const coverIcon = (action: string, stateObj?: HassEntity) => {
10 | const closedState = action == 'close';
11 | switch (stateObj?.attributes.device_class) {
12 | case 'garage':
13 | return closedState ? 'mdi:garage' : 'mdi:garage-open';
14 | case 'door':
15 | return closedState ? 'mdi:door-closed' : 'mdi:door-open';
16 | case 'blind':
17 | return closedState ? 'mdi:blinds' : 'mdi:blinds-open';
18 | case 'window':
19 | return closedState ? 'mdi:window-closed' : 'mdi:window-open';
20 | default:
21 | return closedState ? 'mdi:window-shutter' : 'mdi:window-shutter-open';
22 | }
23 | };
24 |
25 | const actionIcons: IconList = {
26 | alarm_control_panel: {
27 | alarm_disarm: 'mdi:lock-open-variant-outline',
28 | alarm_arm_home: 'mdi:home-outline',
29 | alarm_arm_away: 'mdi:exit-run',
30 | alarm_arm_night: 'mdi:power-sleep',
31 | alarm_arm_custom_bypass: 'mdi:shield-lock-outline',
32 | arm_vacation: 'mdi:shield-airplane',
33 | },
34 | automation: {
35 | turn_on: 'mdi:power',
36 | turn_off: 'mdi:power-off',
37 | trigger: 'mdi:play',
38 | },
39 | button: {
40 | press: 'mdi:gesture-tap-button',
41 | },
42 | climate: {
43 | turn_off: 'mdi:power-off',
44 | heat: 'mdi:fire',
45 | cool: 'mdi:snowflake',
46 | heat_cool: 'mdi:thermometer',
47 | heat_cool_range: 'mdi:thermometer',
48 | set_temperature: 'mdi:thermometer',
49 | auto: 'mdi:autorenew',
50 | set_mode: 'mdi:cog-transfer-outline',
51 | set_preset: 'mdi:cloud-download-outline',
52 | set_fan_mode: 'mdi:fan',
53 | },
54 | cover: {
55 | close: coverIcon,
56 | open: coverIcon,
57 | set_position: 'mdi:ray-vertex',
58 | set_tilt_position: 'mdi:valve',
59 | },
60 | fan: {
61 | turn_on: 'mdi:power',
62 | turn_off: 'mdi:power-off',
63 | set_percentage: 'mdi:weather-windy',
64 | set_oscillation: 'mdi:arrow-left-right',
65 | set_direction: 'mdi:cog-clockwise',
66 | set_preset_mode: 'mdi:cloud-download-outline',
67 | },
68 | humidifier: {
69 | turn_on: 'mdi:power',
70 | turn_off: 'mdi:power-off',
71 | set_humidity: 'mdi:water-percent',
72 | set_mode: 'mdi:cog-transfer-outline',
73 | },
74 | input_boolean: {
75 | turn_on: 'mdi:flash',
76 | turn_off: 'mdi:flash-off',
77 | },
78 | input_button: {
79 | press: 'mdi:gesture-tap-button',
80 | },
81 | input_number: {
82 | set_value: 'mdi:counter',
83 | },
84 | input_select: {
85 | select_option: 'mdi:counter',
86 | },
87 | lawn_mower: {
88 | start_mowing: 'mdi:play',
89 | pause: 'mdi:pause',
90 | dock: 'mdi:home-import-outline'
91 | },
92 | light: {
93 | turn_on: 'mdi:lightbulb',
94 | turn_off: 'mdi:lightbulb-off',
95 | },
96 | lock: {
97 | lock: 'mdi:lock-outline',
98 | unlock: 'mdi:lock-open-variant-outline',
99 | },
100 | media_player: {
101 | turn_on: 'mdi:power',
102 | turn_off: 'mdi:power-off',
103 | select_source: 'mdi:music-box-multiple-outline',
104 | },
105 | notify: {
106 | '{entity_id}': 'mdi:message-alert',
107 | },
108 | number: {
109 | set_value: 'mdi:counter',
110 | },
111 | scene: {
112 | turn_on: 'mdi:play',
113 | },
114 | script: {
115 | turn_on: 'mdi:flash',
116 | turn_off: 'mdi:flash-off',
117 | '{entity_id}': 'mdi:play',
118 | },
119 | select: {
120 | select_option: 'mdi:counter',
121 | },
122 | switch: {
123 | turn_on: 'mdi:flash',
124 | turn_off: 'mdi:flash-off',
125 | },
126 | vacuum: {
127 | turn_on: 'mdi:power',
128 | start: 'mdi:play-circle-outline',
129 | play_pause: 'mdi:play-circule-outline',
130 | },
131 | water_heater: {
132 | set_temperature: 'mdi:thermometer',
133 | set_mode: 'mdi:cog-transfer-outline',
134 | set_away_mode: 'mdi:car-traction-control',
135 | },
136 | };
137 |
138 | export const actionIcon = (domain: string, action: string, stateObj: HassEntity | undefined): string | undefined => {
139 | if (domain in actionIcons && action in actionIcons[domain]) {
140 | let item = actionIcons[domain][action];
141 | if (item instanceof Function) {
142 | item = item(action, stateObj);
143 | }
144 | return item;
145 | }
146 | return DefaultActionIcon;
147 | };
148 |
--------------------------------------------------------------------------------
/src/standard-configuration/action_name.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeEntity, HomeAssistant } from 'custom-card-helpers';
2 | import { getLocale } from '../helpers';
3 | import { localize } from '../localize/localize';
4 |
5 | type actionNameTemplate = (action: string) => string;
6 |
7 | const actionNamesList: Record> = {
8 | alarm_control_panel: {
9 | alarm_disarm: 'ui.card.alarm_control_panel.disarm',
10 | alarm_arm_home: 'ui.card.alarm_control_panel.arm_home',
11 | alarm_arm_away: 'ui.card.alarm_control_panel.arm_away',
12 | alarm_arm_night: 'ui.card.alarm_control_panel.arm_night',
13 | alarm_arm_custom_bypass: 'ui.card.alarm_control_panel.arm_custom_bypass',
14 | alarm_arm_vacation: 'ui.card.alarm_control_panel.arm_vacation',
15 | },
16 | automation: {
17 | turn_on: 'ui.card.vacuum.actions.turn_on',
18 | turn_off: 'ui.card.vacuum.actions.turn_off',
19 | trigger: 'ui.card.script.run',
20 | },
21 | button: {
22 | press: 'ui.card.button.press',
23 | },
24 | climate: {
25 | turn_off: 'ui.card.vacuum.actions.turn_off',
26 | heat: 'services.climate.set_temperature_hvac_mode_heat',
27 | cool: 'services.climate.set_temperature_hvac_mode_cool',
28 | heat_cool: 'services.climate.set_temperature_hvac_mode_heat_cool',
29 | heat_cool_range: 'services.climate.set_temperature_hvac_mode_heat_cool_range',
30 | auto: 'services.climate.set_temperature_hvac_mode_auto',
31 | set_temperature: 'services.climate.set_temperature',
32 | set_mode: 'services.climate.set_hvac_mode',
33 | set_preset: 'services.climate.set_preset_mode',
34 | set_fan_mode: 'services.climate.set_fan_mode',
35 | },
36 | cover: {
37 | close: 'services.cover.close_cover',
38 | open: 'services.cover.open_cover',
39 | set_position: 'services.cover.set_cover_position',
40 | set_tilt: 'services.cover.set_cover_tilt_position',
41 | },
42 | fan: {
43 | turn_on: 'ui.card.vacuum.actions.turn_on',
44 | turn_off: 'ui.card.vacuum.actions.turn_off',
45 | set_speed: 'services.fan.set_speed',
46 | set_oscillation: 'services.fan.oscillate',
47 | set_direction: 'services.fan.set_direction',
48 | set_preset: 'services.climate.set_preset_mode',
49 | },
50 | humidifier: {
51 | turn_on: 'ui.card.vacuum.actions.turn_on',
52 | turn_off: 'ui.card.vacuum.actions.turn_off',
53 | set_humidity: 'services.humidifier.set_humidity',
54 | set_mode: 'services.humidifier.set_mode',
55 | },
56 | input_boolean: {
57 | turn_on: 'ui.card.vacuum.actions.turn_on',
58 | turn_off: 'ui.card.vacuum.actions.turn_off',
59 | },
60 | input_button: {
61 | press: 'ui.card.button.press',
62 | },
63 | input_number: {
64 | set_value: 'services.input_number.set_value',
65 | },
66 | input_select: {
67 | select_option: 'services.input_select.select_option',
68 | },
69 | lawn_mower: {
70 | start_mowing: 'ui.card.lawn_mower.actions.start_mowing',
71 | pause: 'ui.card.timer.actions.pause',
72 | dock: 'ui.card.lawn_mower.actions.dock'
73 | },
74 | light: {
75 | turn_on: 'ui.card.vacuum.actions.turn_on',
76 | turn_off: 'ui.card.vacuum.actions.turn_off',
77 | },
78 | lock: {
79 | lock: 'ui.card.lock.lock',
80 | unlock: 'ui.card.lock.unlock',
81 | },
82 | media_player: {
83 | turn_on: 'ui.card.vacuum.actions.turn_on',
84 | turn_off: 'ui.card.vacuum.actions.turn_off',
85 | select_source: 'services.media_player.select_source',
86 | },
87 | notify: {
88 | '{entity_id}': 'services.notify.notify',
89 | },
90 | number: {
91 | set_value: 'services.input_number.set_value',
92 | },
93 | scene: {
94 | turn_on: 'ui.card.vacuum.actions.turn_on',
95 | },
96 | script: {
97 | turn_on: 'ui.card.vacuum.actions.turn_on',
98 | turn_off: 'ui.card.vacuum.actions.turn_off',
99 | '{entity_id}': 'services.script.script',
100 | },
101 | select: {
102 | select_option: 'services.input_select.select_option',
103 | },
104 | switch: {
105 | turn_on: 'ui.card.vacuum.actions.turn_on',
106 | turn_off: 'ui.card.vacuum.actions.turn_off',
107 | },
108 | vacuum: {
109 | turn_on: 'ui.card.vacuum.actions.turn_on',
110 | start: 'ui.card.vacuum.start_cleaning',
111 | play_pause: 'services.vacuum.start_pause',
112 | },
113 | water_heater: {
114 | set_temperature: 'services.climate.set_temperature',
115 | set_mode: 'services.water_heater.set_operation_mode',
116 | set_away_mode: 'services.water_heater.set_away_mode',
117 | },
118 | };
119 |
120 | export const actionName = (domain: string, action: string, hass: HomeAssistant) => {
121 | if (domain in actionNamesList && action in actionNamesList[domain]) {
122 | let item = actionNamesList[domain][action];
123 | if (item instanceof Function) {
124 | item = item(action);
125 | }
126 | return item.startsWith('services') ? localize(item, getLocale(hass)) : hass.localize(item);
127 | }
128 | return action;
129 | };
130 |
--------------------------------------------------------------------------------
/src/standard-configuration/attribute.ts:
--------------------------------------------------------------------------------
1 | import { HassEntity } from 'home-assistant-js-websocket';
2 | import { isDefined } from '../helpers';
3 |
4 | export const numericAttribute = (stateObj: HassEntity | undefined, attribute: string | number, fallback?: number) => {
5 | if (typeof attribute == 'number') return attribute;
6 | if (!isDefined(stateObj) || !isDefined(stateObj.attributes[attribute])) return fallback;
7 |
8 | const val = stateObj.attributes[attribute];
9 | if (typeof val == 'number') return val;
10 | return fallback;
11 | };
12 |
13 | export const listAttribute = (stateObj: HassEntity | undefined, attribute: string, initializer: string[] = []) => {
14 | if (!isDefined(stateObj) || !isDefined(stateObj.attributes[attribute])) return initializer;
15 |
16 | const val = stateObj.attributes[attribute];
17 | if (Array.isArray(val)) return val.map(e => String(e));
18 | return initializer;
19 | };
20 |
21 | export const stringAttribute = (stateObj: HassEntity | undefined, attribute: string, initializer = '') => {
22 | if (!isDefined(stateObj) || !isDefined(stateObj.attributes[attribute])) return initializer;
23 |
24 | const val = stateObj.attributes[attribute];
25 | if (typeof val == 'string') return val;
26 | return initializer;
27 | };
28 |
--------------------------------------------------------------------------------
/src/standard-configuration/group.ts:
--------------------------------------------------------------------------------
1 | import { Action, Variable, EVariableType, ListVariable, LevelVariable } from '../types';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { computeDomain, computeEntity, HomeAssistant } from 'custom-card-helpers';
4 | import { listVariable } from '../data/variables/list_variable';
5 | import { levelVariable } from '../data/variables/level_variable';
6 | import { computeCommonActions } from '../data/actions/compute_common_actions';
7 | import { omit } from '../helpers';
8 | import { computeSupportedFeatures } from '../data/entities/compute_supported_features';
9 |
10 | export function groupActions(hass: HomeAssistant, entity: HassEntity, entityActions: Action[][]) {
11 | const entities: string[] =
12 | entity && entity.attributes.entity_id && Array.isArray(entity.attributes.entity_id)
13 | ? entity.attributes.entity_id
14 | : [];
15 |
16 | entityActions = entityActions.map((actions, i) => {
17 | //filter by supported_features
18 | const stateObj: HassEntity | undefined = hass.states[entities[i]];
19 | const supportedFeatures = computeSupportedFeatures(stateObj);
20 | actions = actions
21 | .filter(e => !e.supported_feature || e.supported_feature & supportedFeatures)
22 | .map(action => omit(action, 'supported_feature'));
23 | return actions;
24 | });
25 |
26 | //find matches
27 | const mixedDomains = [...new Set(entities.map(e => computeDomain(e)))].length > 1;
28 | if (mixedDomains) {
29 | entityActions = entityActions.map(actionList => {
30 | return actionList.map(action => {
31 | if (computeEntity(action.service) == 'turn_on' || computeEntity(action.service) == 'turn_off') {
32 | return {
33 | ...action,
34 | service: 'homeassistant' + '.' + computeEntity(action.service),
35 | icon: computeEntity(action.service) == 'turn_on' ? 'flash' : 'flash-off',
36 | };
37 | }
38 | return action;
39 | });
40 | });
41 | }
42 | if (!entityActions.length) return [];
43 | const commonActions = computeCommonActions(entityActions);
44 | return commonActions;
45 | }
46 |
47 | export const groupStates = (_hass: HomeAssistant, _stateObj: HassEntity, entityStates: Variable[]): Variable | null => {
48 | if (!entityStates.length) return null;
49 | if (!entityStates.every(e => e.type == entityStates[0].type)) return null;
50 | if (entityStates[0].type == EVariableType.List) return listVariable(...(entityStates as ListVariable[]));
51 | else if (entityStates[0].type == EVariableType.Level) return levelVariable(...(entityStates as LevelVariable[]));
52 | else return null;
53 | };
54 |
--------------------------------------------------------------------------------
/src/standard-configuration/group_name.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { getLocale } from '../helpers';
3 | import { localize } from '../localize/localize';
4 |
5 | const domainNames: Record = {
6 | alarm_control_panel: 'domains.alarm_control_panel',
7 | automation: 'ui.dialogs.quick-bar.commands.navigation.automation',
8 | binary_sensor: 'domains.binary_sensor',
9 | calendar: 'panel.calendar',
10 | climate: 'domains.climate',
11 | cover: 'domains.cover',
12 | fan: 'domains.fan',
13 | group: 'domains.group',
14 | humidifier: 'domains.humidifier',
15 | input_boolean: 'domains.input_boolean',
16 | input_number: 'domains.input_number',
17 | input_select: 'domains.input_select',
18 | lawn_mower: 'domains.lawn_mower',
19 | light: 'domains.light',
20 | lock: 'domains.lock',
21 | media_player: 'domains.media_player',
22 | notify: 'domains.notify',
23 | person: 'ui.dialogs.quick-bar.commands.navigation.person',
24 | scene: 'ui.dialogs.quick-bar.commands.navigation.scene',
25 | script: 'ui.dialogs.quick-bar.commands.navigation.script',
26 | sensor: 'ui.panel.config.devices.entities.sensor',
27 | sun: 'ui.panel.config.automation.editor.conditions.type.sun.label',
28 | switch: 'domains.switch',
29 | vacuum: 'domains.vacuum',
30 | water_heater: 'domains.water_heater',
31 | };
32 |
33 | export const standardGroupNames = (domain: string, hass: HomeAssistant) => {
34 | if (domain in domainNames) {
35 | const translationKey = domainNames[domain];
36 | const domainTranslation = translationKey.startsWith('domains')
37 | ? localize(translationKey, getLocale(hass))
38 | : hass.localize(translationKey);
39 |
40 | if (domainTranslation) return domainTranslation;
41 | }
42 | return domain;
43 | };
44 |
--------------------------------------------------------------------------------
/src/standard-configuration/standardActions.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeEntity, HomeAssistant } from 'custom-card-helpers';
2 | import { DefaultActionIcon } from '../const';
3 | import { isDefined, pick } from '../helpers';
4 | import { Action, EVariableType, ListVariable } from '../types';
5 | import { parseVariable, VariableConfig } from './variables';
6 | import { ActionItem, actionList } from './actions';
7 | import { HassEntity } from 'home-assistant-js-websocket';
8 | import { levelVariable } from '../data/variables/level_variable';
9 | import { listVariable } from '../data/variables/list_variable';
10 | import { textVariable } from '../data/variables/text_variable';
11 | import { actionName } from './action_name';
12 | import { actionIcon } from './action_icons';
13 | import { getVariableName } from './variable_name';
14 | import { getVariableOptionName, getVariableOptions } from './variable_options';
15 | import { getVariableOptionIcon } from './variable_icons';
16 | import { listAttribute } from './attribute';
17 | import { groupActions } from './group';
18 |
19 | export const standardActions = (entity_id: string, hass: HomeAssistant, filterCapabilities = true): Action[] => {
20 | const domain = computeDomain(entity_id);
21 |
22 | if (domain == 'group') {
23 | const stateObj = hass.states[entity_id];
24 | const subEntities = listAttribute(stateObj, 'entity_id');
25 | if (!subEntities.length) return [];
26 |
27 | const subActions = subEntities.map(e => standardActions(e, hass, filterCapabilities));
28 | return groupActions(hass, stateObj, subActions);
29 | }
30 |
31 | //not supported by standard configuration
32 | if (!Object.keys(actionList).includes(domain)) return [];
33 |
34 | return Object.entries(actionList[domain])
35 | .map(([id, config]) => parseAction(id, config, entity_id, hass, filterCapabilities))
36 | .filter(isDefined);
37 | };
38 |
39 | const parseAction = (
40 | id: string,
41 | config: ActionItem,
42 | entity_id: string,
43 | hass: HomeAssistant,
44 | filterCapabilities: boolean
45 | ): Action | undefined => {
46 | const domain = computeDomain(entity_id);
47 | const stateObj = hass.states[entity_id];
48 | if (config.condition && !config.condition(stateObj)) return;
49 |
50 | if (id.startsWith('_')) id = id = id.substring(1);
51 |
52 | let action: Action = {
53 | name: '',
54 | icon: DefaultActionIcon,
55 | service: config.service ? `${domain}.${config.service}` : `${domain}.${id}`,
56 | service_data: config.service_data,
57 | };
58 |
59 | if (config.supported_feature) {
60 | const supportedFeature =
61 | config.supported_feature instanceof Function ? config.supported_feature(stateObj) : config.supported_feature;
62 | action = {
63 | ...action,
64 | supported_feature: supportedFeature,
65 | };
66 | }
67 |
68 | action = {
69 | ...action,
70 | name: actionName(domain, id, hass),
71 | icon: actionIcon(domain, id, stateObj),
72 | };
73 |
74 | Object.keys(config.variables || {}).forEach(key => {
75 | action = {
76 | ...action,
77 | variables: {
78 | ...action.variables,
79 | [key]: parseActionVariable(domain, key, config.variables![key], stateObj, hass, filterCapabilities),
80 | },
81 | };
82 | });
83 |
84 | //strip actions having no selectable options
85 | if (
86 | Object.values(action.variables || {}).some(e => e.type == EVariableType.List && !(e as ListVariable).options.length)
87 | )
88 | return;
89 |
90 | //insert entity ID for services notify / script
91 | const match = action.service.match(/^[a-z_]+\.(\{entity_id\})$/);
92 | if (match)
93 | action = {
94 | ...action,
95 | service: action.service.replace(match[1], computeEntity(entity_id)),
96 | };
97 |
98 | return action;
99 | };
100 |
101 | const parseActionVariable = (
102 | domain: string,
103 | variable: string,
104 | variableConfig: VariableConfig,
105 | stateObj: HassEntity | undefined,
106 | hass: HomeAssistant,
107 | filterCapabilities: boolean
108 | ) => {
109 | let config = parseVariable(variableConfig, stateObj, hass);
110 | config = { ...config, name: getVariableName(domain, variable, hass) };
111 | if ('options' in config && isDefined(config.options)) {
112 | let options = [...config.options];
113 | if (!filterCapabilities) {
114 | const extraOptions = getVariableOptions(domain, variable).filter(k => !options.map(e => e.value).includes(k));
115 | options = [...options, ...extraOptions.map(e => Object({ value: e }))];
116 | }
117 | options = options.map(e =>
118 | Object.assign(e, {
119 | name: e.name ? e.name : getVariableOptionName(domain, variable, e.value, hass),
120 | icon: e.icon ? e.icon : getVariableOptionIcon(domain, variable, e.value),
121 | })
122 | );
123 | config = { ...config, options: options };
124 | return listVariable(config);
125 | } else if ('min' in config && isDefined(config.min) && 'max' in config && isDefined(config.max)) {
126 | return levelVariable(config);
127 | } else {
128 | return textVariable(config);
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/src/standard-configuration/standardIcon.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, HomeAssistant, stateIcon } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 |
4 | export const binarySensorIcon = (stateObj: HassEntity) => {
5 | if (stateObj) return stateIcon({ ...stateObj, state: 'off' }) || 'mdi:radiobox-blank';
6 | else return 'mdi:radiobox-blank';
7 | };
8 |
9 | export const sensorIcon = (stateObj: HassEntity) => {
10 | const deviceClass = stateObj.attributes.device_class || '';
11 | switch (deviceClass) {
12 | case 'humidity':
13 | return 'mdi:water-percent';
14 | case 'illuminance':
15 | return 'mdi:brightness-5';
16 | case 'temperature':
17 | return 'mdi:thermometer';
18 | case 'power':
19 | return 'mdi:flash';
20 | case 'pressure':
21 | return 'mdi:gauge';
22 | case 'signal_strength':
23 | return 'mdi:wifi';
24 | default:
25 | return stateObj.attributes.unit_of_measurement?.includes('°') ? 'mdi:thermometer' : 'mdi:eye';
26 | }
27 | };
28 |
29 | const coverIcon = (stateObj: HassEntity, state?: string) => {
30 | const closedState = state == 'closed';
31 | switch (stateObj.attributes.device_class) {
32 | case 'garage':
33 | return closedState ? 'mdi:garage' : 'mdi:garage-open';
34 | case 'door':
35 | return closedState ? 'mdi:door-closed' : 'mdi:door-open';
36 | case 'blind':
37 | return closedState ? 'mdi:blinds' : 'mdi:blinds-open';
38 | case 'window':
39 | return closedState ? 'mdi:window-closed' : 'mdi:window-open';
40 | default:
41 | return closedState ? 'mdi:window-shutter' : 'mdi:window-shutter-open';
42 | }
43 | };
44 |
45 | export const domainIcons: Record = {
46 | alarm_control_panel: 'mdi:alarm-light-outline',
47 | automation: 'mdi:playlist-play',
48 | binary_sensor: 'mdi:radiobox-blank',
49 | button: 'mdi:gesture-tap-button',
50 | calendar: 'mdi:calendar',
51 | camera: 'mdi:camera',
52 | climate: 'mdi:home-thermometer-outline',
53 | cover: 'mdi:window-shutter',
54 | device_tracker: 'mdi:account',
55 | fan: 'mdi:fan',
56 | group: 'mdi:google-circles-communities',
57 | humidifier: 'mdi:air-humidifier',
58 | input_boolean: 'mdi:drawing',
59 | input_button: 'mdi:gesture-tap-button',
60 | input_number: 'mdi:ray-vertex',
61 | input_select: 'mdi:format-list-bulleted',
62 | select: 'mdi:format-list-bulleted',
63 | input_text: 'mdi:textbox',
64 | lawn_mower: 'mdi:robot-mower',
65 | light: 'mdi:lightbulb-outline',
66 | lock: 'mdi:lock-open-outline',
67 | media_player: 'mdi:cast-connected',
68 | number: 'mdi:ray-vertex',
69 | notify: 'mdi:message-text-outline',
70 | person: 'mdi:account-outline',
71 | proximity: 'mdi:map-marker-distance',
72 | remote: 'mdi:remote',
73 | scene: 'mdi:palette-outline',
74 | script: 'mdi:file-document',
75 | sensor: 'mdi:eye',
76 | sun: 'mdi:white-balance-sunny',
77 | switch: 'mdi:flash',
78 | timer: 'mdi:timer',
79 | vacuum: 'mdi:robot-vacuum',
80 | water_heater: 'mdi:water-boiler',
81 | };
82 |
83 | export const standardIcon = (entity_id: string, hass: HomeAssistant) => {
84 | const domain = computeDomain(entity_id);
85 | const stateObj = hass.states[entity_id];
86 |
87 | switch (domain) {
88 | case 'binary_sensor':
89 | return binarySensorIcon(stateObj);
90 | case 'cover':
91 | return coverIcon(stateObj);
92 | case 'sensor':
93 | return sensorIcon(stateObj);
94 | default:
95 | if (domain in domainIcons) return domainIcons[domain];
96 | return 'mdi:folder-outline';
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/src/standard-configuration/standardStates.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeStateDisplay, HomeAssistant } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { DefaultActionIcon } from '../const';
4 | import { levelVariable } from '../data/variables/level_variable';
5 | import { listVariable } from '../data/variables/list_variable';
6 | import { textVariable } from '../data/variables/text_variable';
7 | import { getLocale, isDefined } from '../helpers';
8 | import { localize } from '../localize/localize';
9 | import { Variable } from '../types';
10 | import { listAttribute } from './attribute';
11 | import { groupStates } from './group';
12 | import { statesList } from './states';
13 | import { stateIcon } from './state_icons';
14 | import { parseVariable } from './variables';
15 |
16 | export function standardStates(entity_id: string, hass: HomeAssistant): Variable | null {
17 | const domain = computeDomain(entity_id);
18 | const stateObj: HassEntity | undefined = hass.states[entity_id];
19 | if (!stateObj) return null;
20 |
21 | if (domain == 'group') {
22 | const stateObj = hass.states[entity_id];
23 | const subEntities = listAttribute(stateObj, 'entity_id');
24 | if (!subEntities.length) return null;
25 | const subStates = subEntities.map(e => standardStates(e, hass));
26 | return subStates.every(isDefined) ? groupStates(hass, stateObj, subStates as Variable[]) : null;
27 | }
28 |
29 | if (!Object.keys(statesList).includes(domain)) return null;
30 |
31 | let stateConfig = parseVariable(statesList[domain], stateObj, hass);
32 |
33 | if ('options' in stateConfig && isDefined(stateConfig.options)) {
34 | let options = [...stateConfig.options];
35 | options = options.map(e =>
36 | Object.assign(e, {
37 | icon: e.icon ? e.icon : stateIcon(stateObj, e.value, hass, DefaultActionIcon),
38 | name: e.name ? e.name : getStateName(stateObj, e.value, hass),
39 | })
40 | );
41 | stateConfig = { ...stateConfig, options: options };
42 | if (!options.length) return null;
43 | return listVariable(stateConfig);
44 | } else if ('min' in stateConfig && isDefined(stateConfig.min) && 'max' in stateConfig && isDefined(stateConfig.max)) {
45 | return levelVariable(stateConfig);
46 | } else {
47 | return textVariable(stateConfig);
48 | }
49 | }
50 |
51 | const getStateName = (stateObj: HassEntity, state: string, hass: HomeAssistant) => {
52 | const domain = computeDomain(stateObj.entity_id);
53 | return (
54 | (stateObj.attributes.device_class &&
55 | hass.localize(`component.${domain}.entity_component._.${stateObj.attributes.device_class}.state.${state}`)) ||
56 | hass.localize(`component.${domain}.entity_component._.state.${state}`) ||
57 | state
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/standard-configuration/state_icons.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeEntity, HomeAssistant, stateIcon as HaStateIcon } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { DefaultEntityIcon } from '../const';
4 |
5 | type Template = (stateObj: HassEntity, state: string, hass: HomeAssistant) => string;
6 | type IconItem = string | Template;
7 | type IconList = Record | Template>;
8 |
9 | const binarySensorIcon = (stateObj: HassEntity, state: string) => {
10 | return HaStateIcon({ ...stateObj, state: state });
11 | };
12 |
13 | const coverIcon = (stateObj: HassEntity, state: string) => {
14 | const closedState = state == 'closed';
15 | switch (stateObj.attributes.device_class) {
16 | case 'garage':
17 | return closedState ? 'mdi:garage' : 'mdi:garage-open';
18 | case 'door':
19 | return closedState ? 'mdi:door-closed' : 'mdi:door-open';
20 | case 'blind':
21 | return closedState ? 'mdi:blinds' : 'mdi:blinds-open';
22 | case 'window':
23 | return closedState ? 'mdi:window-closed' : 'mdi:window-open';
24 | default:
25 | return closedState ? 'mdi:window-shutter' : 'mdi:window-shutter-open';
26 | }
27 | };
28 |
29 | const personIcon = (_stateObj: HassEntity, state: string, hass: HomeAssistant) => {
30 | const stateIcons: Record = {
31 | home: 'mdi:home-outline',
32 | not_home: 'mdi:exit-run',
33 | };
34 |
35 | Object.keys(hass.states)
36 | .filter(e => computeDomain(e) == 'zone')
37 | .forEach(e => {
38 | const name = computeEntity(e);
39 | const icon = hass.states[e].attributes.icon;
40 | if (!icon) return;
41 | stateIcons[name] = icon;
42 | });
43 |
44 | return state in stateIcons ? stateIcons[state] : 'mdi:flash';
45 | };
46 |
47 | export const stateIcons: IconList = {
48 | alarm_control_panel: {
49 | disarmed: 'mdi:lock-open-variant-outline',
50 | armed_away: 'mdi:exit-run',
51 | armed_home: 'mdi:home-outline',
52 | armed_night: 'mdi:power-sleep',
53 | triggered: 'mdi:alarm-light-outline',
54 | },
55 | binary_sensor: {
56 | on: binarySensorIcon,
57 | off: binarySensorIcon,
58 | },
59 | calendar: {
60 | on: 'mdi:flash',
61 | off: 'mdi:flash-off',
62 | },
63 | climate: {
64 | off: 'mdi:power-off',
65 | heat: 'mdi:fire',
66 | cool: 'mdi:snowflake',
67 | heat_cool: 'mdi:thermometer',
68 | auto: 'mdi:autorenew',
69 | dry: 'mdi:water-percent',
70 | fan_only: 'mdi:fan',
71 | },
72 | cover: {
73 | closed: coverIcon,
74 | open: coverIcon,
75 | },
76 | device_tracker: {
77 | home: 'mdi:home-outline',
78 | not_home: 'mdi:exit-run',
79 | },
80 | fan: {
81 | on: 'mdi:power',
82 | off: 'mdi:power-off',
83 | },
84 | humidifier: {
85 | on: 'mdi:power',
86 | off: 'mdi:power-off',
87 | },
88 | input_boolean: {
89 | on: 'mdi:flash',
90 | off: 'mdi:flash-off',
91 | },
92 | light: {
93 | on: 'mdi:lightbulb',
94 | off: 'mdi:lightbulb-off',
95 | },
96 | lock: {
97 | unlocked: 'mdi:lock-open-variant-outline',
98 | locked: 'mdi:lock-outline',
99 | },
100 | person: personIcon,
101 | sensor: {
102 | unit: 'attributes.unit_of_measurement',
103 | },
104 | sun: {
105 | below_horizon: 'mdi:weather-sunny-off',
106 | above_horizon: 'mdi:weather-sunny',
107 | },
108 | switch: {
109 | on: 'mdi:flash',
110 | off: 'mdi:flash-off',
111 | },
112 | timer: {
113 | active: 'mdi:play',
114 | paused: 'mdi:pause',
115 | idle: 'mdi:sleep',
116 | },
117 | };
118 |
119 | export const stateIcon = (stateObj: HassEntity, state: string | undefined, hass: HomeAssistant, fallback?: string) => {
120 | const domain = computeDomain(stateObj.entity_id);
121 | if (!state) state = stateObj.state;
122 |
123 | if (domain in stateIcons) {
124 | if (state in stateIcons[domain]) {
125 | const entry = stateIcons[domain][state];
126 | return typeof entry == 'string' ? entry : entry(stateObj, state, hass);
127 | } else if (typeof stateIcons[domain] == 'function') {
128 | return (stateIcons[domain] as Template)(stateObj, state, hass);
129 | }
130 | }
131 | return fallback || DefaultEntityIcon;
132 | };
133 |
--------------------------------------------------------------------------------
/src/standard-configuration/states.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain, computeEntity } from 'custom-card-helpers';
2 | import { numericAttribute, stringAttribute } from './attribute';
3 | import { VariableConfig } from './variables';
4 |
5 | const onOffType = { options: ['on', 'off'] };
6 |
7 | export const statesList: Record = {
8 | alarm_control_panel: {
9 | template: stateObj => {
10 | let modes = ['disarmed', 'triggered'];
11 | const supported = numericAttribute(stateObj, 'supported_features') || 0;
12 | if (supported & 2) modes = [...modes, 'armed_away'];
13 | if (supported & 1) modes = [...modes, 'armed_home'];
14 | if (supported & 4) modes = [...modes, 'armed_night'];
15 | if (supported & 16) modes = [...modes, 'armed_custom_bypass'];
16 | if (supported & 32) modes = [...modes, 'armed_vacation'];
17 | return { options: modes };
18 | },
19 | },
20 | binary_sensor: onOffType,
21 | climate: {
22 | options: 'hvac_modes',
23 | },
24 | calendar: onOffType,
25 | cover: { options: ['open', 'closed'] },
26 | device_tracker: { options: ['home', 'not_home'] },
27 | fan: onOffType,
28 | humidifier: onOffType,
29 | input_boolean: onOffType,
30 | input_number: {
31 | min: 'min',
32 | max: 'max',
33 | unit: 'unit_of_measurement',
34 | step: 'step',
35 | },
36 | input_select: {
37 | options: 'options',
38 | },
39 | light: onOffType,
40 | lock: { options: ['locked', 'unlocked'] },
41 | number: {
42 | min: 'min',
43 | max: 'max',
44 | step: 'step',
45 | },
46 | person: {
47 | template: (_stateObj, hass) => {
48 | const modes = ['home', 'not_home'];
49 | const zones = Object.keys(hass.states)
50 | .filter(e => computeDomain(e) == 'zone')
51 | .map(computeEntity);
52 | return { options: [...new Set([...modes, ...zones])] };
53 | },
54 | },
55 | proximity: {
56 | unit: 'unit_of_measurement',
57 | },
58 | select: {
59 | options: 'options',
60 | },
61 | sensor: {
62 | template: stateObj =>
63 | stateObj && !isNaN(Number(stateObj.state))
64 | ? stringAttribute(stateObj, 'unit_of_measurement') == '%'
65 | ? {
66 | min: 0,
67 | max: 100,
68 | unit: '%',
69 | step: 1,
70 | }
71 | : { unit: 'unit_of_measurement' }
72 | : {},
73 | },
74 | sun: { options: ['below_horizon', 'above_horizon'] },
75 | switch: onOffType,
76 | timer: {
77 | options: ['active', 'paused', 'idle'],
78 | },
79 | water_heater: {
80 | options: 'operation_list',
81 | },
82 | };
83 |
--------------------------------------------------------------------------------
/src/standard-configuration/variable_icons.ts:
--------------------------------------------------------------------------------
1 | type IconList = Record>>;
2 |
3 | const actionIcons: IconList = {
4 | climate: {
5 | hvac_mode: {
6 | off: 'mdi:power-off',
7 | heat: 'mdi:fire',
8 | cool: 'mdi:snowflake',
9 | heat_cool: 'mdi:thermometer',
10 | auto: 'mdi:autorenew',
11 | dry: 'mdi:water-percent',
12 | fan_only: 'mdi:fan',
13 | },
14 | preset_mode: {
15 | activity: 'mdi:account-alert-outline',
16 | away: 'mdi:car-traction-control',
17 | boost: 'mdi:rocket-launch-outline',
18 | comfort: 'mdi:car-seat-cooler',
19 | eco: 'mdi:leaf',
20 | home: 'mdi:home-outline',
21 | none: 'mdi:cancel',
22 | sleep: 'mdi:sleep',
23 | },
24 | },
25 | fan: {
26 | direction: {
27 | forward: 'mdi:autorenew',
28 | reverse: 'mdi:sync',
29 | },
30 | oscillating: {
31 | True: 'mdi:toggle-switch-outline',
32 | False: 'mdi:toggle-switch-off-outline',
33 | },
34 | },
35 | humidifier: {
36 | mode: {
37 | auto: 'mdi:autorenew',
38 | away: 'mdi:car-traction-control',
39 | baby: 'mdi:baby-bottle-outline',
40 | boost: 'mdi:rocket-launch-outline',
41 | comfort: 'mdi:car-seat-cooler',
42 | eco: 'mdi:leaf',
43 | home: 'mdi:home-outline',
44 | normal: 'mdi:account-outline',
45 | sleep: 'mdi:sleep',
46 | },
47 | },
48 | water_heater: {
49 | operation_mode: {
50 | off: 'mdi:power-off',
51 | eco: 'mdi:leaf',
52 | electric: 'mdi:lightning-bolt',
53 | gas: 'mdi:fire',
54 | heat_pump: 'mdi:hvac',
55 | high_demand: 'mdi:water-plus-outline',
56 | performance: 'mdi:rocket-launch-outline',
57 | },
58 | away_mode: {
59 | on: 'mdi:toggle-switch-outline',
60 | off: 'mdi:toggle-switch-off-outline',
61 | },
62 | },
63 | };
64 |
65 | export const getVariableOptionIcon = (domain: string, variable: string, option: string) => {
66 | if (domain in actionIcons && variable in actionIcons[domain] && option in actionIcons[domain][variable])
67 | return actionIcons[domain][variable][option];
68 | return;
69 | };
70 |
--------------------------------------------------------------------------------
/src/standard-configuration/variable_name.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 |
3 | const variableList: Record> = {
4 | climate: {
5 | temperature: 'ui.card.weather.attributes.temperature',
6 | target_temp_low: 'ui.panel.lovelace.editor.card.generic.minimum',
7 | target_temp_high: 'ui.panel.lovelace.editor.card.generic.maximum',
8 | hvac_mode: 'ui.card.climate.operation',
9 | preset_mode: 'ui.card.climate.preset_mode',
10 | fan_mode: 'ui.card.climate.fan_mode',
11 | },
12 | cover: {
13 | position: 'ui.card.cover.position',
14 | tilt_position: 'ui.card.cover.tilt_position',
15 | },
16 | fan: {
17 | percentage: 'ui.card.fan.speed',
18 | oscillating: 'ui.card.fan.oscillate',
19 | direction: 'ui.card.fan.direction',
20 | preset_mode: 'ui.card.fan.preset_mode',
21 | },
22 | humidifier: {
23 | humidity: 'ui.card.humidifier.humidity',
24 | mode: 'ui.card.humidifier.mode',
25 | },
26 | input_number: {
27 | value: 'ui.panel.config.helpers.types.input_number',
28 | },
29 | input_select: {
30 | option: 'ui.components.dialogs.input_select.options',
31 | },
32 | light: {
33 | brightness: 'ui.card.light.brightness',
34 | },
35 | media_player: {
36 | source: 'ui.card.media_player.source',
37 | },
38 | notify: {
39 | title: 'ui.panel.config.automation.editor.actions.type.device_id.extra_fields.title',
40 | message: 'ui.panel.config.automation.editor.actions.type.device_id.extra_fields.message',
41 | },
42 | number: {
43 | value: 'ui.panel.config.helpers.types.input_number',
44 | },
45 | select: {
46 | option: 'ui.components.dialogs.input_select.options',
47 | },
48 | water_heater: {
49 | temperature: 'ui.card.weather.attributes.temperature',
50 | operation_mode: 'ui.card.water_heater.operation',
51 | away_mode: 'ui.card.water_heater.away_mode',
52 | },
53 | };
54 |
55 | export const getVariableName = (domain: string, variable: string, hass: HomeAssistant) => {
56 | if (domain in variableList && variable in variableList[domain]) {
57 | return hass.localize(variableList[domain][variable]);
58 | }
59 | return variable;
60 | };
61 |
--------------------------------------------------------------------------------
/src/standard-configuration/variable_options.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 |
3 | type IconList = Record>>;
4 |
5 | export const variableOptions: IconList = {
6 | climate: {
7 | hvac_mode: {
8 | off: 'component.climate.entity_component._.state.off',
9 | heat: 'component.climate.entity_component._.state.heat',
10 | cool: 'component.climate.entity_component._.state.cool',
11 | heat_cool: 'component.climate.entity_component._.state.heat_cool',
12 | dry: 'component.climate.entity_component._.state.dry',
13 | fan_only: 'component.climate.entity_component._.state.fan_only',
14 | },
15 | preset_mode: {
16 | activity: 'state_attributes.climate.preset_mode.activity',
17 | away: 'state_attributes.climate.preset_mode.away',
18 | boost: 'state_attributes.climate.preset_mode.boost',
19 | comfort: 'state_attributes.climate.preset_mode.comfort',
20 | eco: 'state_attributes.climate.preset_mode.eco',
21 | home: 'state_attributes.climate.preset_mode.home',
22 | none: 'state_attributes.climate.preset_mode.none',
23 | sleep: 'state_attributes.climate.preset_mode.sleep',
24 | },
25 | },
26 | fan: {
27 | direction: {
28 | forward: 'ui.card.fan.forward',
29 | reverse: 'ui.card.fan.reverse',
30 | },
31 | oscillating: {
32 | True: 'state.default.on',
33 | False: 'state.default.off',
34 | },
35 | },
36 | humidifier: {
37 | mode: {
38 | auto: 'state_attributes.humidifier.mode.auto',
39 | away: 'state_attributes.humidifier.mode.away',
40 | baby: 'state_attributes.humidifier.mode.baby',
41 | boost: 'state_attributes.humidifier.mode.boost',
42 | comfort: 'state_attributes.humidifier.mode.comfort',
43 | eco: 'state_attributes.humidifier.mode.eco',
44 | home: 'state_attributes.humidifier.mode.home',
45 | normal: 'state_attributes.humidifier.mode.normal',
46 | sleep: 'state_attributes.humidifier.mode.sleep',
47 | },
48 | },
49 | water_heater: {
50 | operation_mode: {
51 | off: 'component.water_heater.entity_component._.state.off',
52 | eco: 'component.water_heater.entity_component._.state.eco',
53 | electric: 'component.water_heater.entity_component._.state.electric',
54 | gas: 'component.water_heater.entity_component._.state.gas',
55 | heat_pump: 'component.water_heater.entity_component._.state.heat_pump',
56 | high_demand: 'component.water_heater.entity_component._.state.high_demand',
57 | performance: 'component.water_heater.entity_component._.state.performance',
58 | },
59 | away_mode: {
60 | on: 'state.default.on',
61 | off: 'state.default.off',
62 | },
63 | },
64 | };
65 |
66 | export const getVariableOptions = (domain: string, variable: string) => {
67 | if (domain in variableOptions && variable in variableOptions[domain])
68 | return Object.keys(variableOptions[domain][variable]);
69 | return [];
70 | };
71 |
72 | export const getVariableOptionName = (domain: string, variable: string, option: string, hass: HomeAssistant) => {
73 | if (domain in variableOptions && variable in variableOptions[domain] && option in variableOptions[domain][variable])
74 | return hass.localize(variableOptions[domain][variable][option]);
75 | return option;
76 | };
77 |
--------------------------------------------------------------------------------
/src/standard-configuration/variables.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { isDefined, omit, pick } from '../helpers';
4 | import { AtLeast, LevelVariable, ListVariable, TextVariable } from '../types';
5 | import { listAttribute, numericAttribute, stringAttribute } from './attribute';
6 |
7 | type ListOption = { name: string; icon: string };
8 |
9 | type ListVariableConfig = {
10 | options: string | string[] | Record>;
11 | };
12 |
13 | type ListVariableTemplate = (stateObj: HassEntity | undefined, hass: HomeAssistant) => ListVariableConfig;
14 |
15 | export type ListVariableType = ListVariableConfig | { template: ListVariableTemplate };
16 |
17 | type LevelVariableConfig = {
18 | min?: string | number;
19 | max?: string | number;
20 | step?: string | number;
21 | unit?: string;
22 | optional?: boolean;
23 | scale_factor?: number;
24 | };
25 |
26 | type LevelVariableTemplate = (stateObj: HassEntity | undefined, hass: HomeAssistant) => LevelVariableConfig;
27 |
28 | export type LevelVariableType = LevelVariableConfig | { template: LevelVariableTemplate };
29 |
30 | type TextVariableConfig = {
31 | multiline?: boolean;
32 | };
33 |
34 | export type VariableConfig = ListVariableType | LevelVariableType | TextVariableConfig;
35 |
36 | export const parseVariable = (
37 | config: VariableConfig,
38 | stateObj: HassEntity | undefined,
39 | hass: HomeAssistant
40 | ): Partial | AtLeast | Partial => {
41 | const res =
42 | 'template' in config && isDefined(config.template)
43 | ? { ...omit(config, 'template'), ...config.template(stateObj, hass) }
44 | : ({ ...config } as ListVariableConfig | LevelVariableConfig | TextVariableConfig);
45 |
46 | if ('options' in res) {
47 | return parseListVariable(res, stateObj);
48 | } else if ('min' in res && 'max' in res) {
49 | return parseLevelVariable(res, stateObj);
50 | } else {
51 | return res as TextVariableConfig;
52 | }
53 | };
54 |
55 | export const parseListVariable = (
56 | config: ListVariableConfig,
57 | stateObj: HassEntity | undefined
58 | ): AtLeast => {
59 | if (typeof config.options == 'string') {
60 | const res = listAttribute(stateObj, config.options);
61 | return {
62 | options: res.map(e => Object({ value: e })),
63 | };
64 | } else if (Array.isArray(config.options)) {
65 | return {
66 | options: config.options.map(e => Object({ value: e })),
67 | };
68 | } else {
69 | return {
70 | options: Object.entries(config.options).map(([k, v]) => Object({ value: k, ...v })),
71 | };
72 | }
73 | };
74 |
75 | export const parseLevelVariable = (config: LevelVariableConfig, stateObj: HassEntity | undefined) => {
76 | let result: Partial = pick(config, ['unit', 'optional', 'scale_factor']);
77 | if (isDefined(config.min)) result = { ...result, min: numericAttribute(stateObj, config.min) };
78 | if (isDefined(config.max)) result = { ...result, max: numericAttribute(stateObj, config.max) };
79 | if (isDefined(config.step)) result = { ...result, step: numericAttribute(stateObj, config.step) };
80 | if (isDefined(config.unit) && config.unit == 'unit_of_measurement')
81 | result = { ...result, unit: stringAttribute(stateObj, config.unit, '') };
82 | return result;
83 | };
84 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | import { css } from 'lit';
2 |
3 | export const commonStyle = css`
4 | .card-header {
5 | display: flex;
6 | justify-content: space-between;
7 | }
8 | .card-header .name {
9 | white-space: nowrap;
10 | overflow: hidden;
11 | text-overflow: ellipsis;
12 | display: flex;
13 | }
14 | .card-header ha-switch {
15 | padding: 5px;
16 | }
17 | .card-header ha-icon-button {
18 | position: absolute;
19 | right: 6px;
20 | top: 6px;
21 | }
22 | .card-content {
23 | flex: 1;
24 | }
25 | .card-content > *:first-child {
26 | margin-top: 0;
27 | }
28 | .card-content > *:last-child {
29 | margin-bottom: 0;
30 | }
31 | div.text-field, div.secondary {
32 | color: var(--secondary-text-color);
33 | }
34 | .disabled {
35 | color: var(--disabled-text-color);
36 | }
37 | div.header {
38 | color: var(--secondary-text-color);
39 | text-transform: uppercase;
40 | font-weight: 500;
41 | font-size: 12px;
42 | margin: 20px 0px 0px 0px;
43 | display: flex;
44 | flex-direction: row;
45 | }
46 | div.header .switch {
47 | text-transform: none;
48 | font-weight: normal;
49 | font-size: 14px;
50 | display: flex;
51 | flex-grow: 1;
52 | justify-content: flex-end;
53 | }
54 | div.header ha-switch {
55 | display: flex;
56 | align-self: center;
57 | margin: 0px 8px;
58 | line-height: 24px;
59 | }
60 | mwc-button.active {
61 | background: var(--primary-color);
62 | --mdc-theme-primary: var(--text-primary-color);
63 | border-radius: 4px;
64 | }
65 | mwc-button ha-icon {
66 | margin-right: 11px;
67 | }
68 | mwc-button.warning {
69 | --mdc-theme-primary: var(--error-color);
70 | }
71 | div.checkbox-container {
72 | display: grid;
73 | grid-template-columns: max-content 1fr max-content;
74 | grid-template-rows: min-content;
75 | grid-template-areas: "checkbox slider value";
76 | grid-gap: 0px 10px;
77 | }
78 | div.checkbox-container div.checkbox {
79 | grid-area: checkbox;
80 | display: flex;
81 | align-items: center;x
82 | }
83 | div.checkbox-container div.slider {
84 | grid-area: slider;
85 | display: flex;
86 | align-items: center;
87 | }
88 | div.checkbox-container div.value {
89 | grid-area: value;
90 | min-width: 40px;
91 | display: flex;
92 | align-items: center;
93 | }
94 | a {
95 | color: var(--primary-color);
96 | }
97 | a:visited {
98 | color: var(--accent-color);
99 | }
100 |
101 |
102 | .content {
103 | padding: 0px 24px 16px 24px;
104 | }
105 | .buttons {
106 | box-sizing: border-box;
107 | display: flex;
108 | padding: 24px;
109 | padding-top: 16px;
110 | justify-content: space-between;
111 | padding-bottom: max(env(safe-area-inset-bottom), 24px);
112 | background-color: var(--mdc-theme-surface, #fff);
113 | border-top: 1px solid var(--divider-color);
114 | position: sticky;
115 | bottom: 0px;
116 | }
117 | .buttons.centered {
118 | flex-wrap: wrap;
119 | justify-content: center;
120 | }
121 | `;
122 |
123 | export const dialogStyle = css`
124 | ha-dialog {
125 | --mdc-dialog-min-width: 400px;
126 | --mdc-dialog-max-width: 600px;
127 | --mdc-dialog-heading-ink-color: var(--primary-text-color);
128 | --mdc-dialog-content-ink-color: var(--primary-text-color);
129 | --justify-action-buttons: space-between;
130 | --dialog-content-padding: 0px;
131 | }
132 | ha-dialog .form {
133 | color: var(--primary-text-color);
134 | }
135 | a {
136 | color: var(--primary-color);
137 | }
138 | /* make dialog fullscreen on small screens */
139 | @media all and (max-width: 450px), all and (max-height: 500px) {
140 | ha-dialog {
141 | --mdc-dialog-min-width: calc(100vw - env(safe-area-inset-right) - env(safe-area-inset-left));
142 | --mdc-dialog-max-width: calc(100vw - env(safe-area-inset-right) - env(safe-area-inset-left));
143 | --mdc-dialog-min-height: 100%;
144 | --mdc-dialog-max-height: 100%;
145 | --vertical-align-dialog: flex-end;
146 | --ha-dialog-border-radius: 0px;
147 | }
148 | }
149 | mwc-button.warning {
150 | --mdc-theme-primary: var(--error-color);
151 | }
152 | .error {
153 | color: var(--error-color);
154 | }
155 | ha-dialog {
156 | --dialog-surface-position: static;
157 | --dialog-content-position: static;
158 | --vertical-align-dialog: flex-start;
159 | }
160 | .content {
161 | outline: none;
162 | }
163 | .heading {
164 | border-bottom: 1px solid var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
165 | }
166 | :host([tab='time']) ha-dialog {
167 | --dialog-content-padding: 0px;
168 | }
169 | @media all and (min-width: 600px) and (min-height: 501px) {
170 | ha-dialog {
171 | --mdc-dialog-min-width: 560px;
172 | --mdc-dialog-max-width: 580px;
173 | --dialog-surface-margin-top: 40px;
174 | --mdc-dialog-max-height: calc(100% - 72px);
175 | }
176 | :host([large]) ha-dialog {
177 | --mdc-dialog-min-width: 90vw;
178 | --mdc-dialog-max-width: 90vw;
179 | }
180 | }
181 | mwc-tab[disabled] {
182 | --mdc-tab-text-label-color-default: var(--material-disabled-text-color);
183 | pointer-events: none;
184 | }
185 | `;
186 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "lib": [
7 | "es2017",
8 | "dom",
9 | "dom.iterable"
10 | ],
11 | "noEmit": true,
12 | "noUnusedParameters": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "strict": true,
16 | "noImplicitAny": false,
17 | "skipLibCheck": true,
18 | "resolveJsonModule": true,
19 | "experimentalDecorators": true,
20 | "allowSyntheticDefaultImports": true
21 | }
22 | }
--------------------------------------------------------------------------------