├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── common-build-test.yaml
│ ├── hacs-validation.yaml
│ ├── jekyll-gh-pages.yml
│ ├── merge-to-master.yaml
│ ├── pr-test.yaml
│ ├── pr-validation.yaml
│ └── release.yaml
├── .gitignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── babel.config.cjs
├── docs
├── .gitignore
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _data
│ └── navigation.yml
├── _docs
│ ├── 01-01-quick-start.md
│ ├── 01-02-creating-svg-file.md
│ ├── 01-03-creating-goodlooking-floorplan.md
│ ├── 01-04-how-to-handle-size-and-expand-floorplan.md
│ ├── 02-01-examples.md
│ ├── 02-02-example_floorplanner_home.md
│ ├── 02-02-example_home.md
│ ├── 02-02-example_light.md
│ ├── 02-02-example_multi_floor.md
│ ├── 02-02-example_remote.md
│ ├── 02-02-example_ring.md
│ ├── 03-usage.md
│ └── floorplan
│ │ ├── examples
│ │ ├── examples-TODO
│ │ │ └── multi
│ │ │ │ ├── alarm_clock.svg
│ │ │ │ ├── alarm_clock.yaml
│ │ │ │ ├── main.yaml
│ │ │ │ ├── master.css
│ │ │ │ ├── master.svg
│ │ │ │ ├── master.yaml
│ │ │ │ ├── squeezebox.css
│ │ │ │ ├── squeezebox.svg
│ │ │ │ ├── squeezebox.yaml
│ │ │ │ └── states.yaml
│ │ ├── floorplanner_home
│ │ │ ├── floorplanner_home.css
│ │ │ ├── floorplanner_home.svg
│ │ │ ├── floorplanner_home.yaml
│ │ │ └── simulations.yaml
│ │ ├── home
│ │ │ ├── home.css
│ │ │ ├── home.svg
│ │ │ ├── home.yaml
│ │ │ ├── light_off.svg
│ │ │ ├── light_on.svg
│ │ │ └── simulations.yaml
│ │ ├── light
│ │ │ ├── light.css
│ │ │ ├── light.svg
│ │ │ ├── light.yaml
│ │ │ └── simulations.yaml
│ │ ├── multi_floor
│ │ │ ├── multi_floor.css
│ │ │ ├── multi_floor.svg
│ │ │ ├── multi_floor.yaml
│ │ │ └── simulations.yaml
│ │ ├── remote
│ │ │ ├── remote.css
│ │ │ ├── remote.svg
│ │ │ ├── remote.yaml
│ │ │ └── simulations.yaml
│ │ ├── ring
│ │ │ ├── ring.css
│ │ │ ├── ring.svg
│ │ │ ├── ring.yaml
│ │ │ └── simulations.yaml
│ │ ├── rinnai
│ │ │ ├── Rinnai_logo.svg
│ │ │ ├── rinnai.css
│ │ │ ├── rinnai.svg
│ │ │ ├── rinnai.yaml
│ │ │ └── simulations.yaml
│ │ └── test_plate
│ │ │ ├── light_off.svg
│ │ │ ├── light_on.svg
│ │ │ ├── simulations.yaml
│ │ │ ├── test_plate.css
│ │ │ ├── test_plate.svg
│ │ │ └── test_plate.yaml
│ │ ├── favicon.ico
│ │ ├── index-test_plate.html
│ │ └── index.html
├── _includes
│ ├── assets_ref
│ ├── feature_row
│ ├── floorplan_example
│ ├── head
│ │ └── custom.html
│ └── tabs.html
├── _pages
│ ├── 404.md
│ ├── about.md
│ ├── category-archive.md
│ ├── docs.md
│ ├── home.md
│ ├── tag-archive.md
│ └── year-archive.md
├── _posts
│ ├── 2020-01-09-documentation-for-ha-floorplan.md
│ ├── 2020-01-09-releases-of-ha-floorplan.md
│ ├── 2021-07-28-check-our-examples.md
│ └── 2022-11-07-videotutorials-for-ha-floorplan.md
└── assets
│ ├── css
│ └── tabs.css
│ ├── icons
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ └── favicon.ico
│ ├── images
│ ├── bg-frontpage.png
│ ├── bio-photo.jpg
│ ├── docs
│ │ └── quick-start
│ │ │ ├── hacs-setup-1.png
│ │ │ ├── hacs-setup-2.png
│ │ │ ├── video-first-floorplan-part1.png
│ │ │ ├── video-getting-started.png
│ │ │ ├── view-panelmode-1.png
│ │ │ ├── view-panelmode-2.png
│ │ │ └── view-panelmode-3.png
│ ├── header-frontpage.png
│ ├── home
│ │ └── floorplan-background_with_floorplanner.png
│ ├── logo-200x200.png
│ └── logo-88x88.png
│ └── js
│ └── tabs.js
├── hacs.json
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── playwright.config.ts
├── src
├── components
│ ├── floorplan-card
│ │ └── floorplan-card.ts
│ ├── floorplan-examples
│ │ ├── code-block.ts
│ │ ├── floorplan-example.ts
│ │ ├── floorplan-examples.ts
│ │ ├── hass-simulator.ts
│ │ ├── homeassistant.ts
│ │ └── types.ts
│ ├── floorplan-panel
│ │ ├── floorplan-panel.ts
│ │ └── types.ts
│ ├── floorplan
│ │ ├── floorplan-element.ts
│ │ └── lib
│ │ │ ├── color-util.ts
│ │ │ ├── date-util.ts
│ │ │ ├── error-util.ts
│ │ │ ├── eval-helper.ts
│ │ │ ├── events.ts
│ │ │ ├── floorplan-config.ts
│ │ │ ├── floorplan-info.ts
│ │ │ ├── logger.ts
│ │ │ ├── long-clicks.ts
│ │ │ ├── many-clicks.ts
│ │ │ ├── oui-dom-events.js
│ │ │ ├── shadow-dom-helper.ts
│ │ │ ├── simulator-eval-helper.ts
│ │ │ └── types.ts
│ └── lit-toast
│ │ └── lit-toast.ts
├── index.ts
└── lib
│ ├── homeassistant
│ ├── common
│ │ ├── dom
│ │ │ └── fire_event.ts
│ │ └── navigate.ts
│ ├── data
│ │ ├── entity.ts
│ │ ├── haptics.ts
│ │ └── lovelace.ts
│ ├── dialogs
│ │ └── more-info
│ │ │ └── ha-more-info-dialog.ts
│ ├── lovelace
│ │ └── types.ts
│ ├── panels
│ │ └── lovelace
│ │ │ ├── cards
│ │ │ ├── hui-error-card.ts
│ │ │ └── types.ts
│ │ │ ├── common
│ │ │ └── validate-condition.ts
│ │ │ ├── editor
│ │ │ └── types.ts
│ │ │ ├── elements
│ │ │ └── types.ts
│ │ │ ├── entity-rows
│ │ │ └── types.ts
│ │ │ ├── header-footer
│ │ │ └── types.ts
│ │ │ └── types.ts
│ ├── state
│ │ └── more-info-mixin.ts
│ └── types.ts
│ └── utils.ts
├── tests
├── e2e
│ ├── README.md
│ └── floorplan-rule.test.ts
├── jest
│ ├── jest-common-utils.ts
│ ├── jest-floorplan-utils.ts
│ └── tests
│ │ ├── disabled
│ │ └── README.md
│ │ ├── floorplan-action-triggers.test.ts
│ │ ├── floorplan-configuration.test.ts
│ │ ├── floorplan-eval-helpers-functions.test.ts
│ │ ├── floorplan-eval-helpers-objects.test.ts
│ │ ├── floorplan-events.test.ts
│ │ ├── floorplan-examples.test.ts
│ │ ├── floorplan-services-text_set.test.ts
│ │ └── floorplan-services.test.ts
├── setup_tests.sh
└── types
│ └── svg.ts
├── tsconfig.json
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | # don't ever lint node_modules
2 | node_modules
3 |
4 | # don't lint build output (make sure it's set to your correct build folder name)
5 | dist
6 | docs
7 |
8 | # don't lint nyc coverage output
9 | coverage
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true
4 | },
5 | root: true,
6 | parser: '@typescript-eslint/parser',
7 | plugins: [
8 | '@typescript-eslint',
9 | ],
10 | extends: [
11 | 'eslint:recommended',
12 | 'plugin:@typescript-eslint/recommended',
13 | ],
14 | };
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: coffeetoexetico
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report, if you've found an issue. Please use the "Discussions"-section,
4 | if you're looking for general help and usage-support. Visit our documentation, too.
5 | There's plenty to read! Thanks :-)
6 | title: "[BUG]"
7 | labels: waiting on review
8 | assignees: ''
9 |
10 | ---
11 |
12 |
27 |
28 | **Describe the bug**
29 | A clear and concise description of what the bug is.
30 |
31 | **To Reproduce**
32 | Steps to reproduce the behavior:
33 | 1. Go to '...'
34 | 2. Click on '....'
35 | 3. Scroll down to '....'
36 | 4. See error
37 |
38 | **Expected behavior**
39 | A clear and concise description of what you expected to happen.
40 |
41 | **Screenshots**
42 | If applicable, add screenshots to help explain your problem.
43 |
44 | **Desktop (please complete the following information):**
45 | - OS: [e.g. iOS]
46 | - Browser [e.g. chrome, safari]
47 | - Version [e.g. 22]
48 |
49 | **Smartphone (please complete the following information):**
50 | - Device: [e.g. iPhone6]
51 | - OS: [e.g. iOS8.1]
52 | - Browser [e.g. stock browser, safari]
53 | - Version [e.g. 22]
54 |
55 | **Share configuration**
56 | Please share:
57 | - `YAML` (**IMPORTANT!**)
58 | - `CSS`
59 | - SVG-file (**IMPORTANT!**) (Use gofile.io, or something simular)
60 |
61 | _If you create a issue without a SVG example, including the required YAML and CSS to reproduce the issue, it's likely what your issue will be closed, or marked as unresolvable, as it's lacking the proper amount of details. Thank you for understanding the situation._
62 |
63 | **Additional context**
64 | Add any other context about the problem here.
65 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for ha-floorplan. Please note that hanging feature reqeusts will be closed at some point - there's a section for that, in the Discussion area.
4 | title: "[FEATURE] "
5 | labels: [feature request,waiting on review,]
6 | assignees: ''
7 |
8 | ---
9 |
10 |
25 |
26 | **Is your feature request related to a problem? Please describe.**
27 | _A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]_
28 |
29 | **Describe the solution you'd like**
30 | _A clear and concise description of what you want to happen._
31 |
32 | **Describe alternatives you've considered**
33 | _A clear and concise description of any alternative solutions or features you've considered._
34 |
35 | **Illustrations and examples**
36 | _If you're able to provide any examples, please do so. Have you found anything like the feature you're asking for? Please share alternatives from other sources, which will help us understand your thoughts even better._
37 |
38 | **Additional context**
39 | _Add any other context or screenshots about the feature request here._
40 |
--------------------------------------------------------------------------------
/.github/workflows/common-build-test.yaml:
--------------------------------------------------------------------------------
1 | name: Common Build and Test
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | upload-artifacts:
7 | required: false
8 | type: boolean
9 | default: false
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [20.x]
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 |
25 | - name: Build files with Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v4
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 |
30 | - run: npm ci
31 |
32 | # Install other required resources
33 | - run: ./tests/setup_tests.sh
34 |
35 | - run: npm run build
36 |
37 | - name: Run playwright tests
38 | run: npm run test:e2e
39 | env:
40 | CI: true
41 |
42 | - name: Run tests
43 | run: npm test
44 | env:
45 | CI: true
46 |
47 | - name: Publish Test Results
48 | uses: EnricoMi/publish-unit-test-result-action@v2
49 | if: always()
50 | with:
51 | files: |
52 | test-results/jest/*.xml
53 | test-results/e2e/*.xml
54 |
55 | - name: Validate build success
56 | run: |
57 | if [ ! -f "dist/floorplan.js" ]; then
58 | echo "Build failed: 'dist/floorplan.js' not found."
59 | exit 1
60 | fi
61 |
62 | - name: Upload build artifacts
63 | if: ${{ inputs.upload-artifacts }}
64 | uses: actions/upload-artifact@v4
65 | with:
66 | name: build-artifacts
67 | path: dist/
68 |
--------------------------------------------------------------------------------
/.github/workflows/hacs-validation.yaml:
--------------------------------------------------------------------------------
1 | name: HACS Validation
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'docs/**'
7 | - '.github/workflows/'
8 | schedule:
9 | - cron: "0 0 * * *"
10 |
11 | jobs:
12 | validate:
13 | runs-on: "ubuntu-latest"
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: HACS validation
17 | uses: "hacs/action@main"
18 | with:
19 | category: "plugin"
20 |
--------------------------------------------------------------------------------
/.github/workflows/jekyll-gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages
2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["master"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | validate:
26 | runs-on: ubuntu-latest
27 |
28 | outputs:
29 | is_final_version: ${{ steps.check_version.outputs.is_final_version }}
30 |
31 | steps:
32 | - name: Checkout code
33 | uses: actions/checkout@v4
34 |
35 | - name: Check ha-floorplan package version
36 | id: check_version
37 | run: |
38 | PACKAGE_VERSION=$(node -p "require('./package.json').version")
39 | if [[ "$PACKAGE_VERSION" == *-* ]]; then
40 | echo "Non-final version detected ($PACKAGE_VERSION)."
41 | echo "is_final_version=false" >> $GITHUB_OUTPUT
42 | else
43 | echo "Final version detected ($PACKAGE_VERSION)."
44 | echo "is_final_version=true" >> $GITHUB_OUTPUT
45 | fi
46 |
47 | # Build job
48 | build:
49 | needs: validate
50 | if: needs.validate.outputs.is_final_version == 'true'
51 | runs-on: ubuntu-latest
52 | steps:
53 | - name: Checkout
54 | uses: actions/checkout@v4
55 |
56 | - name: Build floorplan-examples with Node.js
57 | uses: actions/setup-node@v4
58 | with:
59 | node-version: "20.x"
60 | - run: npm ci
61 | - run: npm run build
62 | - run: mv dist/floorplan-examples.js docs/_docs/floorplan/floorplan-examples.js
63 |
64 | - name: Setup Pages
65 | uses: actions/configure-pages@v5
66 |
67 | - name: Build with Jekyll
68 | uses: actions/jekyll-build-pages@v1
69 | with:
70 | source: ./docs
71 | destination: ./_site
72 |
73 | - name: Upload artifact
74 | uses: actions/upload-pages-artifact@v3
75 |
76 | # Deployment job
77 | deploy:
78 | environment:
79 | name: github-pages
80 | url: ${{ steps.deployment.outputs.page_url }}
81 | runs-on: ubuntu-latest
82 | needs: build
83 | steps:
84 | - name: Deploy to GitHub Pages
85 | id: deployment
86 | uses: actions/deploy-pages@v4
87 |
--------------------------------------------------------------------------------
/.github/workflows/merge-to-master.yaml:
--------------------------------------------------------------------------------
1 | name: Test on Merge to Master
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | name: Build and Test
11 | uses: ./.github/workflows/common-build-test.yaml
12 | with:
13 | upload-artifacts: true
14 |
--------------------------------------------------------------------------------
/.github/workflows/pr-test.yaml:
--------------------------------------------------------------------------------
1 | name: Test on PR
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | jobs:
8 | build:
9 | name: Build and Test
10 | uses: ./.github/workflows/common-build-test.yaml
11 | with:
12 | upload-artifacts: false
--------------------------------------------------------------------------------
/.github/workflows/pr-validation.yaml:
--------------------------------------------------------------------------------
1 | name: Validate Pull Request
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 | pull_request_target:
7 | types: [opened, synchronize, reopened]
8 |
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 |
13 | jobs:
14 | validate-pr:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout base branch
19 | uses: actions/checkout@v3
20 | with:
21 | ref: ${{ github.base_ref }}
22 |
23 | - name: Fetch pull request branch
24 | run: |
25 | git fetch origin pull/${{ github.event.pull_request.number }}/head:pr
26 | git checkout pr
27 |
28 | - name: Check if workflows folder is modified
29 | id: check-workflows
30 | run: |
31 | if git diff --name-only origin/${{ github.base_ref }} pr | grep -q '^.github/workflows/'; then
32 | echo "workflows_modified=true" >> $GITHUB_ENV
33 | else
34 | echo "workflows_modified=false" >> $GITHUB_ENV
35 | fi
36 |
37 | - name: Add warning comment if workflows are modified
38 | if: env.workflows_modified == 'true'
39 | uses: actions/github-script@v6
40 | with:
41 | script: |
42 | const issueNumber = context.payload.pull_request.number;
43 | const owner = context.repo.owner;
44 | const repo = context.repo.repo;
45 | const labelName = "workflow-modified";
46 |
47 | // Check if the label already exists
48 | const existingLabels = await github.rest.issues.listLabelsOnIssue({
49 | owner,
50 | repo,
51 | issue_number: issueNumber,
52 | });
53 |
54 | const labelExists = existingLabels.data.some(label => label.name === labelName);
55 |
56 | if (!labelExists) {
57 | // Add the label
58 | await github.rest.issues.addLabels({
59 | owner,
60 | repo,
61 | issue_number: issueNumber,
62 | labels: [labelName],
63 | });
64 |
65 | // Add the warning comment
66 | await github.rest.issues.createComment({
67 | issue_number: issueNumber,
68 | owner,
69 | repo,
70 | body: "⚠️ Warning: This PR modifies workflow files in the `.github/workflows/` folder. Please ensure these changes are intentional and secure.",
71 | });
72 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Draft Release with Notes and Build Artifacts
2 | on:
3 | push:
4 | tags:
5 | - "v*.*.*"
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | - name: Install
13 | run: npm ci
14 | - name: Build
15 | run: npm run build
16 | - name: Release
17 | uses: softprops/action-gh-release@v2
18 | if: startsWith(github.ref, 'refs/tags/')
19 | with:
20 | draft: true
21 | prerelease: ${{ contains(github.ref, '-') }}
22 | generate_release_notes: true
23 | append_body: true
24 | files: dist/*.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .env.test
68 |
69 | # parcel-bundler cache (https://parceljs.org/)
70 | .cache
71 |
72 | # next.js build output
73 | .next
74 |
75 | # nuxt.js build output
76 | .nuxt
77 |
78 | # vuepress build output
79 | .vuepress/dist
80 |
81 | # Serverless directories
82 | .serverless/
83 |
84 | # FuseBox cache
85 | .fusebox/
86 |
87 | # DynamoDB Local files
88 | .dynamodb/
89 |
90 | # Parcel
91 | .parcel-cache
92 | parcel-bundle-reports
93 |
94 | # Jekyll
95 | _site/
96 |
97 | # Webpack bundle analyzer
98 | stats.json
99 |
100 | # Local dist folder
101 | dist_local
102 |
103 | # Ignore dist folder
104 | dist
105 |
106 | # MacOS
107 | .DS_Store
108 |
109 | # Tests
110 | test-results/*
111 | test-results/*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "jgclark.vscode-todo-highlight"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "**/.cache": true,
4 | "**/dist": true
5 | // "**/docs": true
6 | },
7 | // Default (format when you paste)
8 | "editor.formatOnPaste": true,
9 | // Default (format when you save)
10 | "editor.formatOnSave": false,
11 | "spellright.language": ["en"],
12 | "spellright.documentTypes": ["markdown", "latex", "plaintext"],
13 | "todohighlight.defaultStyle": {
14 | "color": "#2196f3",
15 | "backgroundColor": "#ffeb3b"
16 | },
17 | "todohighlight.keywords": [
18 | {
19 | "text": "NOTE:",
20 | "color": "white",
21 | "backgroundColor": "#264f95",
22 | "border": "1px solid white",
23 | "borderRadius": "2px",
24 | "overviewRulerColor": "grey",
25 | "cursor": "pointer"
26 | },
27 | {
28 | "text": "HACK:",
29 | "color": "#000",
30 | "isWholeLine": false
31 | },
32 | {
33 | "text": "TODO:",
34 | "color": "red",
35 | "border": "1px solid red",
36 | "borderRadius": "2px", //NOTE: using borderRadius along with `border` or you will see nothing change
37 | "backgroundColor": "rgba(0,0,0,.2)"
38 | },
39 | {
40 | "text": "WARNING:",
41 | "color": "white",
42 | "border": "1px solid white",
43 | "borderRadius": "2px",
44 | "backgroundColor": "red"
45 | },
46 | {
47 | "text": "DEBUG:",
48 | "color": "#E0E0E0",
49 | "border": "1px solid rgba(255,255,255,0.2)",
50 | "borderRadius": "6px",
51 | "backgroundColor": "#404040",
52 | "isWholeLine": true
53 | },
54 | {
55 | "text": "DELETE:",
56 | "color": "white",
57 | "backgroundColor": "#5e2ca5",
58 | "isWholeLine": true,
59 | "border": "1px solid rgba(255,255,255,0.2)",
60 | "borderRadius": "6px"
61 | }
62 | ],
63 | "cSpell.words": [
64 | "Floorplan",
65 | "floorplan",
66 | "ha-floorplan",
67 | "homeassistant"
68 | ]
69 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ha-floorplan
6 |
7 |
8 |
9 | Floorplan for Home Assistant - your imagination (almost) defines the limits
10 |
11 |
12 |
13 |
15 |
16 |
17 |
19 |
20 |
21 |
23 |
24 |
25 |
27 |
28 |
29 |
31 |
32 |
33 |
35 |
36 |
37 |
38 |
39 | •
40 | Floorplan Documentation •
41 | Discussion (Ask for help, feedback & support) •
42 | Home Assistant Community •
43 |
44 |
45 | > [!TIP]
46 | > For the best experience, we highly recommend installing ha-floorplan through HACS. Alternatively, you can download the latest version directly from the [Releases](https://github.com/ExperienceLovelace/ha-floorplan/releases) page, where the `floorplan.js` file is also available as an asset.
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ## Draw it and bring it to life
55 |
56 | This tool expands way beyond creating just floorplans. If you can draw it in an SVG file, you can bring it to life with ha-floorplan. Explore endless possibilities and customize your home automation experience to your liking.
57 |
58 | ## Getting started
59 |
60 | We'll suggest you to visit our [Installation](https://experiencelovelace.github.io/ha-floorplan/docs/quick-start/) guide.
61 |
62 | If you're unsure on where to start in the creation process, we'll suggest you to check our [Live Examples](https://experiencelovelace.github.io/ha-floorplan/docs/examples/).
63 |
64 | Find more documentation on how to use each action and functions, by visiting the [Usage](https://experiencelovelace.github.io/ha-floorplan/docs/usage/) page.
65 |
66 |
67 | ## Features
68 |
69 | - Make Floorplan(s) based on SVG-files
70 | - Trigger states, visualize states and more
71 | - Call services and more, for even more options
72 | - Use as Lovelace-card, or as a panel
73 | - _It's hard to mention everything in a list like this, so **give it a try**_ 🥳
74 |
75 |
76 |
77 |
78 |
79 | #### Resource template for Home Assistant
80 |
81 | ```yaml
82 | resources:
83 | - url: /hacsfiles/ha-floorplan/floorplan.js
84 | type: module
85 | ```
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Would you like to play around with the code behind HA Floorplan? _There's many ways to build HA Floorplan. The question is, what you're going to do?_
96 |
97 | Just execute `npm install` to install the dependencies. Use `npm run build` for the production-env. Are you going to test something, use `npm run build:dev` instead. For other options, check [package.json](https://github.com/ExperienceLovelace/ha-floorplan/blob/master/package.json). Use `npm run start` to serve our example-suite. Testing can be done with `npm run test`.
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | "@babel/preset-env",
5 | {
6 | targets: {
7 | node: "current", // Target the current Node.js version
8 | },
9 | },
10 | ],
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-cache
4 | .jekyll-metadata
5 | vendor
6 | _docs/floorplan/floorplan-examples.js
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | #source "https://rubygems.org"
2 | # Hello! This is where you manage which Jekyll version is used to run.
3 | # When you want to use a different version, change it below, save the
4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
5 | #
6 | # bundle exec jekyll serve
7 | #
8 | # This will help ensure the proper Jekyll version is running.
9 | # Happy Jekylling!
10 | #gem "jekyll", "~> 4.1.0"
11 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
12 | #gem "minima", "~> 2.5"
13 | #gem "minima"
14 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
15 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
16 | #gem "github-pages", group: :jekyll_plugins
17 | # If you have any plugins, put them here!
18 | #group :jekyll_plugins do
19 | # gem "jekyll-feed", "~> 0.12"
20 | #end
21 |
22 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
23 | # and associated library.
24 | #platforms :mingw, :x64_mingw, :mswin, :jruby do
25 | # gem "tzinfo", "~> 1.2"
26 | # gem "tzinfo-data"
27 | #end
28 | #
29 | # Performance-booster for watching directories on Windows
30 | #gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
31 |
32 | source "https://rubygems.org"
33 |
34 | gem "github-pages", group: :jekyll_plugins
35 |
36 | gem "tzinfo-data"
37 | gem "wdm", "~> 0.1.0" if Gem.win_platform?
38 |
39 | # If you have any plugins, put them here!
40 | group :jekyll_plugins do
41 | gem "jekyll-paginate"
42 | gem "jekyll-sitemap"
43 | gem "jekyll-gist"
44 | gem "jekyll-feed"
45 | gem "jemoji"
46 | gem "jekyll-include-cache"
47 | end
48 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 | #
11 | # If you need help with YAML syntax, here are some quick references for you:
12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
13 | # https://learnxinyminutes.com/docs/yaml/
14 | #
15 | # Site settings
16 | # These are used to personalize your new site. If you look in the HTML files,
17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
18 | # You can create any custom variable you would like, and they will be accessible
19 | # in the templates via {{ site.myvariable }}.
20 |
21 | title: Floorplan for Home Assistant
22 | subtitle: Your imagination (almost) defines the limits
23 | email: tobiasnordahl@gmail.com
24 | description: >- # this means to ignore newlines until "baseurl:"
25 | Documentation for HA Floorplan, which are used in Home Assistant.
26 | Turn a dumb SVG-file into your new floorplan, and control devices,
27 | visualize states and much more. We're always doing our best to
28 | support whatever you'd like to do. So, get started now - and please
29 | share your feedback!
30 | logo: /assets/images/logo-88x88.png
31 | repository: ExperienceLovelace/ha-floorplan
32 | baseurl: "/ha-floorplan" # the subpath of your site, e.g. /blog
33 | url: "" # the base hostname & protocol for your site, e.g. http://example.com
34 | search: true
35 |
36 |
37 | # Build settings
38 | markdown: kramdown
39 | remote_theme: "mmistakes/minimal-mistakes@4.26.2"
40 | minimal_mistakes_skin: "air"
41 |
42 | # Outputting
43 | permalink: /:categories/:title/
44 | #paginate: 5 # amount of posts to show
45 | #paginate_path: /page:num/
46 |
47 | include:
48 | - _pages
49 |
50 | # Exclude from processing.
51 | # The following items will not be processed, by default. Create a custom list
52 | # to override the default setting.
53 | # exclude:
54 | # - Gemfile
55 | # - Gemfile.lock
56 | # - node_modules
57 | # - vendor/bundle/
58 | # - vendor/cache/
59 | # - vendor/gems/
60 | # - vendor/ruby/
61 |
62 | # Plugins (previously gems:)
63 | plugins:
64 | - jekyll-paginate
65 | - jekyll-sitemap
66 | - jekyll-gist
67 | - jekyll-feed
68 | - jemoji
69 | - jekyll-include-cache
70 | - jekyll-remote-theme
71 |
72 | # author:
73 | # name : "First Lastname"
74 | # avatar : "/assets/images/bio-photo.jpg"
75 | # bio : "My awesome biography constrained to a sentence or two goes here."
76 | # links:
77 | # - label: "Website"
78 | # icon: "fas fa-fw fa-link"
79 | # url: "https://"
80 | # - label: "Twitter"
81 | # icon: "fab fa-fw fa-twitter-square"
82 | # url: "https://twitter.com/"
83 | # - label: "GitHub"
84 | # icon: "fab fa-fw fa-github"
85 | # url: "https://github.com/"
86 | # - label: "Instagram"
87 | # icon: "fab fa-fw fa-instagram"
88 | # url: "https://instagram.com/"
89 |
90 | footer:
91 | links:
92 | - label: "GitHub"
93 | icon: "fab fa-fw fa-github"
94 | url: "https://github.com/ExperienceLovelace/ha-floorplan"
95 | - label: "Ask for help & support"
96 | icon: "fas fa-fw fa-comments"
97 | url: "https://github.com/ExperienceLovelace/ha-floorplan/discussions"
98 | - label: "Home Assistant Community"
99 | icon: "fas fa-users"
100 | url: "https://community.home-assistant.io/t/floorplan-now-available-as-a-lovelace-card/115489"
101 |
102 | defaults:
103 | # _posts
104 | - scope:
105 | path: ""
106 | type: posts
107 | values:
108 | layout: single
109 | author_profile: true
110 | read_time: true
111 | comments: true
112 | share: true
113 | related: true
114 | # _pages
115 | - scope:
116 | path: "_pages"
117 | type: pages
118 | values:
119 | layout: single
120 | author_profile: true
121 | # _docs
122 | - scope:
123 | path: ""
124 | type: docs
125 | values:
126 | layout: single
127 | read_time: false
128 | author_profile: false
129 | share: false
130 | comments: false
131 | sidebar:
132 | nav: "docs"
133 |
134 | category_archive:
135 | type: liquid
136 | path: /categories/
137 | tag_archive:
138 | type: liquid
139 | path: /tags/
140 |
141 |
142 | # Collections
143 | collections:
144 | docs:
145 | output: true
146 | permalink: /:collection/:path/
147 |
148 | # Breadcrumbs
149 | breadcrumbs: true
150 |
--------------------------------------------------------------------------------
/docs/_data/navigation.yml:
--------------------------------------------------------------------------------
1 | main:
2 | - title: "About"
3 | url: /about/
4 |
5 | - title: "Quick Start"
6 | url: /docs/quick-start/
7 |
8 | - title: "Usage"
9 | url: /docs/usage/
10 |
11 | - title: "Examples"
12 | url: /docs/examples/
13 |
14 | - title: "Posts"
15 | url: /posts/
16 |
17 | # - title: "Categories"
18 | # url: /categories/
19 |
20 | - title: "Tags"
21 | url: /tags/
22 |
23 | # - title: "External Link"
24 | # url: https://google.com
25 |
26 | # Documentation Sidebar
27 | docs:
28 | - title: "Getting Started"
29 | children:
30 | - title: "Quick Start"
31 | url: /docs/quick-start/
32 | - title: "Your First SVG File"
33 | url: /docs/create-svg-file/
34 | - title: "Good-looking Floorplans"
35 | url: /docs/create-goodlooking-floorplan/
36 | - title: "Handle Size and Expand Floorplan"
37 | url: /docs/how-to-handle-size-and-expand-floorplan/
38 |
39 | - title: "Examples"
40 | url: /docs/examples/
41 | children:
42 | - title: "Home"
43 | url: /docs/example-home/
44 | - title: "Light"
45 | url: /docs/example-light/
46 | - title: "Ring"
47 | url: /docs/example-ring/
48 | - title: "Remote"
49 | url: /docs/example-remote/
50 | - title: "Multi Floor"
51 | url: /docs/example-multi-floor/
52 | - title: "Floorplanner Home"
53 | url: /docs/example-floorplanner-home/
54 |
55 | - title: Usage
56 | url: /docs/usage/
57 |
--------------------------------------------------------------------------------
/docs/_docs/01-02-creating-svg-file.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/create-svg-file/
3 | title: "Creating Your First SVG File"
4 | toc: true
5 | ---
6 |
7 | [Inkscape](https://inkscape.org/en/develop/about-svg/) is a free application that lets you create vector images. You can make your floorplan as simple or as detailed as you want. It is recommended that you create an SVG element (i.e. `rect`, `path`, `text`, etc.) for each HA entity ( i.e. binary sensor, switch, camera, etc.) you want to display on your floorplan. Each of these elements should have its `id` set to the corresponding entity name in Home Assistant.
8 |
9 | For example, below is what the SVG element looks like for a Front Hallway binary sensor. The `id` of the shape is set to the entity name `binary_sensor.front_hallway`. This allows the shape to automatically get hooked up to the right entity when the floorplan is displayed.
10 |
11 | ```html
12 |
14 | ```
15 |
16 | If you need a good source of SVG icons / images, the following resources are a good starting point.
17 |
18 | - [SVG Repo](https://www.svgrepo.com)
19 | - [Free SVG](https://freesvg.org)
20 | - [Online Web Fonts](https://www.onlinewebfonts.com/icon)
21 | - [Material Design Icons](https://materialdesignicons.com)
22 | - [Noun Project](https://thenounproject.com)
23 | - [Flat Icon](http://flaticon.com)
24 |
25 | ## Animations not in the right position?
26 |
27 | If you're using animations in your floorplans, and your SVG elements are not appearing in the right position or are spinning off the page, it's most likely because your SVG element already has a [transform](https://www.w3schools.com/cssref/css3_pr_transform.asp) applied to it. Best way to resolve this is to view the SVG file in a text editor, and locate your SVG element. If the SVG element contains a `transform` attribute, it means that any `transform` you apply in Floorplan will likely conflict with this existing `transform`. Below is an example of an SVG element with a `transform` already applied.
28 |
29 | ```xml
30 |
33 | ```
34 |
35 | The best way to resolve this is to create a `` element to act as a container for your SVG element, and move the original `transform` attribute to the `` element.
36 |
37 | Below is an example of a `` element that contains the original SVG element. As you can see, the `` element contains the original `transform`, which frees up the SVG element to use any `transform` applied in Floorplan.
38 |
39 | ```xml
40 |
43 |
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/_docs/01-03-creating-goodlooking-floorplan.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/create-goodlooking-floorplan/
3 | title: "Create Good-looking Floorplans"
4 | toc: true
5 | ---
6 |
7 | {% capture workinprogress-notice-1 %}
8 | #### Work in progress...
9 |
10 | We're still working on the **Create Good-looking Floorplans** guide.
11 |
12 | {% endcapture %}
13 |
14 | {{ workinprogress-notice-1 | markdownify }}
15 |
16 | It can be very time-consuming to create a good looking floorplan. So be prepared before you start.
17 |
18 | A good piece of software with awesome assets is [Floorplanner](https://floorplanner.com) `Not sponsored`. Please keep in mind that Floorplanner will ask for a few bucks, if you would like to export the render in higher quality.
19 |
20 | If you're searching for an all-free alternative, checkout [Sweet Home 3D](http://www.sweethome3d.com).
21 |
--------------------------------------------------------------------------------
/docs/_docs/01-04-how-to-handle-size-and-expand-floorplan.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/how-to-handle-size-and-expand-floorplan/
3 | title: "How to Handle Size and Expand Floorplan"
4 | toc: true
5 | panel_mode_gallery:
6 | - url: /assets/images/docs/quick-start/view-panelmode-1.png
7 | image_path: /assets/images/docs/quick-start/view-panelmode-1.png
8 | alt: "Before enabling Panel-mode"
9 | - url: /assets/images/docs/quick-start/view-panelmode-2.png
10 | image_path: /assets/images/docs/quick-start/view-panelmode-2.png
11 | alt: "Here's how to enable Panel-mode"
12 | - url: /assets/images/docs/quick-start/view-panelmode-3.png
13 | image_path: /assets/images/docs/quick-start/view-panelmode-3.png
14 | alt: "After Panel-mode are enabled"
15 | ---
16 |
17 | [ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan) comes with a built-in way to handle the render size of your floorplan. It's defined as the `full_height` option, to prevent vertical scollbars. By combining the `full_height` option with Home Assistant's [panel mode](https://www.home-assistant.io/lovelace/panel/) for views, you're all set.
18 |
19 | The `full_height` option should be added at the same level as the `config:` and `type:` definitions (in the root).
20 |
21 | ## Panel mode with `full_height` in YAML-mode
22 |
23 | Here's example where both _panel mode_ and our `full_height` option are used. It's a good idea to wrap your floorplan card in a vertical and horizontal stack, which allows you to have proper control of the size.
24 |
25 | ```yaml
26 | - title: Floorplan
27 | icon: 'mdi:floor-plan'
28 | panel: true
29 | cards:
30 | - type: vertical-stack
31 | cards:
32 | - type: horizontal-stack
33 | cards:
34 | - config: !include lovelace/floorplan/_config-floorplan1.yaml
35 | type: 'custom:floorplan-card'
36 | full_height: true
37 | ```
38 |
39 | ## Panel mode in GUI-mode
40 |
41 | Most people joining Home Assistant today will start using lovelace cards right away - and most of them, won't go _back_ to YAML mode. If you're still in the normal edit mode, you can change the [panel mode](https://www.home-assistant.io/lovelace/panel/) through the GUI.
42 |
43 | 1. Click the 3 vertical dots to the right of the screen
44 | 2. Click 'Edit Dashboard'
45 | 2. Select your page
46 | 3. Click the pencil to the right of the page title
47 | 4. Select 'Panel Mode'
48 |
49 | {% include gallery id="panel_mode_gallery" caption="Here's how the view is rendered before and after panel mode is enabled, including the required steps." %}
50 |
51 | After that's done, try and see if everything is working as expected. If not, please add `full_height: true` to your YAML code, and see if that makes any difference.
52 |
53 | ## Other methods
54 |
55 | We'll always recommend for you to stick to the default way of handling the floorplan size, but if you really want to play around, here are a few words about that...
56 |
57 | There's always more than a single way, right? If you're familiar with CSS, you should try and play around with the size by yourself.
58 |
59 | In CSS you'd likely take a look at the options like:
60 | - `min-width`
61 | - `max-width`
62 | - `min-height`
63 | - `max-height`
64 | - `height`
65 | - `width`
66 |
67 | Options like `100%` and `100vh` will be good to know, and maybe even the `calc( 100vh - 100px)` operator, if you haven't tried that before.
68 |
--------------------------------------------------------------------------------
/docs/_docs/02-01-examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/examples/
3 | title: "Examples"
4 | toc: true
5 | ---
6 |
7 | ## Help is on the way
8 |
9 | Yay! Examples are created for you to get a good idea of how to use [ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan) as best as possible.
10 |
11 | Not sure where to start? Give the [Home](https://experiencelovelace.github.io/ha-floorplan/docs/example-home/) example a try.
12 |
13 | {% include assets_ref %}
14 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_floorplanner_home.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-floorplanner-home/
3 | title: "Floorplanner Home Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "floorplanner_home"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | This floorplan has a background, created with the online web-based software called Floorplanner .
13 |
14 | {% include floorplan_example %}
15 |
16 | {% include assets_ref %}
17 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_home.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-home/
3 | title: "Home Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "home"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | {% include floorplan_example %}
13 |
14 | {% include assets_ref %}
15 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_light.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-light/
3 | title: "Light Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "light"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | {% include floorplan_example %}
13 |
14 | {% include assets_ref %}
15 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_multi_floor.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-multi-floor/
3 | title: "Multi Floor Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "multi_floor"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | {% include floorplan_example %}
13 |
14 | {% include assets_ref %}
15 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_remote.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-remote/
3 | title: "Remote Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "remote"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | {% include floorplan_example %}
13 |
14 | {% include assets_ref %}
15 |
--------------------------------------------------------------------------------
/docs/_docs/02-02-example_ring.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /docs/example-ring/
3 | title: "Ring Example"
4 | toc: true
5 | floorplan_example:
6 | - example_name: "ring"
7 | example_classes: "size-auto"
8 | ---
9 |
10 | ## Example
11 |
12 | {% include floorplan_example %}
13 |
14 | {% include assets_ref %}
15 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/alarm_clock.yaml:
--------------------------------------------------------------------------------
1 | page_id: alarm_clock
2 | image: /local/floorplan/examples/home-multi/alarm_clock.svg
3 | #stylesheet: /local/floorplan/examples/home-multi/alarm_clock.css
4 | log_level: warning
5 |
6 | defaults:
7 | hover_over: false
8 | more_info: false
9 |
10 | variables:
11 | - name: floorplan.hours
12 | - name: floorplan.minutes
13 |
14 | startup:
15 | action:
16 | - service: floorplan.variable_set
17 | data:
18 | variable: floorplan.hours
19 | value_template: 'return parseInt(entities["input_datetime.alarm_time"].state.slice(0, 2));'
20 | - service: floorplan.variable_set
21 | data:
22 | variable: floorplan.minutes
23 | value_template: 'return parseInt(entities["input_datetime.alarm_time"].state.slice(3, 5));'
24 |
25 | rules:
26 | - entity: input_datetime.alarm_time
27 | text_template: '${entity.state.slice(0, 5)}'
28 |
29 | - entities:
30 | - floorplan.hours
31 | - floorplan.minutes
32 | text_template: '${("0" + entity.state).slice(-2)}'
33 |
34 | - entity: floorplan.hours
35 | element: input_number.alarm_time_hours_up
36 | action:
37 | service: floorplan.variable_set
38 | data:
39 | variable: floorplan.hours
40 | value_template: '${(parseInt(entity.state) + 1) % 24}'
41 |
42 | - entity: floorplan.hours
43 | element: input_number.alarm_time_hours_down
44 | action:
45 | service: floorplan.variable_set
46 | data:
47 | variable: floorplan.hours
48 | value_template: '${((parseInt(entity.state) - 1) + 24) % 24}'
49 |
50 | - entity: floorplan.minutes
51 | element: input_number.alarm_time_minutes_up
52 | action:
53 | service: floorplan.variable_set
54 | data:
55 | variable: floorplan.minutes
56 | value_template: '${((parseInt(entity.state / 5) * 5) + 5) % 60}'
57 |
58 | - entity: floorplan.minutes
59 | element: input_number.alarm_time_minutes_down
60 | action:
61 | service: floorplan.variable_set
62 | data:
63 | variable: floorplan.minutes
64 | value_template: '${(((parseInt(entity.state / 5) * 5) - 5) + 60) % 60}'
65 |
66 | - element: save_alarm_time_button
67 | action:
68 | service: input_datetime.set_datetime
69 | data_template: '{ "entity_id": "input_datetime.alarm_time", "time": "${entities[`floorplan.hours`].state}:${entities[`floorplan.minutes`].state}" }'
70 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/main.yaml:
--------------------------------------------------------------------------------
1 | log_level: error
2 |
3 | fully_kiosk:
4 |
5 | - name: Entry Tablet
6 | address: 88:71:e5:60:a2:a8
7 | motion_sensor: binary_sensor.entry_motion
8 | plugged_sensor: binary_sensor.entry_plugged
9 | screensaver_light: light.entry_screensaver
10 | media_player: media_player.entry_alarm_panel
11 | presence_detection:
12 | location_name: Entry Hall
13 |
14 | - name: Bedroom Kiosk
15 | address: 88:71:e5:af:d6:ad
16 | motion_sensor: binary_sensor.bedroom_motion
17 | plugged_sensor: binary_sensor.bedroom_plugged
18 | screensaver_light: light.bedroom_screensaver
19 | media_player: media_player.bedroom_alarm_panel
20 | presence_detection:
21 | location_name: Bedroom
22 |
23 | pages:
24 | - /local/floorplan/examples/home-multi/master.yaml
25 | - /local/floorplan/examples/home/home.yaml
26 | - /local/floorplan/examples/home-multi/alarm_clock.yaml
27 | - /local/floorplan/examples/home-multi/squeezebox.yaml
28 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/master.css:
--------------------------------------------------------------------------------
1 | /* Page background */
2 |
3 | #master {
4 | /* background: #000; */
5 | }
6 |
7 |
8 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/master.yaml:
--------------------------------------------------------------------------------
1 | page_id: master
2 | #image: /local/floorplan/examples/home-multi/master.svg
3 | stylesheet: /local/floorplan/examples/home-multi/master.css
4 |
5 | master_page:
6 | content_element: content
7 |
8 | image:
9 | sizes:
10 | - min_width: 0
11 | location: /local/floorplan/examples/home-multi/master.svg
12 |
13 | rules:
14 |
15 | # Page navigation
16 |
17 | - element: floorplan.button_home
18 | action:
19 | service: floorplan.page_navigate
20 | data:
21 | page_id: home
22 |
23 | - element: floorplan.button_squeezebox
24 | action:
25 | service: floorplan.page_navigate
26 | data:
27 | page_id: squeezebox
28 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/squeezebox.css:
--------------------------------------------------------------------------------
1 | /* SVG elements */
2 |
3 | svg * {
4 | vector-effect: non-scaling-stroke !important;
5 | }
6 |
7 | /* Page background */
8 |
9 | #squeezebox {
10 | /* background: #000; */
11 | }
12 |
13 | /* Visibility */
14 |
15 | .visible {
16 | display: initial !important;
17 | }
18 |
19 | .hidden {
20 | display: none !important;
21 | }
22 |
23 | .hidden-enabled {
24 | visibility: hidden !important;
25 | }
26 |
27 | /* Active media item */
28 |
29 | .active-media-item {
30 | fill: #000 !important;
31 | }
32 |
33 | .active-media-item-background {
34 | fill: #63be7b !important;
35 | fill-opacity: 1 !important;
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/examples-TODO/multi/states.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - name: Simulate some binary sensors
3 | enabled: true
4 | entities:
5 | - binary_sensor.front_hallway
6 | - binary_sensor.salon
7 | - binary_sensor.back_hallway
8 | - binary_sensor.kitchen
9 | states:
10 | - state: on
11 | duration: 2
12 | - state: off
13 | duration: 5
14 |
15 | - name: Simulate even more binary sensors
16 | enabled: true
17 | entities:
18 | - binary_sensor.master_bedroom
19 | - binary_sensor.theatre_room
20 | - binary_sensor.garage
21 | - binary_sensor.garage_door
22 | - binary_sensor.laundry_door
23 | states:
24 | - state: off
25 | duration: 10
26 | - state: on
27 | duration: 1
28 |
29 | - name: Simulate cameras
30 | enabled: true
31 | entities:
32 | - camera.salon
33 | - camera.hallway
34 | - camera.driveway
35 | - camera.front_yard
36 | - camera.front_door
37 | - camera.backyard
38 | - camera.back_garden
39 | - camera.fuse_box
40 | - camera.side_of_house
41 | - binary_sensor.salon_camera_field_detection
42 | states:
43 | - state: off
44 | duration: 10
45 | - state: on
46 | duration: 10
47 |
48 | - name: Simulate a media player
49 | enabled: true
50 | entities:
51 | - media_player.salon
52 | - entity: media_player.salon
53 | attributes:
54 | friendly_name: Salon
55 | entity_picture: /local/floorplan/examples/home/images/squeezebox.svg
56 | - entity: media_player.alfresco
57 | attributes:
58 | entity_picture: /local/floorplan/examples/home/images/squeezebox.svg
59 | states:
60 | - state: playing
61 | duration: 5
62 | - state: paused
63 | duration: 5
64 |
65 | - name: Simulate weather
66 | enabled: true
67 | entities:
68 | - weather.dark_sky_hourly
69 | states:
70 | - state: rainy
71 | duration: 5
72 | - state: windy
73 | duration: 5
74 | - state: sunny
75 | duration: 5
76 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/floorplanner_home/floorplanner_home.css:
--------------------------------------------------------------------------------
1 | #floorplan {
2 | padding: 10px;
3 | }
4 |
5 | /* #floorplan svg{
6 | height: 100vh!important;
7 | } */
8 |
9 | svg tspan {
10 | fill: var(--primary-text-color);
11 | }
12 |
13 | /* Animation */
14 |
15 | .spinning {
16 | animation-name: spin;
17 | animation-duration: 5s;
18 | animation-iteration-count: infinite;
19 | animation-timing-function: linear;
20 | transform-origin: 50% 50%;
21 | transform-box: fill-box;
22 | }
23 |
24 | @keyframes spin {
25 | from {
26 | transform: rotate(0deg);
27 | }
28 | to {
29 | transform: rotate(360deg);
30 | }
31 | }
32 |
33 | /* SVG shapes */
34 |
35 | svg, svg * {
36 | vector-effect: non-scaling-stroke !important;
37 | pointer-events: all !important;
38 | }
39 |
40 | /* Hover over */
41 | .ha-entity:hover {
42 | stroke: #03A9F4 !important;
43 | stroke-width: 1px !important;
44 | stroke-opacity: 1 !important;
45 | }
46 |
47 | /* Binary sensors */
48 |
49 | .binary-sensor-on {
50 | fill: #F9D27C !important;
51 | }
52 |
53 | .binary-sensor-off {
54 | fill: #7CB1F9 !important;
55 | transition: fill 5s ease;
56 | }
57 |
58 | /* Buttons */
59 |
60 | .background-on {
61 | fill: #1ABA92 !important;
62 | }
63 |
64 | .background-off {
65 | fill: #d32f2f !important;
66 | }
67 |
68 | .background-on tspan {
69 | fill: white !important;
70 | }
71 |
72 | /* Light Control */
73 |
74 | path[id*="area."].light-on{
75 | opacity: 0 !important;
76 | }
77 |
78 | path[id*="area."]{
79 | opacity: 0.5 !important;
80 | transition: opacity .25s;
81 | -moz-transition: opacity .25s;
82 | -webkit-transition: opacity .25s;
83 | }
84 |
85 | /* Things Control */
86 |
87 | [id*="thing."].thing-on{
88 | opacity: 1 !important;
89 | filter: drop-shadow(0px -5px 3px #ffedde);
90 | fill: #fffae54f !important;
91 | }
92 |
93 | [id*="thing."]{
94 | opacity: 0 !important;
95 | transition: opacity .25s;
96 | -moz-transition: opacity .25s;
97 | -webkit-transition: opacity .25s;
98 | }
99 |
100 | /* Temperature text */
101 |
102 | .static-temp, .static-temp tspan {
103 | fill: #ffffff;
104 | }
105 |
106 | /* Spinning fan */
107 |
108 | .spinning {
109 | animation-name: spin;
110 | animation-duration: 5s;
111 | animation-iteration-count: infinite;
112 | animation-timing-function: linear;
113 | transform-origin: center;
114 | transform-box: fill-box;
115 | }
116 |
117 | @keyframes spin {
118 | from {
119 | transform: rotate(0deg);
120 | }
121 | to {
122 | transform: rotate(360deg);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/floorplanner_home/floorplanner_home.yaml:
--------------------------------------------------------------------------------
1 | title: Floorplanner Home
2 | config:
3 | image: /local/floorplan/examples/floorplanner_home/floorplanner_home.svg
4 | stylesheet: /local/floorplan/examples/floorplanner_home/floorplanner_home.css
5 |
6 | defaults:
7 | hover_action: hover-info
8 | tap_action: more-info
9 |
10 | rules:
11 | - name: Rooms
12 | entities:
13 | - entity: light.udestue
14 | element: area.udestue
15 | - entity: light.restroom
16 | element: area.restroom
17 | - entity: light.kitchen
18 | element: area.kitchen
19 | - entity: light.guestroom
20 | element: area.guestroom
21 | - entity: light.livingroom
22 | element: area.livingroom
23 | - entity: light.office
24 | element: area.office
25 | - entity: light.hallway
26 | element: area.hallway
27 | - entity: light.guesttoilet
28 | element: area.guesttoilet
29 | - entity: light.bedroom
30 | element: area.bedroom
31 | tap_action: light.toggle
32 | state_action:
33 | service: floorplan.class_set
34 | service_data: '${(entity.state === "on") ? "light-on" : "light-off"}'
35 |
36 | - name: Temperature
37 | entities:
38 | - sensor.hallway
39 | - sensor.livingroom
40 | - sensor.udestue
41 | state_action:
42 | - service: floorplan.text_set
43 | service_data: '${(entity.state !== undefined) ? Math.round(entity.state * 10) / 10 + "°" : "unknown"}'
44 | - service: floorplan.class_set
45 | service_data:
46 | class: 'static-temp'
47 |
48 | - entity: switch.udestue_fan
49 | tap_action: toggle
50 | state_action:
51 | service: floorplan.class_set
52 | service_data: '${(entity.state === "on") ? "spinning" : ""}'
53 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/floorplanner_home/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entities:
3 | - light.kitchen
4 | - light.livingroom
5 | - light.office
6 | - light.hallway
7 | states:
8 | - state: on
9 |
10 | - entities:
11 | - light.udestue
12 | - light.restroom
13 | - light.guestroom
14 | - light.guesttoilet
15 | - light.bedroom
16 | states:
17 | - state: off
18 |
19 | - entity: sensor.hallway
20 | states:
21 | - state: 23.3
22 | duration: 1
23 | - state: 23.2
24 | duration: 1
25 | - state: 23.0
26 | duration: 1
27 | - state: 23.3
28 | duration: 2
29 |
30 | - entity: sensor.livingroom
31 | states:
32 | - state: 24.4
33 | duration: 1
34 | - state: 24.3
35 | duration: 1
36 | - state: 24.2
37 | duration: 1
38 | - state: 24.3
39 | duration: 2
40 |
41 | - entity: sensor.udestue
42 | states:
43 | - state: 25.7
44 | duration: 1
45 | - state: 25.6
46 | duration: 1
47 | - state: 25.5
48 | duration: 1
49 | - state: 25.6
50 | duration: 2
51 |
52 | - entity: switch.udestue_fan
53 | states:
54 | - state: on
55 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/home/home.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan {
4 | padding: 10px;
5 | }
6 |
7 | #floorplan>svg {
8 | max-width: 700px;
9 | }
10 |
11 | /* SVG elements */
12 |
13 | svg * {
14 | vector-effect: non-scaling-stroke !important;
15 | }
16 |
17 | /* Hover over */
18 |
19 | .floorplan-shape:hover,
20 | g.floorplan-hover> :not(text):hover,
21 | g.floorplan-click> :not(text):hover,
22 | g.floorplan-long-click> :not(text):hover,
23 | :not(text).floorplan-hover:hover,
24 | :not(text).floorplan-click:hover,
25 | :not(text).floorplan-long-click:hover {
26 | stroke: #03a9f4 !important;
27 | stroke-width: 1px !important;
28 | stroke-opacity: 1 !important;
29 | }
30 |
31 | /* Animation */
32 |
33 | .spinning {
34 | animation-name: spin;
35 | animation-duration: 5s;
36 | animation-iteration-count: infinite;
37 | animation-timing-function: linear;
38 | transform-origin: 50% 50%;
39 | transform-box: fill-box;
40 | }
41 |
42 | @keyframes spin {
43 | from {
44 | transform: rotate(0deg);
45 | }
46 |
47 | to {
48 | transform: rotate(360deg);
49 | }
50 | }
51 |
52 | /* Binary sensors */
53 |
54 | .binary-sensor-on {
55 | fill: #f9d27c !important;
56 | }
57 |
58 | .binary-sensor-off {
59 | fill: #7cb1f9 !important;
60 | transition: fill 5s ease;
61 | }
62 |
63 | /* Buttons */
64 |
65 | .button-on rect {
66 | fill: #1aba92 !important;
67 | }
68 |
69 | .button-off rect {
70 | fill: #d32f2f !important;
71 | }
72 |
73 | .button-on tspan,
74 | .button-off tspan {
75 | fill: white !important;
76 | }
77 |
78 | /* Lights */
79 |
80 | .light-on * {
81 | /* Nothing to do */
82 | }
83 |
84 | .light-off * {
85 | -webkit-filter: grayscale(100%);
86 | filter: grayscale(100%);
87 | filter: gray;
88 | }
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/home/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entity: sensor.wind_direction
3 | states: |
4 | >
5 | var MIN = 0;
6 | var MAX = 360;
7 | var STEP = 10;
8 |
9 | var currentLevel = entity.state ? entity.state : MIN;
10 |
11 | var level = (currentLevel + STEP) % MAX;
12 |
13 | return {
14 | state: level,
15 | duration: '25ms'
16 | };
17 |
18 | - entity: sensor.test
19 | states: |
20 | >
21 | var MIN = 0;
22 | var MAX = 100;
23 | var STEP = 1;
24 |
25 | var currentLevel = entity.attributes ? entity.attributes.level : MIN;
26 | var currentIsAscending = entity.attributes ? entity.attributes.isAscending : true;
27 |
28 | var level = (currentIsAscending && (currentLevel < MAX)) || (!currentIsAscending && (currentLevel <= MIN)) ?
29 | currentLevel + STEP : currentLevel - STEP;
30 |
31 | var isAscending = (currentIsAscending && (currentLevel >= MAX)) ? false :
32 | ((!currentIsAscending && (currentLevel <= MIN)) ? true : currentIsAscending);
33 |
34 | return {
35 | state: 'on',
36 | attributes: { level: level, isAscending: isAscending },
37 | duration: '25ms'
38 | };
39 |
40 | - entity: sensor.moisture_level
41 | states: |
42 | >
43 | var MIN = 0;
44 | var MAX = 100;
45 | var STEP = 1;
46 |
47 | var currentLevel = entity.attributes ? entity.attributes.level : MIN;
48 | var currentIsAscending = entity.attributes ? entity.attributes.isAscending : true;
49 |
50 | var level = (currentIsAscending && (currentLevel < MAX)) || (!currentIsAscending && (currentLevel <= MIN)) ?
51 | currentLevel + STEP : currentLevel - STEP;
52 |
53 | var isAscending = (currentIsAscending && (currentLevel >= MAX)) ? false :
54 | ((!currentIsAscending && (currentLevel <= MIN)) ? true : currentIsAscending);
55 |
56 | return {
57 | state: 'on',
58 | attributes: { level: level, isAscending: isAscending },
59 | duration: '25ms'
60 | };
61 |
62 | - entity:
63 | entity_id: binary_sensor.living_area
64 | state: on
65 | attributes:
66 | foo: bar
67 | states:
68 | - state: on
69 | duration: 3
70 | - state: off
71 | duration: 2
72 |
73 | - entity: binary_sensor.main_bedroom
74 | states:
75 | - state: on
76 | duration: 5
77 | - state: off
78 | duration: 3
79 |
80 | - entity: binary_sensor.garage
81 | states:
82 | - state: on
83 | duration: 4
84 | - state: off
85 | duration: 2
86 |
87 | - entities:
88 | - light.garage
89 | - light.main_bedroom
90 | - switch.living_area_fan
91 | states:
92 | - state: on
93 |
94 | - entity: camera.zagreb
95 | states:
96 | - state: idle
97 | attributes:
98 | entity_picture: https://cdn.whatsupcams.com/snapshot/hr_zagreb5.jpg
99 | duration: 3
100 | - state: idle
101 | attributes:
102 | entity_picture: https://cdn.whatsupcams.com/snapshot/hr_zagreb6.jpg
103 | duration: 3
104 | - state: idle
105 | attributes:
106 | entity_picture: https://cdn.whatsupcams.com/snapshot/hr_zagreb7.jpg
107 | duration: 3
108 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/light/light.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan > svg {
4 | max-width: 200px;
5 | }
6 |
7 | /* SVG elements */
8 |
9 | svg * {
10 | vector-effect: non-scaling-stroke !important;
11 | }
12 |
13 | /* Hover over */
14 |
15 | .floorplan-shape:hover,
16 | g.floorplan-hover > :not(text):hover,
17 | g.floorplan-click > :not(text):hover,
18 | g.floorplan-long-click > :not(text):hover,
19 | :not(text).floorplan-hover:hover,
20 | :not(text).floorplan-click:hover,
21 | :not(text).floorplan-long-click:hover {
22 | stroke: #03a9f4 !important;
23 | stroke-width: 1px !important;
24 | stroke-opacity: 1 !important;
25 | }
26 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/light/light.yaml:
--------------------------------------------------------------------------------
1 | title: Light
2 | config:
3 | image:
4 | location: /local/floorplan/examples/light/light.svg
5 | cache: true
6 | stylesheet:
7 | location: /local/floorplan/examples/light/light.css
8 | cache: true
9 |
10 | defaults:
11 | hover_action:
12 | action: hover-info
13 | tap_action:
14 | action: more-info
15 |
16 | rules:
17 | - entities:
18 | - light.kitchen
19 | - light.office
20 | tap_action:
21 | action: navigate
22 | navigation_path: /lovelace/lights
23 | state_action:
24 | - action: call-service
25 | service: floorplan.style_set
26 | service_data: |
27 | >
28 | var elements = [
29 | `${entity.entity_id}`,
30 | `${entity.entity_id}.gradient_color_0`,
31 | ];
32 |
33 | var color = 'rgb(0, 0, 0)';
34 | var opacity = 0;
35 |
36 | if (entity.state === 'on') {
37 | if (entity.attributes.color_temp) {
38 | var rgb = util.color.miredToRGB(entity.attributes.color_temp);
39 | color = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
40 | opacity = 1;
41 | }
42 | else if (entity.attributes.rgb_color) {
43 | var rgb = entity.attributes.rgb_color;
44 | color = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
45 | opacity = 1;
46 | }
47 | }
48 | var style = `fill: ${color}; stop-color: ${color}; stop-opacity: ${opacity};`;
49 |
50 | return {
51 | elements: elements,
52 | style: style,
53 | };
54 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/light/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entities:
3 | - light.kitchen
4 | states:
5 | - state: on
6 | attributes:
7 | rgb_color:
8 | - 252
9 | - 191
10 | - 123
11 | duration: 0.25
12 | - state: on
13 | attributes:
14 | rgb_color:
15 | - 254
16 | - 220
17 | - 129
18 | duration: 0.25
19 | - state: on
20 | attributes:
21 | rgb_color:
22 | - 238
23 | - 230
24 | - 131
25 | duration: 0.25
26 | - state: on
27 | attributes:
28 | rgb_color:
29 | - 204
30 | - 221
31 | - 130
32 | duration: 0.25
33 | - state: on
34 | attributes:
35 | rgb_color:
36 | - 169
37 | - 210
38 | - 127
39 | duration: 0.25
40 | - state: on
41 | attributes:
42 | rgb_color:
43 | - 204
44 | - 221
45 | - 130
46 | duration: 0.25
47 | - state: on
48 | attributes:
49 | rgb_color:
50 | - 238
51 | - 230
52 | - 131
53 | duration: 0.25
54 | - state: on
55 | attributes:
56 | rgb_color:
57 | - 254
58 | - 220
59 | - 129
60 | duration: 0.25
61 | - state: on
62 | attributes:
63 | rgb_color:
64 | - 252
65 | - 191
66 | - 123
67 | duration: 0.25
68 | - state: on
69 | attributes:
70 | rgb_color:
71 | - 251
72 | - 162
73 | - 118
74 | duration: 0.25
75 |
76 | - entities:
77 | - light.office
78 | states:
79 | - state: on
80 | attributes:
81 | rgb_color:
82 | - 251
83 | - 162
84 | - 118
85 | duration: 0.25
86 | - state: on
87 | attributes:
88 | rgb_color:
89 | - 252
90 | - 191
91 | - 123
92 | duration: 0.25
93 | - state: on
94 | attributes:
95 | rgb_color:
96 | - 254
97 | - 220
98 | - 129
99 | duration: 0.25
100 | - state: on
101 | attributes:
102 | rgb_color:
103 | - 238
104 | - 230
105 | - 131
106 | duration: 0.25
107 | - state: on
108 | attributes:
109 | rgb_color:
110 | - 204
111 | - 221
112 | - 130
113 | duration: 0.25
114 | - state: on
115 | attributes:
116 | rgb_color:
117 | - 169
118 | - 210
119 | - 127
120 | duration: 0.25
121 | - state: on
122 | attributes:
123 | rgb_color:
124 | - 204
125 | - 221
126 | - 130
127 | duration: 0.25
128 | - state: on
129 | attributes:
130 | rgb_color:
131 | - 238
132 | - 230
133 | - 131
134 | duration: 0.25
135 | - state: on
136 | attributes:
137 | rgb_color:
138 | - 254
139 | - 220
140 | - 129
141 | duration: 0.25
142 | - state: on
143 | attributes:
144 | rgb_color:
145 | - 252
146 | - 191
147 | - 123
148 | duration: 0.25
149 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/multi_floor/multi_floor.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan {
4 | padding: 10px;
5 | }
6 |
7 | #floorplan > svg {
8 | max-width: 700px;
9 | }
10 |
11 | /* SVG elements */
12 |
13 | svg * {
14 | vector-effect: non-scaling-stroke !important;
15 | }
16 |
17 | /* Hover over */
18 |
19 | .floorplan-shape:hover,
20 | g.floorplan-hover > :not(text):hover,
21 | g.floorplan-click > :not(text):hover,
22 | g.floorplan-long-click > :not(text):hover,
23 | :not(text).floorplan-hover:hover,
24 | :not(text).floorplan-click:hover,
25 | :not(text).floorplan-long-click:hover {
26 | stroke: #03a9f4 !important;
27 | stroke-width: 1px !important;
28 | stroke-opacity: 1 !important;
29 | }
30 |
31 | /* Layers */
32 |
33 | .layer-visible {
34 | display: inline !important;
35 | }
36 |
37 | .layer-hidden {
38 | display: none !important;
39 | }
40 |
41 | /* Binary sensors */
42 |
43 | .binary-sensor-on {
44 | fill: #f9d27c !important;
45 | }
46 |
47 | .binary-sensor-off {
48 | fill: #7cb1f9 !important;
49 | transition: fill 5s ease;
50 | }
51 |
52 | /* Buttons */
53 |
54 | .button-on rect {
55 | fill: #1aba92 !important;
56 | }
57 |
58 | /*
59 | .button-off rect {
60 | fill: #d32f2f !important;
61 | }
62 | */
63 |
64 | .button-on tspan,
65 | .button-off tspan {
66 | fill: white !important;
67 | }
68 |
69 |
70 | /* Hover */
71 |
72 | .hover {
73 | stroke: red !important;
74 | stroke-width: 10px !important;
75 | }
76 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/multi_floor/multi_floor.yaml:
--------------------------------------------------------------------------------
1 | sidebar_title: Multi Floor
2 | config:
3 | show_side_bar: false
4 | show_app_header: false
5 | config:
6 | image: /local/floorplan/examples/multi_floor/multi_floor.svg
7 | stylesheet: /local/floorplan/examples/multi_floor/multi_floor.css
8 | # log_level: info
9 | console_log_level: info
10 |
11 | defaults:
12 | hover_action: hover-info
13 | tap_action: more-info
14 |
15 | startup_action:
16 | - service: floorplan.class_set
17 | service_data:
18 | element: ground_floor
19 | class: layer-visible
20 | - service: floorplan.class_set
21 | service_data:
22 | elements:
23 | - first_floor
24 | class: layer-hidden
25 | - service: floorplan.class_set
26 | service_data:
27 | element: ground_floor.button
28 | class: button-on
29 | - service: floorplan.class_set
30 | service_data:
31 | elements:
32 | - first_floor.button
33 | class: button-off
34 | - service: floorplan.text_set
35 | service_data:
36 | element: sample.multilinegroup_text
37 | shift_y_axis: 1.5em
38 | text: |
39 | > /* Split text to two tspans*/
40 | return 'Multiline\nTSPAN-Print';
41 |
42 | rules:
43 | - element: ground_floor.button
44 | tap_action:
45 | - service: floorplan.class_set
46 | service_data:
47 | element: ground_floor
48 | class: layer-visible
49 | - service: floorplan.class_set
50 | service_data:
51 | elements:
52 | - first_floor
53 | class: layer-hidden
54 | - service: floorplan.class_set
55 | service_data:
56 | element: ground_floor.button
57 | class: button-on
58 | - service: floorplan.class_set
59 | service_data:
60 | elements:
61 | - first_floor.button
62 | class: button-off
63 |
64 | - element: first_floor.button
65 | tap_action:
66 | - service: floorplan.class_set
67 | service_data:
68 | element: first_floor
69 | class: layer-visible
70 | - service: floorplan.class_set
71 | service_data:
72 | elements:
73 | - ground_floor
74 | class: layer-hidden
75 | - service: floorplan.class_set
76 | service_data:
77 | element: first_floor.button
78 | class: button-on
79 | - service: floorplan.class_set
80 | service_data:
81 | elements:
82 | - ground_floor.button
83 | class: button-off
84 |
85 | - entities:
86 | - binary_sensor.garage
87 | - binary_sensor.activity
88 | state_action:
89 | - service: floorplan.class_set
90 | service_data: '${ entity.state === "on" ? "binary-sensor-on" : "binary-sensor-off"}'
91 |
92 | - entities:
93 | - binary_sensor.garage
94 | hover_action:
95 | - service: floorplan.class_set
96 | service_data: '${element.matches(":hover") ? "hover" : ""}'
97 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/multi_floor/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entity:
3 | entity_id: binary_sensor.activity
4 | state: on
5 | attributes:
6 | foo: bar
7 | states:
8 | - state: on
9 | duration: 3
10 | - state: off
11 | duration: 2
12 |
13 | - entity: binary_sensor.garage
14 | states:
15 | - state: on
16 | duration: 4
17 | - state: off
18 | duration: 2
19 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/remote/remote.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan > svg {
4 | max-width: 600px;
5 | }
6 |
7 | /* SVG elements */
8 |
9 | svg * {
10 | vector-effect: non-scaling-stroke !important;
11 | }
12 |
13 | /* Hover over */
14 |
15 | .floorplan-shape:hover,
16 | g.floorplan-hover > :not(text):hover,
17 | g.floorplan-click > :not(text):hover,
18 | g.floorplan-long-click > :not(text):hover,
19 | :not(text).floorplan-hover:hover,
20 | :not(text).floorplan-click:hover,
21 | :not(text).floorplan-long-click:hover {
22 | stroke: #03a9f4 !important;
23 | stroke-width: 1px !important;
24 | stroke-opacity: 1 !important;
25 | }
26 |
27 | /* TV power (fill) */
28 |
29 | .tv-on-fill {
30 | fill: green !important;
31 | }
32 |
33 | .tv-off-fill {
34 | fill: red !important;
35 | }
36 |
37 | /* TV power (outline) */
38 |
39 | .tv-on-outline {
40 | stroke: green !important;
41 | }
42 |
43 | .tv-off-outline {
44 | stroke: red !important;
45 | }
46 |
47 | /* TV content */
48 |
49 | .show {
50 | visibility: visible !important;
51 | }
52 |
53 | .hide {
54 | visibility: hidden !important;
55 | }
56 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/remote/remote.yaml:
--------------------------------------------------------------------------------
1 | title: Remote
2 | config:
3 | image: /local/floorplan/examples/remote/remote.svg
4 | stylesheet: /local/floorplan/examples/remote/remote.css
5 |
6 | defaults:
7 | hover_action: hover-info
8 | tap_action: more-info
9 |
10 | rules:
11 | - entity: media_player.tv
12 | state_action:
13 | - action: call-service
14 | service: floorplan.text_set
15 | service_data: ${entity.state.toUpperCase()}
16 | - action: call-service
17 | service: floorplan.class_set
18 | service_data: 'tv-${entity.state}-fill'
19 | - action: call-service
20 | service: floorplan.class_set
21 | service_data:
22 | elements:
23 | - tv_rect
24 | - power_rect
25 | class: 'tv-${entity.state}-outline'
26 | - action: call-service
27 | service: floorplan.text_set
28 | service_data:
29 | element: media_player.tv.volume_level
30 | text: '${(entity.attributes.volume_level * 100).toFixed(0)}%'
31 | - action: call-service
32 | service: floorplan.class_set
33 | service_data:
34 | element: media_player.tv.volume_group
35 | class: '${entity.state === "on" ? "show" : "hide" }'
36 |
37 | - elements:
38 | - button.power
39 | - button.tv
40 | entity: media_player.tv
41 | tap_action:
42 | action: call-service
43 | service: homeassistant.toggle
44 |
45 | - element: button.volume_up
46 | entity: media_player.tv
47 | tap_action:
48 | action: call-service
49 | service: media_player.volume_up
50 |
51 | - element: button.volume_down
52 | entity: media_player.tv
53 | tap_action:
54 | action: call-service
55 | service: media_player.volume_down
56 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/remote/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entities:
3 | - media_player.tv
4 | states:
5 | - state: off
6 | attributes:
7 | volume_level: 0.5
8 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/ring/ring.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan {
4 | margin: 0px 10px 10px 10px;
5 | }
6 |
7 | #floorplan > svg {
8 | max-width: 200px;
9 | }
10 |
11 | /* SVG elements */
12 |
13 | svg * {
14 | vector-effect: non-scaling-stroke !important;
15 | }
16 |
17 | /* Hover over */
18 |
19 | .floorplan-shape:hover,
20 | g.floorplan-hover > :not(text):hover,
21 | g.floorplan-click > :not(text):hover,
22 | g.floorplan-long-click > :not(text):hover,
23 | :not(text).floorplan-hover:hover,
24 | :not(text).floorplan-click:hover,
25 | :not(text).floorplan-long-click:hover {
26 | stroke: #03A9F4 !important;
27 | stroke-width: 1px !important;
28 | stroke-opacity: 1 !important;
29 | }
30 |
31 | /* Ring doorbell */
32 |
33 | .ring-motion {
34 | fill: #F9D27C !important;
35 | fill-opacity: 1 !important;
36 | }
37 |
38 | .ring-ding {
39 | stroke: #379FD4;
40 | stroke-width: 5;
41 | stroke-linecap: round;
42 | animation: dash 1.5s ease-in-out infinite;
43 | }
44 |
45 | @keyframes rotate {
46 | 100% {
47 | transform: rotate(360deg);
48 | }
49 | }
50 |
51 | @keyframes dash {
52 | 0% {
53 | stroke-dasharray: 1, 150;
54 | stroke-dashoffset: 0;
55 | }
56 | 50% {
57 | stroke-dasharray: 90, 150;
58 | stroke-dashoffset: -35;
59 | }
60 | 100% {
61 | stroke-dasharray: 90, 150;
62 | stroke-dashoffset: -124;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/ring/ring.yaml:
--------------------------------------------------------------------------------
1 | title: Ring
2 | config:
3 | image: /local/floorplan/examples/ring/ring.svg
4 | stylesheet: /local/floorplan/examples/ring/ring.css
5 | log_level: info
6 |
7 | defaults:
8 | hover_action:
9 | action: hover-info
10 | tap_action: more-info
11 |
12 | functions: |
13 | >
14 | return {
15 |
16 | getPercentageFill: (entity) => {
17 | var max = [195, 232, 141];
18 | var min = [240, 113, 120];
19 | var r = Math.floor(min[0] + ((max[0] - min[0]) * (entity.state / 100)));
20 | var g = Math.floor(min[1] + ((max[1] - min[1]) * (entity.state / 100)));
21 | var b = Math.floor(min[2] + ((max[2] - min[2]) * (entity.state / 100)));
22 | return `fill: rgb(${r}, ${g}, ${b})`;
23 | },
24 |
25 | someOtherFunctionA: (entity, entities, hass) => {
26 | return 'foo';
27 | },
28 |
29 | someOtherFunctionB: (entity, entities, hass) => {
30 | return 'bar';
31 | },
32 |
33 | };
34 |
35 | rules:
36 | - entity: sensor.ring_salon_battery
37 | state_action:
38 | - service: floorplan.text_set
39 | service_data:
40 | element: sensor.ring_salon_battery
41 | text: |
42 | >
43 | return (entity.state !== undefined) ? entity.state + "%" : "unknown";
44 | - service: floorplan.style_set
45 | service_data: ${functions.getPercentageFill(entity)}
46 |
47 | - entities:
48 | - binary_sensor.ring_salon_motion
49 | state_action:
50 | service: floorplan.class_set
51 | service_data:
52 | class: '${(entity.state === "on") ? "ring-motion" : ""}'
53 |
54 | - entity: binary_sensor.ring_salon_ding
55 | state_action:
56 | service: floorplan.class_set
57 | service_data:
58 | class: |
59 | >
60 | switch (entity.state) {
61 | case "on":
62 | return "ring-ding";
63 |
64 | case "off":
65 | return "";
66 |
67 | default:
68 | return "";
69 | }
70 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/ring/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entity: binary_sensor.ring_salon_motion
3 | states:
4 | - state: on
5 | duration: 3
6 | - state: off
7 | duration: 3
8 |
9 | - entity: binary_sensor.ring_salon_ding
10 | states:
11 | - state: on
12 | duration: 2
13 | - state: off
14 | duration: 2
15 |
16 | - entity: sensor.ring_salon_battery
17 | states:
18 | - state: 100.0
19 | duration: 1
20 | - state: 99.9
21 | duration: 1
22 | - state: 88.8
23 | duration: 1
24 | - state: 77.7
25 | duration: 1
26 | - state: 65.5
27 | duration: 1
28 | - state: 55.5
29 | duration: 1
30 | - state: 44.4
31 | duration: 1
32 | - state: 33.3
33 | duration: 1
34 | - state: 22.2
35 | duration: 1
36 | - state: 11.1
37 | duration: 1
38 | - state: 00.0
39 | duration: 1
40 | - state: 11.1
41 | duration: 1
42 | - state: 22.2
43 | duration: 1
44 | - state: 33.3
45 | duration: 1
46 | - state: 44.4
47 | duration: 1
48 | - state: 55.5
49 | duration: 1
50 | - state: 65.5
51 | duration: 1
52 | - state: 65.5
53 | duration: 1
54 | - state: 77.7
55 | duration: 1
56 | - state: 88.8
57 | duration: 1
58 | - state: 99.9
59 | duration: 1
60 | - state: 100.0
61 | duration: 1
62 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/rinnai/rinnai.css:
--------------------------------------------------------------------------------
1 | /* SVG size */
2 |
3 | #floorplan {
4 | margin: 0px 10px 10px 10px;
5 | }
6 |
7 | #floorplan > svg {
8 | /* max-width: 200px; */
9 | }
10 |
11 | /* SVG elements */
12 |
13 | svg * {
14 | vector-effect: non-scaling-stroke !important;
15 | }
16 |
17 | /* Hover over */
18 |
19 | .floorplan-shape:hover,
20 | g.floorplan-hover > :not(text):hover,
21 | g.floorplan-click > :not(text):hover,
22 | g.floorplan-long-click > :not(text):hover,
23 | :not(text).floorplan-hover:hover,
24 | :not(text).floorplan-click:hover,
25 | :not(text).floorplan-long-click:hover {
26 | stroke: #03A9F4 !important;
27 | stroke-width: 1px !important;
28 | stroke-opacity: 1 !important;
29 | }
30 |
31 | /* Button on */
32 |
33 | .button-on .button-background {
34 | fill: #accee9 !important;
35 | }
36 |
37 | .button-on .button-icon,
38 | .button-on .button-icon tspan,
39 | .button-on .button-text tspan {
40 | fill: #060c31 !important;
41 | }
42 |
43 | /* Button off */
44 |
45 | .button-off .button-background {
46 | fill: #1e3b6c !important;
47 | }
48 |
49 | .button-off .button-icon,
50 | .button-off .button-icon tspan,
51 | .button-off .button-text tspan {
52 | fill: #accee9 !important;
53 | }
54 |
55 | /* Button disabled */
56 |
57 | .button-disabled .button-background {
58 | fill: #112550 !important;
59 | }
60 |
61 | .button-disabled .button-icon,
62 | .button-disabled .button-text tspan {
63 | fill: #3e557f !important;
64 | }
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/rinnai/rinnai.yaml:
--------------------------------------------------------------------------------
1 | title: Rinnai
2 | config:
3 | image: /local/floorplan/examples/rinnai/rinnai.svg
4 | stylesheet: /local/floorplan/examples/rinnai/rinnai.css
5 |
6 | defaults:
7 | hover_action: hover-info
8 | tap_action: more-info
9 |
10 | rules:
11 | - entities:
12 | - entity: binary_sensor.hvac_on_off_sensor
13 | element: button_power
14 | - entity: binary_sensor.hvac_heat_mode_sensor
15 | element: button_heat
16 | - entity: binary_sensor.hvac_cool_mode_sensor
17 | element: button_cool
18 | - entity: binary_sensor.hvac_fan_mode_sensor
19 | element: button_fan
20 | - entity: binary_sensor.hvac_zone_a_sensor
21 | element: button_zone_a
22 | - entity: binary_sensor.hvac_zone_b_sensor
23 | element: button_zone_b
24 | - entity: binary_sensor.hvac_zone_c_sensor
25 | element: button_zone_c
26 | tap_action: toggle
27 | state_action:
28 | service: floorplan.class_set
29 | service_data: '${(entity.state === "on") ? "button-on" : "button-off"}'
30 |
31 | - entities:
32 | - entity: binary_sensor.hvac_heat_mode_sensor
33 | element: image_heat
34 | - entity: binary_sensor.hvac_cool_mode_sensor
35 | element: image_cool
36 | - entity: binary_sensor.hvac_fan_mode_sensor
37 | element: image_fan
38 | - entity: binary_sensor.hvac_zone_a_sensor
39 | element: image_zone_a
40 | - entity: binary_sensor.hvac_zone_b_sensor
41 | element: image_zone_b
42 | - entity: binary_sensor.hvac_zone_c_sensor
43 | element: image_zone_c
44 | state_action:
45 | service: floorplan.style_set
46 | service_data: 'display: ${(entity.state === "on") ? "block" : "none"}'
47 |
48 | - entities:
49 | # - sensor.hvac_current_temperature
50 | - sensor.hvac_set_temperature
51 | state_action: floorplan.text_set
52 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/rinnai/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entity: binary_sensor.hvac_on_off_sensor
3 | state: off
4 |
5 | - entity: binary_sensor.hvac_heat_mode_sensor
6 | state: on
7 |
8 | - entity: binary_sensor.hvac_cool_mode_sensor
9 | state: off
10 |
11 | - entity: binary_sensor.hvac_fan_mode_sensor
12 | state: off
13 |
14 | - entity: binary_sensor.hvac_zone_a_sensor
15 | state: off
16 |
17 | - entity: binary_sensor.hvac_zone_b_sensor
18 | state: off
19 |
20 | - entity: binary_sensor.hvac_zone_c_sensor
21 | state: off
22 |
23 | - entity: sensor.hvac_mode
24 | state: heat
25 |
26 | - entity: sensor.hvac_mode_icon
27 | state: heat
28 |
29 | - entity: sensor.hvac_current_temperature
30 | state: 24.5
31 |
32 | - entity: sensor.hvac_set_temperature
33 | state: 22.0
34 |
35 | - entity: binary_sensor.hvac_fan_speed
36 | state: 2
37 |
38 | - entity: sensor.hvac_zone_a
39 | state: off
40 |
41 | - entity: sensor.hvac_zone_b
42 | state: off
43 |
44 | - entity: sensor.hvac_zone_c
45 | state: off
46 |
47 | - entity: sensor.hvac_wifimodule_status
48 | state: ok
49 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/test_plate/simulations.yaml:
--------------------------------------------------------------------------------
1 | simulations:
2 | - entity: sensor.int_0_to_100
3 | states: |
4 | >
5 | var MIN = 0;
6 | var MAX = 100;
7 | var STEP = 1;
8 |
9 | var currentLevel = entity.attributes ? entity.attributes.level : MIN;
10 |
11 | var level = (currentLevel + STEP) > MAX ? MIN : currentLevel + STEP;
12 |
13 | var isAscending = true;
14 |
15 | return {
16 | state: 'on',
17 | attributes: { level: level, isAscending: isAscending },
18 | duration: '25ms'
19 | };
20 |
21 | - entity: sensor.warning_level
22 | states: |
23 | >
24 | var MIN = 0;
25 | var MAX = 100;
26 | var STEP = 1;
27 |
28 | var currentLevel = entity.attributes ? entity.attributes.level : MIN;
29 | var currentIsAscending = entity.attributes ? entity.attributes.isAscending : true;
30 |
31 | var level = (currentIsAscending && (currentLevel < MAX)) || (!currentIsAscending && (currentLevel <= MIN)) ?
32 | currentLevel + STEP : currentLevel - STEP;
33 |
34 | var isAscending = (currentIsAscending && (currentLevel >= MAX)) ? false :
35 | ((!currentIsAscending && (currentLevel <= MIN)) ? true : currentIsAscending);
36 |
37 | return {
38 | state: 'on',
39 | attributes: { level: level, isAscending: isAscending },
40 | duration: '25ms'
41 | };
42 |
43 | - entity:
44 | entity_id: sensor.temperature_living_area
45 | state: 20
46 | states:
47 | - state: 23
48 | duration: 1
49 | - state: 40
50 | duration: 1
51 | - state: 10
52 | duration: 1
53 |
54 | - entity:
55 | entity_id: light.living_area
56 | state: on
57 | states:
58 | - state: on
59 | duration: 1
60 | - state: off
61 | duration: 2
62 |
63 | - entity:
64 | entity_id: sensor.random_text
65 | state: on
66 | states:
67 | - state: 'Hello'
68 | duration: 3
69 | - state: 'World'
70 | duration: 3
71 |
72 | - entity:
73 | entity_id: sensor.empty_text
74 | state: ''
75 | states:
76 | - state: ''
77 | duration: 1
78 |
79 | - entity: binary_sensor.radar_bg
80 | states:
81 | - state: on
82 | duration: 1
83 | - state: off
84 | duration: 1
--------------------------------------------------------------------------------
/docs/_docs/floorplan/examples/test_plate/test_plate.css:
--------------------------------------------------------------------------------
1 | #temp-icons.temp-warning #temp-warning {
2 | display: block!important;
3 | fill: red;
4 | }
5 |
6 | #temp-icons.temp-cold #temp-cold {
7 | display: block!important;
8 | fill: rgb(0, 191, 255);
9 | }
10 |
11 | #temp-icons.temp-ok #temp-ok {
12 | display: block!important;
13 | fill: rgb(94, 221, 162);
14 | }
15 |
16 | .visible {
17 | display: block;
18 | }
19 |
20 | .hidden {
21 | display: none;
22 | }
23 |
24 | .radar-bg-opacity-dimmed{
25 | animation: fadeIn 1s ease-in-out;
26 | }
27 |
28 | @keyframes fadeIn {
29 | 0%, 100% {
30 | opacity: 1;
31 | }
32 | 50% {
33 | opacity: 0.5;
34 | }
35 | }
36 |
37 | #group-row-1{
38 | opacity:0.7;
39 |
40 | }
41 |
42 | #group-row-1.hover_action_class{
43 | opacity:1;
44 | }
--------------------------------------------------------------------------------
/docs/_docs/floorplan/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/_docs/floorplan/favicon.ico
--------------------------------------------------------------------------------
/docs/_docs/floorplan/index-test_plate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Floorplan Examples
6 |
7 |
8 |
9 |
10 |
23 |
24 |
25 |
26 |
27 | Floorplan Examples
28 | Go to all examples
29 |
30 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/_docs/floorplan/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Floorplan Examples
6 |
7 |
8 |
9 |
10 |
11 |
24 |
25 |
26 |
27 |
28 | Floorplan Examples
29 | Go to test_plate-example
30 |
31 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/_includes/assets_ref:
--------------------------------------------------------------------------------
1 | ## Assets
2 |
3 | All assets can be found in the [ha-floorplan repository on GitHub](https://github.com/ExperienceLovelace/ha-floorplan/tree/master/docs/_docs/floorplan/examples).
4 |
5 | Here you'll find the `.svg`, `.css` and `.yaml` used in the example.
6 |
--------------------------------------------------------------------------------
/docs/_includes/feature_row:
--------------------------------------------------------------------------------
1 | {% if include.id %}
2 | {% assign feature_row = page[include.id] %}
3 | {% else %}
4 | {% assign feature_row = page.feature_row %}
5 | {% endif %}
6 |
7 |
8 |
9 | {% for f in feature_row %}
10 |
11 | {% if f.url contains "://" %}
12 | {% capture f_url %}{{ f.url }}{% endcapture %}
13 | {% else %}
14 | {% capture f_url %}{{ f.url | absolute_url }}{% endcapture %}
15 | {% endif %}
16 |
17 |
55 | {% endfor %}
56 |
57 |
58 |
--------------------------------------------------------------------------------
/docs/_includes/floorplan_example:
--------------------------------------------------------------------------------
1 | {% assign floorplan_example = page.floorplan_example %}
2 |
3 |
4 |
5 | {% for e in floorplan_example %}
6 |
7 | {% if e.example_name %}
8 |
9 |
10 |
11 |
12 |
13 |
14 | Floorplan
15 | YAML
16 | CSS
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% highlight yaml %}
28 | {% include_relative floorplan/examples/{{ e.example_name }}/{{ e.example_name }}.yaml %}
29 | {% endhighlight %}
30 |
31 |
32 |
33 |
34 |
35 | {% highlight css %}
36 | {% include_relative floorplan/examples/{{ e.example_name }}/{{ e.example_name }}.css %}
37 | {% endhighlight %}
38 |
39 |
40 |
41 |
42 |
43 | {% endif %}
44 |
45 | {% endfor %}
46 |
47 |
--------------------------------------------------------------------------------
/docs/_includes/head/custom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/_includes/tabs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/_pages/404.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Page Not Found"
3 | excerpt: "Page not found. Your pixels are in another canvas."
4 | sitemap: false
5 | permalink: /404.html
6 | ---
7 |
8 | Sorry, but the page you were trying to view does not exist.
9 |
--------------------------------------------------------------------------------
/docs/_pages/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /about/
3 | title: "About"
4 | ---
5 |
6 | ## Story
7 |
8 | The first version of Floorplan for Home Assistant dates back to 2017. The project was developed by [Petar Kožul](https://github.com/pkozul) based on earlier work done using SVG files for home automation. With the ermergence of Lovelace in HA, Petar added support for running [ha-floorplan](https://github.com/pkozul/ha-floorplan) as a Lovelace card. The old Lovelace version was called [lovelace-floorplan](https://github.com/ExperienceLovelace/lovelace-floorplan). [Tobias Nordahl Kristensen](https://github.com/exetico) joined the ha-floorplan project during a period where not much development was taking place on the project. After contributing a huge pull request, lovelace-floorplan was finally able to support HACS. What Tobias didn't know, was that Petar had already commenced a complete rewrite of the code, this time using TypeScript. The intention was to improve performance, add new features, and bring Floorplan to a whole new level.
9 |
10 | We (Petar and Tobias) moved lovelace-floorplan to a new team called [ExperienceLovelace](https://github.com/ExperienceLovelace). After a few weeks of testing and improvements to the new code base, we finally released the first version of the new [ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan) (Yep, that's the current one).
11 |
12 | ## Your imagination (almost) defines the limits
13 |
14 | With [ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan), we finally have a module which allows you to create tons of things. Create an SVG, and map entities to it.
15 |
16 | The primary usage is floorplans, but you're able to create whatever you need. You're able to import a normal image to an SVG file, so it's possible to do many things with it. Therefore, kindly head over to our examples area if you're ready to see [ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan) in action.
17 |
18 | ## Get help along the way
19 |
20 | If you're struggling to get started, please check our examples, Quick Start guide and more. After reading the docs, we'd like to introduce you to our [discussion area](https://github.com/ExperienceLovelace/ha-floorplan/discussions) on GitHub. Here you're more than welcome to ask for help. If you'd like to contribute to the project, feel free to create pull requests and issues in the main repo. If you like the Home Assistant Community, [come join us](https://community.home-assistant.io/t/floorplan-now-available-as-a-lovelace-card/115489), too.
21 |
22 | ## About the docs
23 |
24 | Our docs are generated by Jekyll, and powered by the [Minimal Mistakes](https://mmistakes.github.io/minimal-mistakes/) ([GitHub](https://github.com/mmistakes/minimal-mistakes)) theme. There's plenty of ways of doing [formatting](https://mmistakes.github.io/minimal-mistakes/markup/markup-html-tags-and-formatting/) ([GitHub](https://github.com/mmistakes/minimal-mistakes/edit/master/docs/_posts/2013-01-11-markup-html-tags-and-formatting.md)), and the [helpers](https://mmistakes.github.io/minimal-mistakes/docs/helpers/), [layouts](https://mmistakes.github.io/minimal-mistakes/docs/layouts/) and [utility classes](https://mmistakes.github.io/minimal-mistakes/docs/utility-classes/) are all awesome. Minimal Mistakes can be used free of charge, and can be posted with GitHub Pages - like our docs. We'd just like to give a big shout-out to [Michael Rose](https://github.com/mmistakes), for a great Jekyll theme. Thanks!
25 |
26 | ## Not sponsored - disclaimer
27 |
28 | There are no sponsors for ha-floorplan, and that's totally fine with us. We'd just like to point that we may be recommending software which will require a few bucks, if you're searching for the best result. Recommendations towards paid or [freemium](https://en.wikipedia.org/wiki/Freemium) services are done, cause we like those. All links to services like these, are marked with a `Not sponsored` disclaimer.
29 |
--------------------------------------------------------------------------------
/docs/_pages/category-archive.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Posts by Category"
3 | layout: categories
4 | permalink: /categories/
5 | author_profile: true
6 | ---
7 |
--------------------------------------------------------------------------------
/docs/_pages/docs.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | layout: collection
4 | permalink: /docs/
5 | collection: docs
6 | entries_layout: grid
7 | classes: wide
8 | ---
9 |
--------------------------------------------------------------------------------
/docs/_pages/tag-archive.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Posts by Tag"
3 | permalink: /tags/
4 | layout: tags
5 | author_profile: true
6 | ---
7 |
--------------------------------------------------------------------------------
/docs/_pages/year-archive.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Posts by Year"
3 | permalink: /posts/
4 | layout: posts
5 | author_profile: true
6 | ---
7 |
--------------------------------------------------------------------------------
/docs/_posts/2020-01-09-documentation-for-ha-floorplan.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Documentation for ha-floorplan!"
3 | date: 2020-01-09T18:10:30+01:00
4 | last_modified_at: 2020-01-09T18:25:10+01:00
5 | categories:
6 | - Meta
7 | tags:
8 | - ha-floorplan
9 | - Update
10 | - Releases
11 | ---
12 |
13 | Wuhuu!🥳 Welcome to the official documentation for ha-floorplan. We hope you'll find it usefull, while playing around with our custom module for Home Assistant.
14 |
15 | Please keep in mind that we're still working on the docs. Many pages has a very limited about of content, but it will get better down the line - we promise!
16 |
17 | ## Discussion - Help, support & feedback
18 |
19 | While waiting for the last bits and bytes, we kindly ask you to use our [Discussion][discussion-github]-area on GitHub, if you need any support.
20 |
21 |
22 | ## Home Assistant Community
23 |
24 | We're also on the official [Home Assistant Community][homeassistant-community], so feel free to join us there, too. But please keep in mind that it's just better to reach out, in the [Discussion][discussion-github]-area.
25 |
26 | [discussion-github]: https://github.com/ExperienceLovelace/ha-floorplan/discussions
27 | [homeassistant-community]: https://community.home-assistant.io/t/floorplan-now-available-as-a-lovelace-card/115489/1
28 |
--------------------------------------------------------------------------------
/docs/_posts/2020-01-09-releases-of-ha-floorplan.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Get the latest version of ha-floorplan"
3 | date: 2020-01-09T18:31:20+01:00
4 | categories:
5 | - Meta
6 | tags:
7 | - Release
8 | link: https://github.com/ExperienceLovelace/ha-floorplan/releases
9 | ---
10 |
11 | If you're searching for the latest version of ha-floorplan, please head over to the [Releases page on GitHub](https://github.com/ExperienceLovelace/ha-floorplan/releases).
12 |
13 | But please keep in mind that we're fully supported by HACS. It's already a good idea to use that, for better handling of custom modules to Home Assistant. Updates are almost as simple as a single click, too. Read more about how to do it the HACS-way, in our [Quick Start](./docs/quick-start/)-guide.
14 |
--------------------------------------------------------------------------------
/docs/_posts/2021-07-28-check-our-examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "New examples and usage-guide"
3 | date: 2021-07-28T16:04:22+00:00
4 | categories:
5 | - Meta
6 | tags:
7 | - Examples
8 | link: https://experiencelovelace.github.io/ha-floorplan/docs/examples/
9 | ---
10 |
11 | In the last couple of months, we've released a few new examples. Check out the [remote control](https://experiencelovelace.github.io/ha-floorplan/docs/example-remote/), and the [modern floorplan](https://experiencelovelace.github.io/ha-floorplan/docs/example-floorplanner-home/).
12 |
13 | If you've though about creating a SVG-files with multiple layers, also known as floors, that's now possible. Go visit the [multiple floors example](https://experiencelovelace.github.io/ha-floorplan/docs/example-multi-floor/) to learn more.
14 |
15 | Remember to check out the [examples](https://github.com/ExperienceLovelace/ha-floorplan/tree/master/docs/_docs/floorplan/examples) section on GitHub too, if you're searching for the source-files. It's also a good idea to visit our [Usage](https://experiencelovelace.github.io/ha-floorplan/docs/usage/)-page, if you're searching for even more knowledge.
16 |
17 | Thank you for using our floorplan solution! You're always welcome to join our [community](https://github.com/ExperienceLovelace/ha-floorplan/discussions).
18 |
--------------------------------------------------------------------------------
/docs/_posts/2022-11-07-videotutorials-for-ha-floorplan.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Video-tutorials for all of you"
3 | date: 2022-11-07T17:53:10+00:00
4 | excerpt: "There's now video tutorials on YouTube, ready for you to watch. So, now you can watch how to get started with ha-floorplan!"
5 | categories:
6 | - Meta
7 | tags:
8 | - Video
9 | - Tutorials
10 | link: https://www.youtube.com/playlist?list=PL5xKVw-BInX1phV-Tnjznwd2YG5mEOvWL
11 | ---
12 |
13 | ## Tutorials ready on YouTube
14 | I've been working on getting videos ready for you, guys. And today I'm happy to announce that the first [three videos has been released on YouTube](https://www.youtube.com/playlist?list=PL5xKVw-BInX1phV-Tnjznwd2YG5mEOvWL), in the last couple of weeks.
15 |
16 | I can't promise that I'll be pushing videos regularly, but I'll try to at least share a few more, before (possibly) changing topic on the channel from time to time.
17 |
18 | [Find the ha-floorplan playlist here on YouTube](https://www.youtube.com/playlist?list=PL5xKVw-BInX1phV-Tnjznwd2YG5mEOvWL)
19 |
20 | _Was the videos helpful to you? Please drop a comment on the video, and press the like-button. Thanks!_
21 |
22 | ## Share your frustrations
23 |
24 | If you're facing a specific problem and feels that other must have faced the same, please share it in the discussion where I'm asking for inputs:
25 |
26 | [Please share your first frustrations about ha-floorplan](https://github.com/ExperienceLovelace/ha-floorplan/discussions/247)
27 |
28 | ## New service in ha-floorplan
29 |
30 | I'll also like to mention that a new `execute` function has been added to the ha-floorplan service-list. We've also added a great example on [how to use browser_mod in a JavaScript-call](http://localhost:4000/ha-floorplan/docs/usage/#using-execute-with-browser_mod), where we use the `execute` service. The `execute` service was implemented, after one user asked for a [better way to pass entities to browser_mod](https://github.com/ExperienceLovelace/ha-floorplan/discussions/252).
31 |
32 | Thank you for using ha-floorplan, and as always: Feel free to join our [community](https://github.com/ExperienceLovelace/ha-floorplan/discussions).
33 |
--------------------------------------------------------------------------------
/docs/assets/css/tabs.css:
--------------------------------------------------------------------------------
1 | /* Style the tab */
2 | .tab {
3 | overflow: hidden;
4 | border: 1px solid #ccc;
5 | background-color: #f1f1f1;
6 | }
7 |
8 | /* Style the buttons that are used to open the tab content */
9 | .tab button {
10 | background-color: inherit;
11 | float: left;
12 | border: none;
13 | outline: none;
14 | cursor: pointer;
15 | padding: 14px 16px;
16 | transition: 0.3s;
17 | }
18 |
19 | /* Change background color of buttons on hover */
20 | .tab button:hover {
21 | background-color: #ddd;
22 | }
23 |
24 | /* Create an active/current tablink class */
25 | .tab button.active {
26 | background-color: #ccc;
27 | }
28 |
29 | /* Style the tab content */
30 | .tabcontent {
31 | display: none;
32 | padding: 6px 12px;
33 | border: 1px solid #ccc;
34 | border-top: none;
35 |
36 | overflow: auto;
37 | }
38 |
39 | .tabcontent.active {
40 | display: block;
41 | }
42 |
43 | .tabcontent:not(.active) {
44 | display: none;
45 | }
46 |
47 | .tabcontent-container.size-sm .tabcontent {
48 | height: 300px;
49 | }
50 |
51 | .tabcontent-container.size-md .tabcontent {
52 | height: 400px;
53 | }
54 |
55 | .tabcontent-container.size-lg .tabcontent {
56 | height: 500px;
57 | }
58 |
59 | .tabcontent-container.size-xl .tabcontent {
60 | height: 600px;
61 | }
62 |
63 | .tabcontent-container.size-auto .tabcontent {
64 | height: auto;
65 | }
66 |
67 | .tabcontent-container figure.highlight {
68 | margin: 0px;
69 | }
70 |
--------------------------------------------------------------------------------
/docs/assets/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/assets/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/assets/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/docs/assets/images/bg-frontpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/bg-frontpage.png
--------------------------------------------------------------------------------
/docs/assets/images/bio-photo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/bio-photo.jpg
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/hacs-setup-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/hacs-setup-1.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/hacs-setup-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/hacs-setup-2.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/video-first-floorplan-part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/video-first-floorplan-part1.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/video-getting-started.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/video-getting-started.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/view-panelmode-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/view-panelmode-1.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/view-panelmode-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/view-panelmode-2.png
--------------------------------------------------------------------------------
/docs/assets/images/docs/quick-start/view-panelmode-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/docs/quick-start/view-panelmode-3.png
--------------------------------------------------------------------------------
/docs/assets/images/header-frontpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/header-frontpage.png
--------------------------------------------------------------------------------
/docs/assets/images/home/floorplan-background_with_floorplanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/home/floorplan-background_with_floorplanner.png
--------------------------------------------------------------------------------
/docs/assets/images/logo-200x200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/logo-200x200.png
--------------------------------------------------------------------------------
/docs/assets/images/logo-88x88.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ExperienceLovelace/ha-floorplan/3bd324056002e10f74ed116bd5260f0adbe4511f/docs/assets/images/logo-88x88.png
--------------------------------------------------------------------------------
/docs/assets/js/tabs.js:
--------------------------------------------------------------------------------
1 | function showTab(evt, tabName) {
2 | const clickedTabLink = evt.currentTarget;
3 | const tabLinkDiv = clickedTabLink.parentNode;
4 | const tabContentContainer = tabLinkDiv.nextElementSibling;
5 |
6 | // Get all elements with class="tabcontent" and hide them
7 | for (const tabContent of tabContentContainer.querySelectorAll('.tabcontent')) {
8 | tabContent.className = tabContent.className.split(' ').map(x => x.trim()).filter(x => x !== 'active').join(' ');
9 | }
10 |
11 | // Get all elements with class="tablinks" and remove the class "active"
12 | for (const tabLink of tabLinkDiv.querySelectorAll('.tablinks')) {
13 | tabLink.className = tabLink.className.split(' ').map(x => x.trim()).filter(x => x !== 'active').join(' ');
14 | }
15 |
16 | // Show the current tab, and add an "active" class to the button that opened the tab
17 | document.querySelector(`[data-tab=${tabName}]`).className += " active";
18 | clickedTabLink.className += " active";
19 | }
20 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ha-floorplan 🖌🎨 | Your imagination (almost) defines the limits",
3 | "filename": "floorplan.js",
4 | "render_readme": true,
5 | "homeassistant": "2024.6.0"
6 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} **/
2 | export default {
3 | testEnvironment: 'jsdom',
4 | transform: {
5 | '^.+\\.tsx?$': ['ts-jest', { useESM: true }], // Enable ESM for ts-jest
6 | '^.+\\.js$': 'babel-jest', // Use Babel to transform plain JavaScript files
7 | },
8 | extensionsToTreatAsEsm: ['.ts', '.tsx'], // Treat TypeScript files as ESM
9 | transformIgnorePatterns: [
10 | '/node_modules/(?!lit|@testing-library|@lit|home-assistant-js-websocket|oui-dom-events)', // Allow specific ESM dependencies to be transformed
11 | ],
12 | moduleNameMapper: {
13 | '^(\\.{1,2}/.*)\\.js$': '$1', // Fix imports with .js extensions
14 | },
15 | setupFilesAfterEnv: ['/jest.setup.js', '@testing-library/jest-dom'], // Ensure jest.setup.js and jest-dom are executed
16 | detectOpenHandles: true, // Detect open handles to avoid Jest hanging
17 | maxWorkers: 1, // Limit to one worker to avoid issues with ESM
18 | testPathIgnorePatterns: ['/tests/e2e/', '/tests/jest/tests/disabled'], // Ignore E2E tests for Jest
19 | testMatch: ['/tests/jest/tests/**/*.test.ts'], // Adjust to match your Jest test files
20 | testTimeout: 30000, // Extend timeout for tests
21 | testEnvironmentOptions: {
22 | url: 'http://localhost:8080',
23 | },
24 | };
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import 'whatwg-fetch'; // Polyfill fetch and Request
3 | import packageJson from './package.json';
4 | import express from 'express';
5 | import path from 'path'; // Import path module for resolving paths
6 | import { jest } from '@jest/globals'; // Ensure Jest is recognized
7 |
8 | // Ensure Request is explicitly available
9 | if (typeof global.Request === 'undefined') {
10 | global.Request = window.Request;
11 | global.Headers = window.Headers;
12 | global.origin = window.location.origin;
13 | }
14 |
15 | // Define global variables for Jest environment
16 | global.NAME = packageJson.name;
17 | global.DESCRIPTION = packageJson.description + ' (Test Jest-env)';
18 | global.VERSION = packageJson.version;
19 |
20 | let examples_server;
21 |
22 | beforeAll(() => {
23 | // Mock getBBox for SVG elements
24 | Object.defineProperty(SVGElement.prototype, 'getBBox', {
25 | value: jest.fn().mockReturnValue({
26 | x: 0,
27 | y: 0,
28 | width: 100,
29 | height: 100,
30 | }),
31 | });
32 |
33 | // Mock SVGTextElement
34 | global.SVGTextElement = class extends SVGElement {};
35 |
36 | const app = express();
37 | const examplesPath = path.resolve(process.cwd(), 'docs/_docs/floorplan');
38 |
39 | app.use('/', express.static(examplesPath));
40 |
41 | examples_server = app.listen(8080, () => {
42 | console.log('Serving examples folder at http://localhost:8080/');
43 | });
44 | });
45 |
46 | afterAll((done) => {
47 | // Close the server after tests are done
48 | examples_server.close(done);
49 | });
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ha-floorplan",
3 | "version": "1.1.2",
4 | "description": "Floorplan for Home Assistant",
5 | "homepage": "https://experiencelovelace.github.io/ha-floorplan",
6 | "keywords": [
7 | "homeassistant",
8 | "home assisant",
9 | "floorplan",
10 | "svg"
11 | ],
12 | "bugs": {
13 | "url": "https://github.com/ExperienceLovelace/ha-floorplan/issues"
14 | },
15 | "license": "ISC",
16 | "contributors": [
17 | {
18 | "name": "Petar Kožul",
19 | "email": "pkozul@yahoo.com",
20 | "url": "https://github.com/pkozul"
21 | },
22 | {
23 | "name": "Tobias Nordahl Kristensen",
24 | "email": "tobiasnordahl@gmail.com",
25 | "url": "https://github.com/exetico"
26 | }
27 | ],
28 | "main": "dist/floorplan.js",
29 | "repository": "https://github.com/ExperienceLovelace/ha-floorplan",
30 | "type": "module",
31 | "scripts": {
32 | "start": "npx webpack serve --progress",
33 | "build": "webpack --env production",
34 | "build:dev": "webpack",
35 | "analyze": "webpack --profile --json > stats.json && webpack-bundle-analyzer ./stats.json",
36 | "update:docs": "docker run -it --rm --volume=${PWD}/docs:/srv/jekyll -p 4000:4000 jekyll/jekyll bundle update github-pages",
37 | "serve:docs": "docker run -it --rm --volume=${PWD}/docs:/srv/jekyll -p 4000:4000 jekyll/jekyll jekyll serve --incremental",
38 | "prettier": "prettier --config .prettierrc \"src/**/*.ts\" \"src/**/*.ts\" --write",
39 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx",
40 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --reporters=default --reporters=jest-junit",
41 | "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --watch",
42 | "test:e2e": "playwright test"
43 | },
44 | "dependencies": {
45 | "@playwright/test": "^1.51.1",
46 | "home-assistant-js-websocket": "^8.0.1",
47 | "js-yaml": "^4.1.0",
48 | "lit": "^2.4.1",
49 | "parse-duration": "^2.1.3",
50 | "serialize-javascript": "^6.0.2",
51 | "strftime": "^0.10.1",
52 | "superstruct": "^1.0.3",
53 | "sval": "^0.4.8"
54 | },
55 | "devDependencies": {
56 | "@babel/core": "^7.26.10",
57 | "@babel/preset-env": "^7.26.9",
58 | "@jest/types": "^29.6.3",
59 | "@testing-library/dom": "^9.0.0",
60 | "@testing-library/jest-dom": "^5.16.5",
61 | "@types/jest": "^29.5.14",
62 | "@types/js-yaml": "^4.0.5",
63 | "@types/node": "^18.11.10",
64 | "@types/strftime": "^0.9.4",
65 | "@typescript-eslint/eslint-plugin": "^5.45.0",
66 | "@typescript-eslint/parser": "^5.45.0",
67 | "babel-jest": "^29.7.0",
68 | "copy-webpack-plugin": "^11.0.0",
69 | "eslint": "^8.56.0",
70 | "html-loader": "^4.2.0",
71 | "jest": "^29.7.0",
72 | "jest-environment-jsdom": "^29.0.0",
73 | "jest-junit": "^16.0.0",
74 | "playwright": "^1.37.0",
75 | "prettier": "^2.8.0",
76 | "ts-jest": "^29.3.1",
77 | "ts-loader": "^9.4.2",
78 | "typescript": "^4.9.3",
79 | "webpack": "^5.94.0",
80 | "webpack-bundle-analyzer": "^4.10.0",
81 | "webpack-cli": "^5.0.0",
82 | "webpack-dev-server": "^4.15.0",
83 | "whatwg-fetch": "^3.6.20"
84 | },
85 | "jest-junit": {
86 | "outputDirectory": "test-results/jest",
87 | "outputName": "junit.xml"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@playwright/test';
2 |
3 | export default defineConfig({
4 | testDir: './tests/e2e', // Directory for Playwright tests
5 | outputDir: './test-results/e2e',
6 | use: {
7 | baseURL: 'http://localhost:8080', // Adjust to your local dev server
8 | testIdAttribute: 'data-floorplan-ref',
9 | },
10 | reporter: [
11 | ['list'], // Keep the default list reporter
12 | ['junit', { outputFile: 'test-results/e2e/results.xml' }] // Add JUnit reporter
13 | ],
14 | });
15 |
--------------------------------------------------------------------------------
/src/components/floorplan-card/floorplan-card.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../lib/homeassistant/types';
2 | import { LovelaceCard } from '../../lib/homeassistant/panels/lovelace/types';
3 | import { LovelaceCardConfig } from '../../lib/homeassistant/data/lovelace';
4 | import {
5 | css,
6 | CSSResult,
7 | html,
8 | LitElement,
9 | TemplateResult,
10 | PropertyValues,
11 | } from 'lit';
12 | import { customElement, property } from 'lit/decorators.js';
13 | import { ShadowDomHelper } from './../floorplan/lib/shadow-dom-helper';
14 | import '../floorplan/floorplan-element';
15 | import { ifDefined } from 'lit-html/directives/if-defined.js';
16 | import { styleMap, StyleInfo } from 'lit-html/directives/style-map.js';
17 |
18 | @customElement('floorplan-card')
19 | export class FloorplanCard extends LitElement implements LovelaceCard {
20 | @property({ type: Object }) public hass!: HomeAssistant;
21 | @property({ type: Boolean }) public isPanel!: boolean;
22 | @property({ type: Boolean }) public editMode!: boolean;
23 |
24 | @property({ type: Object }) public config!: LovelaceCardConfig;
25 |
26 | @property({ type: String }) public examplespath!: string;
27 | @property({ type: Boolean }) public isDemo!: boolean;
28 | @property({ type: Function }) public notify!: (message: string) => void;
29 |
30 | styles: StyleInfo = {
31 | dummy: `calc(100vh - ${this.appHeaderHeight}px - ${this.cardHeaderHeight}px)`,
32 | };
33 |
34 | _view: Element | null | undefined;
35 | _appHeader: Element | null | undefined;
36 |
37 | static cardHeaderHeight = 76;
38 |
39 | protected render(): TemplateResult {
40 | if (!this.config) {
41 | return html``;
42 | }
43 |
44 | return html`
45 |
46 | ${this.isDisplayCardHeader
47 | ? html` `
48 | : ''}
49 |
50 |
51 |
59 |
60 |
61 | `;
62 | }
63 |
64 | static get styles(): CSSResult {
65 | return css`
66 | /* header (main toolbar) */
67 | /* --header-height: 56px; */
68 |
69 | /* card header */
70 | /* height: 76px; */
71 |
72 | :host .content,
73 | :host .content floorplan-element {
74 | display: flex;
75 | flex-flow: column;
76 | flex: 1;
77 | min-height: 0;
78 | }
79 | `;
80 | }
81 |
82 | get isFullHeight(): boolean {
83 | return this.config?.full_height;
84 | }
85 |
86 | get view(): Element | null | undefined {
87 | if (!this._view) {
88 | this._view = ShadowDomHelper.closestElement('#view', this);
89 | }
90 | return this._view;
91 | }
92 |
93 | get appHeader(): Element | null | undefined {
94 | if (!this._appHeader) {
95 | this._appHeader = this.view?.previousElementSibling;
96 | }
97 | return this._appHeader;
98 | }
99 |
100 | get appHeaderHeight(): number {
101 | if (this.isDemo) return 0;
102 | const appHeader = this.appHeader;
103 | return appHeader ? appHeader.clientHeight : 0;
104 | }
105 |
106 | get cardHeaderHeight(): number {
107 | if (this.isDemo) return 0;
108 | return this.isDisplayCardHeader ? FloorplanCard.cardHeaderHeight : 0;
109 | }
110 |
111 | get isDisplayCardHeader(): boolean {
112 | if (this.isDemo) return false;
113 | return (this.config?.title as string)?.trim().length > 0;
114 | }
115 |
116 | getCardSize(): number | Promise {
117 | return 1;
118 | }
119 |
120 | setConfig(config: LovelaceCardConfig): void {
121 | this.config = config;
122 | }
123 |
124 | update(changedProperties: PropertyValues): void {
125 | if (this.isFullHeight) {
126 | this.styles = {
127 | height: `calc(100vh - ${this.appHeaderHeight}px - ${this.cardHeaderHeight}px)`,
128 | };
129 | } else {
130 | this.styles = { dummy: '' };
131 | }
132 |
133 | super.update(changedProperties);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/floorplan-examples/code-block.ts:
--------------------------------------------------------------------------------
1 | import { html, LitElement, css, CSSResult } from 'lit';
2 | import { customElement, property } from 'lit/decorators.js';
3 | import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
4 | /*
5 | import hljs from 'highlight.js/lib/core';
6 | import highlightYaml from 'highlight.js/lib/languages/yaml';
7 | import highlightCss from 'highlight.js/lib/languages/css';
8 | hljs.registerLanguage('yaml', highlightYaml);
9 | hljs.registerLanguage('css', highlightCss);
10 | */
11 |
12 | @customElement('code-block')
13 | export class CodeBlockElement extends LitElement {
14 | @property({ type: String }) public lang = '';
15 | @property({ type: String }) public code = '';
16 |
17 | render() {
18 | /*
19 | const highlightedCode = hljs.highlightAuto(
20 | this.code,
21 | this.lang ? [this.lang] : undefined
22 | ).value;
23 |
24 | return html` ${unsafeHTML(highlightedCode)}
`;
25 | */
26 |
27 | return html` ${unsafeHTML(this.code)}
`;
28 | }
29 |
30 | static get styles(): CSSResult {
31 | return css`
32 | .hljs {
33 | display: block;
34 | overflow-x: auto;
35 | padding: 0.5em;
36 | background: #f0f0f0;
37 | }
38 |
39 | /* Base color: saturation 0; */
40 |
41 | .hljs,
42 | .hljs-subst {
43 | color: #444;
44 | }
45 |
46 | .hljs-comment {
47 | color: #888888;
48 | }
49 |
50 | .hljs-keyword,
51 | .hljs-attribute,
52 | .hljs-selector-tag,
53 | .hljs-meta-keyword,
54 | .hljs-doctag,
55 | .hljs-name {
56 | font-weight: bold;
57 | }
58 |
59 | /* User color: hue: 0 */
60 |
61 | .hljs-type,
62 | .hljs-string,
63 | .hljs-number,
64 | .hljs-selector-id,
65 | .hljs-selector-class,
66 | .hljs-quote,
67 | .hljs-template-tag,
68 | .hljs-deletion {
69 | color: #880000;
70 | }
71 |
72 | .hljs-title,
73 | .hljs-section {
74 | color: #880000;
75 | font-weight: bold;
76 | }
77 |
78 | .hljs-regexp,
79 | .hljs-symbol,
80 | .hljs-variable,
81 | .hljs-template-variable,
82 | .hljs-link,
83 | .hljs-selector-attr,
84 | .hljs-selector-pseudo {
85 | color: #bc6060;
86 | }
87 |
88 | /* Language color: hue: 90; */
89 |
90 | .hljs-literal {
91 | color: #78a960;
92 | }
93 |
94 | .hljs-built_in,
95 | .hljs-bullet,
96 | .hljs-code,
97 | .hljs-addition {
98 | color: #397300;
99 | }
100 |
101 | /* Meta color: hue: 200 */
102 |
103 | .hljs-meta {
104 | color: #1f7199;
105 | }
106 |
107 | .hljs-meta-string {
108 | color: #4d99bf;
109 | }
110 |
111 | /* Misc effects */
112 |
113 | .hljs-emphasis {
114 | font-style: italic;
115 | }
116 |
117 | .hljs-strong {
118 | font-weight: bold;
119 | }
120 | `;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/floorplan-examples/floorplan-example.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../lib/homeassistant/types';
2 | import { HassSimulator } from './hass-simulator';
3 | import { HassSimulatorConfig, FloorplanExample } from './types';
4 | import { FloorplanPanelConfig } from '../floorplan-panel/types';
5 | import { LovelaceCardConfig } from '../../lib/homeassistant/data/lovelace';
6 | import { Utils } from '../../lib/utils';
7 | import {
8 | css,
9 | CSSResult,
10 | html,
11 | LitElement,
12 | TemplateResult,
13 | PropertyValues,
14 | } from 'lit';
15 | import { customElement, property } from 'lit/decorators.js';
16 | import { ifDefined } from 'lit-html/directives/if-defined.js';
17 | import '../floorplan-card/floorplan-card';
18 | import '../floorplan-panel/floorplan-panel';
19 | import './code-block';
20 |
21 | @customElement('floorplan-example')
22 | export class FloorplanExampleElement extends LitElement {
23 | @property({ type: Object }) public hass!: HomeAssistant;
24 | @property({ type: Object }) public config!:
25 | | LovelaceCardConfig
26 | | FloorplanPanelConfig;
27 | @property({ type: String }) public configYaml!: string;
28 |
29 | @property({ type: String }) public examplespath!: string;
30 | @property({ type: Object }) public example!: FloorplanExample;
31 | @property({ type: Boolean }) public isDemo!: boolean;
32 | @property({ type: Function }) public notify!: (message: string) => void;
33 |
34 | simulator?: HassSimulator;
35 |
36 | protected render(): TemplateResult {
37 | return html`
38 |
39 |
40 | ${typeof this.config?.config === 'undefined'
41 | ? ''
42 | : this.example.isCard
43 | ? html` `
51 | : html` `}
59 |
60 |
61 |
64 |
65 | `;
66 | }
67 |
68 | static get styles(): CSSResult {
69 | return css``;
70 | }
71 |
72 | async update(changedProperties: PropertyValues): Promise {
73 | super.update(changedProperties);
74 |
75 | if (
76 | (changedProperties.has('example') ||
77 | changedProperties.has('examplespath')) &&
78 | this.example &&
79 | this.examplespath
80 | ) {
81 | let configYamlText = this.example?.configYaml as string;
82 |
83 | // Inline Yaml does have first priority, but if not set, we need to fetch it
84 | if (!configYamlText) {
85 | const configUrl = `${this.examplespath}/${this.example.dir}/${this.example.configFile}`;
86 | configYamlText = await Utils.fetchText(
87 | configUrl,
88 | true,
89 | this.examplespath,
90 | false
91 | );
92 | }
93 |
94 | const config = await Utils.parseYaml(configYamlText) as
95 | | LovelaceCardConfig
96 | | FloorplanPanelConfig;
97 |
98 | this.configYaml = configYamlText;
99 | this.config = config;
100 |
101 | // Preparing the simulator, which are optional
102 | if (this.example?.simulationFile || this.example?.simulationYaml) {
103 | let simulatorYamlText = this.example?.simulationYaml as string;
104 | if (!simulatorYamlText) {
105 | const simulatorUrl = `${this.examplespath}/${this.example.dir}/${this.example.simulationFile}`;
106 | simulatorYamlText = await Utils.fetchText(
107 | simulatorUrl,
108 | true,
109 | this.examplespath,
110 | false
111 | );
112 | }
113 |
114 | const simulatorConfig = Utils.parseYaml(
115 | simulatorYamlText
116 | ) as HassSimulatorConfig;
117 | this.simulator = new HassSimulator(
118 | simulatorConfig,
119 | this.setHass.bind(this)
120 | );
121 | }
122 | }
123 | }
124 |
125 | setHass(hass: HomeAssistant): void {
126 | this.hass = hass;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/floorplan-examples/floorplan-examples.ts:
--------------------------------------------------------------------------------
1 | import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
2 | import { customElement, property } from 'lit/decorators.js';
3 | import { FloorplanExample } from './types';
4 | import './floorplan-example';
5 | import '../lit-toast/lit-toast';
6 | import { LitToast } from '../lit-toast/lit-toast';
7 |
8 | @customElement('floorplan-examples')
9 | export class FloorplanExamples extends LitElement {
10 | @property({ type: String }) public examplespath!: string;
11 | @property({ type: Array }) public examples!: FloorplanExample[];
12 |
13 | floorplanExamples = [
14 | // Cards
15 | {
16 | name: 'test_plate',
17 | dir: 'test_plate',
18 | configFile: 'test_plate.yaml',
19 | simulationFile: 'simulations.yaml',
20 | isCard: true,
21 | },
22 | {
23 | name: 'home',
24 | dir: 'home',
25 | configFile: 'home.yaml',
26 | simulationFile: 'simulations.yaml',
27 | isCard: true,
28 | },
29 | {
30 | name: 'remote',
31 | dir: 'remote',
32 | configFile: 'remote.yaml',
33 | simulationFile: 'simulations.yaml',
34 | isCard: true,
35 | },
36 | {
37 | name: 'light',
38 | dir: 'light',
39 | configFile: 'light.yaml',
40 | simulationFile: 'simulations.yaml',
41 | isCard: true,
42 | },
43 | {
44 | name: 'ring',
45 | dir: 'ring',
46 | configFile: 'ring.yaml',
47 | simulationFile: 'simulations.yaml',
48 | isCard: true,
49 | },
50 | {
51 | name: 'rinnai',
52 | dir: 'rinnai',
53 | configFile: 'rinnai.yaml',
54 | simulationFile: 'simulations.yaml',
55 | isCard: true,
56 | },
57 | {
58 | name: 'floorplanner_home',
59 | dir: 'floorplanner_home',
60 | configFile: 'floorplanner_home.yaml',
61 | simulationFile: 'simulations.yaml',
62 | isCard: true,
63 | },
64 | // Panels
65 | {
66 | name: 'multi_floor',
67 | dir: 'multi_floor',
68 | configFile: 'multi_floor.yaml',
69 | simulationFile: 'simulations.yaml',
70 | isCard: false,
71 | },
72 | ] as FloorplanExample[];
73 |
74 | constructor() {
75 | super();
76 |
77 | //console.log("NODE_ENV", process.env.NODE_ENV);
78 | }
79 |
80 | protected render(): TemplateResult {
81 | return html`
82 | ${this.examples?.map(
83 | (example) =>
84 | html` `
90 | )}
91 |
92 |
93 | `;
94 | }
95 |
96 | static get styles(): CSSResult {
97 | return css``;
98 | }
99 |
100 | connectedCallback(): void {
101 | super.connectedCallback();
102 |
103 | if (this.dataset.include && !this.examples) {
104 | const exampleNames = this.dataset.include.split(',').map((x) => x.trim());
105 | this.examples = this.floorplanExamples.filter((x) =>
106 | exampleNames.includes(x.name.toLocaleLowerCase())
107 | );
108 | }
109 | }
110 |
111 | get litToast(): LitToast {
112 | return this.shadowRoot?.querySelector('lit-toast') as LitToast;
113 | }
114 |
115 | notify(message: string): void {
116 | this.litToast.show(message);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/floorplan-examples/homeassistant.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { MessageBase } from 'home-assistant-js-websocket';
3 | import {
4 | HomeAssistant as IHomeAssistant,
5 | ServiceCallRequest,
6 | ServiceCallResponse,
7 | } from '../../lib/homeassistant/types';
8 | import {
9 | HassEntityAttributeBase,
10 | HassEntityBase as IHassEntityBase,
11 | } from 'home-assistant-js-websocket';
12 |
13 | export class HomeAssistant implements IHomeAssistant {
14 | states: Record = {};
15 | dockedSidebar!: 'docked' | 'always_hidden' | 'auto';
16 |
17 | callWS(msg: MessageBase): Promise {
18 | console.log(msg);
19 | return Promise.resolve(null as any);
20 | }
21 |
22 | callService(
23 | domain: ServiceCallRequest['domain'],
24 | service: ServiceCallRequest['service'],
25 | serviceData?: ServiceCallRequest['serviceData']
26 | ): Promise {
27 | if (domain && service && serviceData) {
28 | // placeholder
29 | }
30 |
31 | const response = {
32 | context: {
33 | id: '',
34 | parent_id: undefined,
35 | user_id: undefined,
36 | },
37 | } as ServiceCallResponse;
38 |
39 | return Promise.resolve(response);
40 | }
41 |
42 | clone(): HomeAssistant {
43 | const hass = new HomeAssistant();
44 | hass.callService = this.callService;
45 | hass.states = JSON.parse(JSON.stringify(this.states));
46 | return hass;
47 | }
48 | }
49 |
50 | export class HassEntity implements IHassEntityBase {
51 | entity_id!: string;
52 | state!: string;
53 | last_changed!: string;
54 | last_updated!: string;
55 | attributes!: HassEntityAttributeBase;
56 | context!: { id: string; user_id: string | null; parent_id: string | null };
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/floorplan-examples/types.ts:
--------------------------------------------------------------------------------
1 | import { HassEntity } from './homeassistant';
2 |
3 | export interface FloorplanExample {
4 | name: string;
5 | dir: string;
6 | configFile?: string;
7 | configYaml?: string;
8 | simulationFile?: string;
9 | simulationYaml?: string;
10 | isCard: boolean;
11 | }
12 |
13 | export interface HassSimulatorConfig {
14 | simulations: HassSimulation[];
15 | }
16 |
17 | export interface HassSimulation {
18 | entity: string;
19 | entities: string[];
20 | state: HassEntity;
21 | states: HassEntity[];
22 | enabled: boolean;
23 | }
24 |
25 | export interface TimedHassEntity extends HassEntity {
26 | duration: number | string;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/floorplan-panel/floorplan-panel.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../lib/homeassistant/types';
2 | import { FloorplanPanelInfo } from './types';
3 | import {
4 | css,
5 | CSSResult,
6 | html,
7 | LitElement,
8 | TemplateResult,
9 | PropertyValues,
10 | } from 'lit';
11 | import { customElement, property } from 'lit/decorators.js';
12 | import '../floorplan/floorplan-element';
13 | import { ifDefined } from 'lit-html/directives/if-defined.js';
14 | import { styleMap, StyleInfo } from 'lit-html/directives/style-map.js';
15 |
16 | @customElement('floorplan-panel')
17 | export class FloorplanPanel extends LitElement {
18 | @property({ type: Object }) public hass!: HomeAssistant;
19 | @property({ type: Boolean }) public narrow!: boolean;
20 | @property({ type: Object }) public panel!: FloorplanPanelInfo;
21 |
22 | @property({ type: Boolean }) public showSideBar!: boolean;
23 | @property({ type: Boolean }) public showAppHeader!: boolean;
24 |
25 | @property({ type: String }) public examplespath!: string;
26 | @property({ type: Boolean }) public isDemo!: boolean;
27 | @property({ type: Function }) public notify!: (message: string) => void;
28 |
29 | styles: StyleInfo = { height: 'calc(100vh)' };
30 |
31 | static appHeaderHeight = 64;
32 |
33 | protected render(): TemplateResult {
34 | return html`
35 |
36 |
37 |
38 |
39 |
42 | ${this.panel?.title}
43 |
44 |
45 |
46 |
47 |
55 |
56 |
57 |
58 |
59 | `;
60 | }
61 |
62 | static get styles(): CSSResult {
63 | return css`
64 | :host .content,
65 | :host .content floorplan-element {
66 | display: flex;
67 | flex-flow: column;
68 | flex: 1;
69 | min-height: 0;
70 | }
71 |
72 | [hidden] {
73 | display: none !important;
74 | }
75 | `;
76 | }
77 |
78 | get appHeaderHeight(): number {
79 | if (this.isDemo) return 0;
80 | return this.showAppHeader ? FloorplanPanel.appHeaderHeight : 0;
81 | }
82 |
83 | update(changedProperties: PropertyValues): void {
84 | if (this.panel) {
85 | this.showSideBar = this.panel.config.show_side_bar !== false;
86 | this.showAppHeader =
87 | this.panel.config.show_app_header !== false && !this.isDemo;
88 |
89 | if (this.hass && this.panel.config.show_side_bar === false) {
90 | this.hass.dockedSidebar = 'always_hidden';
91 | }
92 |
93 | this.styles = { height: `calc(100vh - ${this.appHeaderHeight}px)` };
94 | }
95 |
96 | super.update(changedProperties);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/floorplan-panel/types.ts:
--------------------------------------------------------------------------------
1 | import { PanelInfo } from '../../lib/homeassistant/types';
2 | import { FloorplanConfig } from '../floorplan/lib/floorplan-config';
3 |
4 | export interface FloorplanPanelInfo extends PanelInfo {
5 | config: FloorplanPanelConfig;
6 | }
7 |
8 | export interface FloorplanPanelConfig {
9 | show_side_bar: boolean;
10 | show_app_header: boolean;
11 | config: FloorplanConfig | string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/color-util.ts:
--------------------------------------------------------------------------------
1 | export class ColorUtil {
2 | static miredToRGB(mired: number): [number, number, number] {
3 | return this.kelvinToRGB(1e6 / mired);
4 | }
5 |
6 | static kelvinToRGB(kelvin: number): [number, number, number] {
7 | const temp = kelvin / 100;
8 | let red, green, blue;
9 |
10 | if (temp <= 66) {
11 | red = 255;
12 | green = temp;
13 | green = 99.4708025861 * Math.log(green) - 161.1195681661;
14 |
15 | if (temp <= 19) {
16 | blue = 0;
17 | } else {
18 | blue = temp - 10;
19 | blue = 138.5177312231 * Math.log(blue) - 305.0447927307;
20 | }
21 | } else {
22 | red = temp - 60;
23 | red = 329.698727446 * Math.pow(red, -0.1332047592);
24 | green = temp - 60;
25 | green = 288.1221695283 * Math.pow(green, -0.0755148492);
26 | blue = 255;
27 | }
28 |
29 | return [
30 | Math.min(Math.max(Math.round(red), 0), 255),
31 | Math.min(Math.max(Math.round(green), 0), 255),
32 | Math.min(Math.max(Math.round(blue), 0), 255),
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/date-util.ts:
--------------------------------------------------------------------------------
1 | import strftime from 'strftime';
2 |
3 | export class DateUtil {
4 | static strftime = strftime;
5 |
6 | static MILLISECONDS_IN_SECOND = 1000;
7 | static MILLISECONDS_IN_MINUTE = 1000 * 60;
8 | static MILLISECONDS_IN_HOUR = 1000 * 60 * 60;
9 | static MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;
10 | static MILLISECONDS_IN_YEAR = 1000 * 60 * 60 * 24 * 365;
11 | static DEFAULT_LANG = 'en';
12 |
13 | static relativeTimeFormat = new Intl.RelativeTimeFormat((
14 | typeof window !== 'undefined' ? window.navigator?.language : this.DEFAULT_LANG
15 | ), {
16 | numeric: 'auto',
17 | style: 'long',
18 | });
19 |
20 | static timeago(date: string | Date): string {
21 | const targetDate = typeof date === 'string' ? new Date(date) : date;
22 |
23 | let unit = 'second' as RelativeTimeFormatUnit;
24 | let diff = 0;
25 |
26 | const diffMilliseconds = targetDate.getTime() - new Date().getTime();
27 |
28 | const diffYears = diffMilliseconds / this.MILLISECONDS_IN_YEAR;
29 | if (Math.abs(diffYears) >= 1) {
30 | unit = 'year';
31 | diff = diffYears;
32 | } else {
33 | const diffDays = diffMilliseconds / this.MILLISECONDS_IN_DAY;
34 | if (Math.abs(diffDays) >= 1) {
35 | unit = 'day';
36 | diff = diffDays;
37 | } else {
38 | const diffHours = diffMilliseconds / this.MILLISECONDS_IN_HOUR;
39 | if (Math.abs(diffHours) >= 1) {
40 | unit = 'hour';
41 | diff = diffHours;
42 | } else {
43 | const diffMinutes = diffMilliseconds / this.MILLISECONDS_IN_MINUTE;
44 | if (Math.abs(diffMinutes) >= 1) {
45 | unit = 'minute';
46 | diff = diffMinutes;
47 | } else {
48 | const diffSeconds = diffMilliseconds / this.MILLISECONDS_IN_SECOND;
49 | unit = 'second';
50 | diff = diffSeconds;
51 | }
52 | }
53 | }
54 | }
55 |
56 | return this.relativeTimeFormat.format(Math.round(diff), unit);
57 | }
58 | }
59 |
60 | type RelativeTimeFormatUnit =
61 | | 'year'
62 | | 'years'
63 | | 'quarter'
64 | | 'quarters'
65 | | 'month'
66 | | 'months'
67 | | 'week'
68 | | 'weeks'
69 | | 'day'
70 | | 'days'
71 | | 'hour'
72 | | 'hours'
73 | | 'minute'
74 | | 'minutes'
75 | | 'second'
76 | | 'seconds';
77 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/error-util.ts:
--------------------------------------------------------------------------------
1 | type ErrWithMessage = {
2 | message: string;
3 | };
4 |
5 | const errorContainsMessage = (err: unknown): err is ErrWithMessage => {
6 | return (
7 | typeof err === 'object' &&
8 | err !== null &&
9 | 'message' in err &&
10 | typeof (err as Record).message === 'string'
11 | );
12 | };
13 |
14 | const toErrorWithMsg = (possibleErrorMsg: unknown): ErrWithMessage => {
15 | if (errorContainsMessage(possibleErrorMsg)) {
16 | return possibleErrorMsg;
17 | }
18 |
19 | try {
20 | return new Error(JSON.stringify(possibleErrorMsg));
21 | } catch {
22 | // fallback in case there's an error stringifying the maybeError
23 | // like with circular references for example.
24 | return new Error(String(possibleErrorMsg));
25 | }
26 | };
27 |
28 | export function getErrorMessage(err: unknown): string {
29 | return toErrorWithMsg(err).message;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/eval-helper.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../../lib/homeassistant/types';
2 | import { HassEntity } from '../../floorplan-examples/homeassistant';
3 | import { FloorplanConfig, FloorplanCallServiceActionConfig } from './/floorplan-config';
4 | import { FloorplanRuleInfo, FloorplanSvgElementInfo } from './floorplan-info';
5 | import { ColorUtil } from './color-util';
6 | import { DateUtil } from './date-util';
7 | import Sval from 'sval';
8 | import { getErrorMessage } from './error-util';
9 | import estree from 'estree';
10 | import { dispatchFloorplanActionCallEvent } from './events';
11 |
12 | export class EvalHelper {
13 | static cache: { [key: string]: estree.Node } = {};
14 |
15 | static interpreter = new Sval({ ecmaVer: 2019, sandBox: true });
16 | static parsedFunction: estree.Node;
17 |
18 | static expression: string;
19 | static functionBody: string;
20 | static entityState: HassEntity | undefined;
21 |
22 | static util = {
23 | color: ColorUtil,
24 | date: DateUtil,
25 | };
26 |
27 | static isCode(expression: string): boolean {
28 | return this.isCodeBlock(expression) || this.isCodeLine(expression);
29 | }
30 |
31 | static isCodeBlock(expression: string): boolean {
32 | return expression.trim().startsWith('>');
33 | }
34 |
35 | static isCodeLine(expression: string): boolean {
36 | return expression.includes('${') && expression.includes('}');
37 | }
38 |
39 | static evaluate(
40 | expression: string,
41 | hass: HomeAssistant,
42 | config: FloorplanConfig,
43 | entityId?: string,
44 | svgElement?: SVGGraphicsElement,
45 | svgElements?: { [elementId: string]: SVGGraphicsElement },
46 | functions?: unknown,
47 | svgElementInfo?: FloorplanSvgElementInfo,
48 | svg?: SVGGraphicsElement,
49 | ruleInfo?: FloorplanRuleInfo
50 | ): unknown {
51 | this.expression = expression.trim();
52 |
53 | const cacheKey = `${this.expression}_${svgElement ?? ''}`;
54 |
55 | this.parsedFunction = this.cache[cacheKey];
56 | if (this.parsedFunction === undefined) {
57 | this.functionBody = this.expression;
58 |
59 | if (this.isCodeBlock(this.functionBody)) {
60 | this.functionBody = this.functionBody.slice('>'.length).trim(); // expression beginning with > is real JavaScript code
61 | } else if (this.isCodeLine(this.functionBody)) {
62 | if (
63 | this.functionBody.startsWith('"') &&
64 | this.functionBody.endsWith('"')
65 | ) {
66 | this.functionBody = this.functionBody.slice(
67 | 1,
68 | this.functionBody.length - 2
69 | ); // remove leading and trailing quotes
70 | }
71 |
72 | this.functionBody = this.functionBody.replace(/\\"/g, '"'); // change escaped quotes to just quotes
73 |
74 | this.functionBody = `\`${this.functionBody}\`;`;
75 |
76 | if (!this.functionBody.includes('return')) {
77 | this.functionBody = `return ${this.functionBody}`;
78 | }
79 | }
80 |
81 | this.parsedFunction = this.interpreter.parse(
82 | `exports.result = (() => { ${this.functionBody} })();`
83 | ) as estree.Node;
84 | this.cache[cacheKey] = this.parsedFunction;
85 |
86 | // Add global modules in interpreter (static data)
87 | this.interpreter.import('config', config);
88 | this.interpreter.import('util', this.util);
89 | }
90 |
91 | this.entityState = entityId ? hass.states[entityId] : undefined;
92 |
93 | // Add global modules in interpreter (dynamic data)
94 | this.interpreter.import('functions', functions);
95 | this.interpreter.import('entity', this.entityState);
96 | this.interpreter.import('entities', hass.states);
97 | this.interpreter.import('states', hass.states);
98 | this.interpreter.import('hass', hass);
99 | this.interpreter.import('element', svgElement);
100 | this.interpreter.import('elements', svgElements);
101 | this.interpreter.import('svg', svg); // Provide direct access to the root element for rule scripts
102 |
103 | // Let the user call "action" function (to call our service call-handler)
104 | this.interpreter.import('action',
105 | (actionConfig :
106 | FloorplanCallServiceActionConfig) => {
107 | // Set default action
108 | actionConfig.action = actionConfig?.action || 'call-service';
109 |
110 | // Dispatch event to call service
111 | dispatchFloorplanActionCallEvent(svgElement as SVGGraphicsElement, {
112 | actionConfig,
113 | entityId,
114 | svgElementInfo,
115 | ruleInfo,
116 | });
117 | }
118 | );
119 |
120 | try {
121 | this.interpreter.run(this.parsedFunction as estree.Node);
122 | } catch (error) {
123 | throw new EvalError(getErrorMessage(error));
124 |
125 | // throw new EvalError(
126 | // 'Errors while evaluate function (' + error.message + ')'
127 | // );
128 | }
129 |
130 | return this.interpreter.exports.result;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/events.ts:
--------------------------------------------------------------------------------
1 | // src/lib/events.ts
2 | export const HA_FLOORPLAN_ACTION_CALL_EVENT = 'ha-floorplan-service-call';
3 |
4 | export function dispatchFloorplanActionCallEvent(
5 | el: SVGElement | SVGGraphicsElement,
6 | detail: any
7 | ) {
8 | const event = new CustomEvent(HA_FLOORPLAN_ACTION_CALL_EVENT, {
9 | detail,
10 | bubbles: true,
11 | composed: true,
12 | });
13 | el.dispatchEvent(event);
14 | }
--------------------------------------------------------------------------------
/src/components/floorplan/lib/floorplan-config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActionConfig,
3 | BaseActionConfig,
4 | } from '../../../lib/homeassistant/lovelace/types';
5 |
6 | import {
7 | ToggleActionConfig,
8 | CallServiceActionConfig,
9 | NavigateActionConfig,
10 | UrlActionConfig,
11 | MoreInfoActionConfig,
12 | NoActionConfig,
13 | CustomActionConfig,
14 | } from '../../../lib/homeassistant/lovelace/types';
15 |
16 | import {
17 | FloorplanSvgElementInfo,
18 | FloorplanRuleInfo
19 | } from './floorplan-info';
20 |
21 | export class FloorplanConfig {
22 | // Core features
23 | image!: FloorplanImageConfig | string;
24 | stylesheet!: FloorplanStylesheetConfig | string;
25 | log_level!: string;
26 | console_log_level!: string;
27 | rules!: FloorplanRuleConfig[];
28 |
29 | // Optional features
30 | startup_action!:
31 | | FloorplanActionConfig[]
32 | | FloorplanActionConfig
33 | | string
34 | | false;
35 | defaults!: FloorplanRuleConfig;
36 | image_mobile!: FloorplanImageConfig | string;
37 | functions!: string;
38 |
39 | // Experimental features
40 | pages!: string[];
41 | variables!: FloorplanVariableConfig[];
42 | pan_zoom: unknown;
43 | }
44 |
45 | declare global {
46 | interface HASSDomEvents {
47 | 'll-custom': ActionConfig;
48 | }
49 | }
50 |
51 | export interface FloorplanCallServiceActionConfig
52 | extends CallServiceActionConfig {
53 | value: unknown;
54 | _is_internal_action_scope?: boolean;
55 | }
56 |
57 | export interface HoverInfoActionConfig extends BaseActionConfig {
58 | action: 'hover-info';
59 | }
60 |
61 | export type FloorplanActionConfig =
62 | | ToggleActionConfig
63 | | FloorplanCallServiceActionConfig
64 | | NavigateActionConfig
65 | | UrlActionConfig
66 | | MoreInfoActionConfig
67 | | NoActionConfig
68 | | CustomActionConfig
69 | | HoverInfoActionConfig;
70 |
71 | export class FloorplanPageConfig extends FloorplanConfig {
72 | page_id!: string;
73 | master_page!: FloorplanMasterPageConfig;
74 | }
75 |
76 | export class FloorplanMasterPageConfig extends FloorplanPageConfig {
77 | content_element!: string;
78 | }
79 |
80 | export class FloorplanImageConfig {
81 | location!: string;
82 | cache!: boolean;
83 | sizes!: FloorplanImageSize[];
84 | use_screen_width?: boolean;
85 | }
86 |
87 | export class FloorplanImageSize {
88 | min_width = 0;
89 | location!: string;
90 | cache!: boolean;
91 | }
92 |
93 | export class FloorplanStylesheetConfig {
94 | location!: string;
95 | cache!: boolean;
96 | }
97 |
98 | export class FloorplanRuleConfig {
99 | entity!: string;
100 | entities!: (string | FloorplanRuleEntityElementConfig)[];
101 | groups!: string[];
102 | element!: string;
103 | elements!: string[];
104 |
105 | // action_name?: string;
106 | service?: string;
107 | service_data?: Record;
108 | // url?: string;
109 | state_action!:
110 | | FloorplanActionConfig
111 | | FloorplanActionConfig[]
112 | | string
113 | | false;
114 | tap_action!: FloorplanActionConfig | FloorplanActionConfig[] | string | false;
115 | hold_action!:
116 | | FloorplanActionConfig
117 | | FloorplanActionConfig[]
118 | | string
119 | | false;
120 | double_tap_action!:
121 | | FloorplanActionConfig
122 | | FloorplanActionConfig[]
123 | | string
124 | | false;
125 | hover_action!:
126 | | FloorplanActionConfig
127 | | FloorplanActionConfig[]
128 | | string
129 | | false;
130 | hover_info_filter!: string[];
131 | }
132 |
133 | export class FloorplanRuleEntityElementConfig {
134 | entity!: string;
135 | element!: string;
136 | }
137 |
138 | export class FloorplanVariableConfig {
139 | name!: string;
140 | value!: unknown;
141 | }
142 |
143 | export interface FloorplanEventActionCallDetail {
144 | actionConfig: FloorplanCallServiceActionConfig;
145 | entityId?: string;
146 | svgElementInfo?: FloorplanSvgElementInfo;
147 | ruleInfo?: FloorplanRuleInfo;
148 | }
--------------------------------------------------------------------------------
/src/components/floorplan/lib/floorplan-info.ts:
--------------------------------------------------------------------------------
1 | import { HassEntity } from 'home-assistant-js-websocket';
2 | import {
3 | FloorplanPageConfig,
4 | FloorplanRuleConfig,
5 | FloorplanActionConfig,
6 | } from './floorplan-config';
7 |
8 | export class FloorplanPageInfo {
9 | index!: number;
10 | config!: FloorplanPageConfig;
11 | svg!: SVGGraphicsElement;
12 | isMaster!: boolean;
13 | isDefault!: boolean;
14 | }
15 |
16 | export class FloorplanElementInfo {
17 | ruleInfos!: FloorplanRuleInfo[];
18 | }
19 |
20 | export class FloorplanSvgElementInfo {
21 | constructor(
22 | public entityId: string,
23 | public svgElement: SVGGraphicsElement,
24 | public originalSvgElement: SVGGraphicsElement,
25 | public originalBBox: DOMRect | null
26 | ) {}
27 | }
28 |
29 | export class FloorplanRuleInfo {
30 | svgElementInfos: { [key: string]: FloorplanSvgElementInfo } = {};
31 | imageUrl!: string;
32 | imageLoader!: number | undefined;
33 |
34 | constructor(public rule: FloorplanRuleConfig) {}
35 | }
36 |
37 | export class FloorplanEntityInfo {
38 | lastState!: HassEntity | undefined;
39 | entityId!: string;
40 | ruleInfos!: FloorplanRuleInfo[];
41 | }
42 |
43 | export class FloorplanClickContext {
44 | constructor(
45 | public instance: HTMLElement,
46 | public entityId: string | undefined,
47 | public elementId: string | undefined,
48 | public svgElementInfo: FloorplanSvgElementInfo,
49 | public ruleInfo: FloorplanRuleInfo,
50 | public actions: Array
51 | ) {}
52 | }
--------------------------------------------------------------------------------
/src/components/floorplan/lib/logger.ts:
--------------------------------------------------------------------------------
1 | import { Utils } from '../../../lib/utils';
2 |
3 | export class Logger {
4 | logLevelGroups = {
5 | error: ['error'],
6 | warn: ['error', 'warning', 'warn'],
7 | warning: ['error', 'warning', 'warn'],
8 | info: ['error', 'warning', 'warn', 'info'],
9 | debug: ['error', 'warning', 'warn', 'info', 'debug'],
10 | } as { [index: string]: string[] };
11 |
12 | constructor(
13 | public element: HTMLElement,
14 | public logLevel?: string,
15 | public consoleLogLevel?: string
16 | ) {}
17 |
18 | log(level: string, message: string, force = false): void {
19 | const text = `${Utils.formatDate(
20 | new Date()
21 | )} ${level.toUpperCase()} ${message}`;
22 |
23 | const targetLogLevels =
24 | this.logLevel && this.logLevelGroups[this.logLevel.toLowerCase()];
25 | const shouldLog =
26 | targetLogLevels?.length && targetLogLevels.includes(level.toLowerCase());
27 |
28 | if (force || shouldLog) {
29 | if (this.element) {
30 | const listItemElement = document.createElement('li');
31 | Utils.addClass(listItemElement, level);
32 | listItemElement.textContent = text;
33 | this.element.querySelector('ul')?.prepend(listItemElement);
34 | this.element.style.display = 'block';
35 | }
36 | }
37 |
38 | this.consoleLog(level, message, force);
39 | }
40 |
41 | consoleLog(level: string, message: string, force = false): void {
42 | const text = `${Utils.formatDate(
43 | new Date()
44 | )} ${level.toUpperCase()} ${message}`;
45 |
46 | const targetLogLevels =
47 | this.consoleLogLevel &&
48 | this.logLevelGroups[this.consoleLogLevel.toLowerCase()];
49 | const shouldLog =
50 | targetLogLevels?.length && targetLogLevels.includes(level.toLowerCase());
51 |
52 | if (force || shouldLog) {
53 | switch (level) {
54 | case 'error':
55 | console.error(text);
56 | break;
57 |
58 | case 'warn':
59 | case 'warning':
60 | console.warn(text);
61 | break;
62 |
63 | case 'info':
64 | console.info(text);
65 | break;
66 |
67 | default:
68 | console.log(text);
69 | break;
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/long-clicks.ts:
--------------------------------------------------------------------------------
1 | import OuiDomEvents from './oui-dom-events';
2 | const E = OuiDomEvents;
3 |
4 | export class LongClicks {
5 | static observe(elem: HTMLElement | SVGElement): void {
6 | const longClickDuration = 400;
7 |
8 | let timer: ReturnType;
9 | let isLongClick = false;
10 |
11 | const onTapStart = () => {
12 | // console.log('onTapStart: isLongClick:', isLongClick);
13 |
14 | isLongClick = false;
15 |
16 | timer = setTimeout(() => {
17 | isLongClick = true;
18 | // console.log('timer timed out: isLongClick:', isLongClick);
19 | // console.log('timer timed out: dispatching event:', 'longClick');
20 | elem.dispatchEvent(new Event('longClick'));
21 | }, longClickDuration);
22 | };
23 |
24 | const onTapEnd = (evt: Event) => {
25 | clearTimeout(timer);
26 |
27 | if (isLongClick) {
28 | // console.log('onTapEnd: isLongClick:', isLongClick);
29 | // have already triggered long click
30 | } else {
31 | // trigger shortClick, shortMouseup etc
32 | // console.log(
33 | // 'onTapEnd: dispatching event:',
34 | // 'short' + evt.type[0].toUpperCase() + evt.type.slice(1)
35 | // );
36 | elem.dispatchEvent(
37 | new Event('short' + evt.type[0].toUpperCase() + evt.type.slice(1))
38 | );
39 | }
40 | };
41 |
42 | const onTap = (evt: Event) => {
43 | if (isLongClick) {
44 | // console.log('onTap: isLongClick:', isLongClick);
45 | evt.preventDefault();
46 | if (evt.stopImmediatePropagation) evt.stopImmediatePropagation();
47 | }
48 | };
49 |
50 | const onClick = (evt: Event) => {
51 | // console.log('onClick: isLongClick:', isLongClick);
52 | evt.preventDefault();
53 | if (evt.stopImmediatePropagation) evt.stopImmediatePropagation();
54 | };
55 |
56 | E.on(elem, 'mousedown', onTapStart.bind(this));
57 | E.on(elem, 'tapstart', onTapStart.bind(this));
58 |
59 | // [Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event.
60 | // Consider marking event handler as 'passive' to make the page more responsive.
61 | // See https://www.chromestatus.com/feature/5745543795965952
62 | E.on(elem, 'touchstart', onTapStart.bind(this), { passive: true });
63 |
64 | E.on(elem, 'click', onTapEnd.bind(this));
65 |
66 | E.on(elem, 'mouseup', onTapEnd.bind(this));
67 | E.on(elem, 'tapend', onTapEnd.bind(this));
68 | E.on(elem, 'touchend', onTapEnd.bind(this));
69 |
70 | E.on(elem, 'tap', onTap.bind(this));
71 | E.on(elem, 'touch', onTap.bind(this));
72 | E.on(elem, 'mouseup', onTap.bind(this));
73 | E.on(elem, 'tapend', onTap.bind(this));
74 | E.on(elem, 'touchend', onTap.bind(this));
75 |
76 | E.on(elem, 'click', onClick.bind(this));
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/many-clicks.ts:
--------------------------------------------------------------------------------
1 | import OuiDomEvents from './oui-dom-events';
2 |
3 | const E = OuiDomEvents;
4 |
5 | const elements = new Set();
6 |
7 | export class ManyClicks {
8 | static observe(elem: HTMLElement | SVGElement): void {
9 | if (elements.has(elem)) {
10 | return;
11 | }
12 | elements.add(elem);
13 |
14 | const doubleClickDuration = 400;
15 |
16 | let timer: ReturnType;
17 |
18 | let clickCount = 0;
19 |
20 | const onClickEvent = () => {
21 | clickCount++;
22 |
23 | timer = setTimeout(() => {
24 | if (clickCount === 1) {
25 | clickCount = 0;
26 | elem.dispatchEvent(new Event('singleClick'));
27 | }
28 | }, doubleClickDuration);
29 |
30 | if (clickCount === 2) {
31 | clearTimeout(timer);
32 | clickCount = 0;
33 | elem.dispatchEvent(new Event('doubleClick'));
34 | }
35 | };
36 |
37 | E.on(elem, 'click', onClickEvent.bind(this));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/shadow-dom-helper.ts:
--------------------------------------------------------------------------------
1 | export class ShadowDomHelper {
2 | static closestElement(selector: string, base: Element): Element | null {
3 | function __closestFrom(
4 | el: Element | Window | Document | HTMLSlotElement | null
5 | ): Element | null {
6 | if (!el || el === document || el === window) return null;
7 | if ((el as Slottable).assignedSlot) el = (el as Slottable).assignedSlot;
8 | const found = (el as Element).closest(selector);
9 | return found
10 | ? found
11 | : __closestFrom(((el as Element).getRootNode() as ShadowRoot).host);
12 | }
13 | return __closestFrom(base);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/simulator-eval-helper.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../../lib/homeassistant/types';
2 | import { HassEntity } from '../../floorplan-examples/homeassistant';
3 | import { FloorplanConfig } from './/floorplan-config';
4 | import { ColorUtil } from './color-util';
5 | import { DateUtil } from './date-util';
6 | import Sval from 'sval';
7 | import { getErrorMessage } from './error-util';
8 | import estree from 'estree';
9 | import { TimedHassEntity } from '../../floorplan-examples/types';
10 |
11 | export class SimulatorEvalHelper {
12 | static cache: { [key: string]: estree.Node } = {};
13 |
14 | static interpreter = new Sval({ ecmaVer: 2019, sandBox: true });
15 | static parsedFunction: estree.Node;
16 |
17 | static expression: string;
18 | static functionBody: string;
19 | static entityState: HassEntity | TimedHassEntity | undefined;
20 |
21 | static util = {
22 | color: ColorUtil,
23 | date: DateUtil,
24 | };
25 |
26 | static isCode(expression: string): boolean {
27 | return this.isCodeBlock(expression) || this.isCodeLine(expression);
28 | }
29 |
30 | static isCodeBlock(expression: string): boolean {
31 | return expression.trim().startsWith('>');
32 | }
33 |
34 | static isCodeLine(expression: string): boolean {
35 | return expression.includes('${') && expression.includes('}');
36 | }
37 |
38 | static evaluate(
39 | expression: string,
40 | entityState: HassEntity | TimedHassEntity,
41 | ): unknown {
42 | this.expression = expression.trim();
43 |
44 | const cacheKey = `${this.expression}_${entityState.entity_id ?? ''}`;
45 |
46 | this.parsedFunction = this.cache[cacheKey];
47 | if (this.parsedFunction === undefined) {
48 | this.functionBody = this.expression;
49 |
50 | if (this.isCodeBlock(this.functionBody)) {
51 | this.functionBody = this.functionBody.slice('>'.length).trim(); // expression beginning with > is real JavaScript code
52 | } else if (this.isCodeLine(this.functionBody)) {
53 | if (
54 | this.functionBody.startsWith('"') &&
55 | this.functionBody.endsWith('"')
56 | ) {
57 | this.functionBody = this.functionBody.slice(
58 | 1,
59 | this.functionBody.length - 2
60 | ); // remove leading and trailing quotes
61 | }
62 |
63 | this.functionBody = this.functionBody.replace(/\\"/g, '"'); // change escaped quotes to just quotes
64 |
65 | this.functionBody = `\`${this.functionBody}\`;`;
66 |
67 | if (!this.functionBody.includes('return')) {
68 | this.functionBody = `return ${this.functionBody}`;
69 | }
70 | }
71 |
72 | this.parsedFunction = this.interpreter.parse(
73 | `exports.result = (() => { ${this.functionBody} })();`
74 | ) as estree.Node;
75 | this.cache[cacheKey] = this.parsedFunction;
76 |
77 | // Add global modules in interpreter (static data)
78 | this.interpreter.import('util', this.util);
79 | }
80 |
81 | this.entityState = entityState;
82 |
83 | // Add global modules in interpreter (dynamic data)
84 | this.interpreter.import('entity', this.entityState);
85 |
86 | try {
87 | this.interpreter.run(this.parsedFunction as estree.Node);
88 | } catch (error) {
89 | throw new EvalError(getErrorMessage(error));
90 |
91 | // throw new EvalError(
92 | // 'Errors while evaluate function (' + error.message + ')'
93 | // );
94 | }
95 |
96 | return this.interpreter.exports.result;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/floorplan/lib/types.ts:
--------------------------------------------------------------------------------
1 | export enum ClickType {
2 | ShortClick,
3 | LongClick,
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/lit-toast/lit-toast.ts:
--------------------------------------------------------------------------------
1 | import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
2 | import { customElement, property } from 'lit/decorators.js';
3 |
4 | @customElement('lit-toast')
5 | export class LitToast extends LitElement {
6 | @property({ type: String }) public _toastText!: string;
7 |
8 | constructor() {
9 | super();
10 |
11 | this._toastText = '';
12 | }
13 |
14 | protected render(): TemplateResult {
15 | return html` ${this._toastText}
`;
16 | }
17 |
18 | static get styles(): CSSResult {
19 | return css`
20 | :host {
21 | display: none;
22 | justify-content: center;
23 | width: 100%;
24 | /*visibility: hidden;*/
25 | position: fixed;
26 | z-index: var(--lt-z-index, 2);
27 | bottom: var(--lt-bottom, 40px);
28 | }
29 |
30 | :host(.show) {
31 | display: flex;
32 | /*visibility: visible;*/
33 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
34 | animation: fadein 0.5s, fadeout 0.5s 2.5s;
35 | }
36 |
37 | div {
38 | min-width: 100px;
39 | background-color: var(--lt-background-color, #292929);
40 | color: var(--lt-color, #dddddd);
41 | text-align: center;
42 | border-radius: var(--lt-border-radius, 2px);
43 | padding: var(--lt-padding, 16px);
44 | border: var(--lt-border, none);
45 | font-size: var(--lt-font-size, 1em);
46 | font-family: var(--lt-font-family, sans-serif);
47 | }
48 |
49 | @-webkit-keyframes fadein {
50 | from {
51 | bottom: 0;
52 | opacity: 0;
53 | }
54 | to {
55 | bottom: var(--lt-bottom, 40px);
56 | opacity: 1;
57 | }
58 | }
59 |
60 | @keyframes fadein {
61 | from {
62 | bottom: 0;
63 | opacity: 0;
64 | }
65 | to {
66 | bottom: var(--lt-bottom, 40px);
67 | opacity: 1;
68 | }
69 | }
70 |
71 | @-webkit-keyframes fadeout {
72 | from {
73 | bottom: var(--lt-bottom, 40px);
74 | opacity: 1;
75 | }
76 | to {
77 | bottom: 0;
78 | opacity: 0;
79 | }
80 | }
81 |
82 | @keyframes fadeout {
83 | from {
84 | bottom: var(--lt-bottom, 40px);
85 | opacity: 1;
86 | }
87 | to {
88 | bottom: 0;
89 | opacity: 0;
90 | }
91 | }
92 | `;
93 | }
94 |
95 | // To read out loud the toast
96 | firstUpdated(): void {
97 | this.style.setProperty('aria-live', 'assertive');
98 | this.style.setProperty('aria-atomic', 'true');
99 | this.style.setProperty('aria-relevant', 'all');
100 | }
101 |
102 | show(text = ''): void {
103 | if (this.className === 'show') {
104 | // Do nothing, prevent spamming
105 | } else {
106 | this._toastText = text;
107 | this.className = 'show';
108 | setTimeout(() => {
109 | this.className = this.className.replace('show', '');
110 | }, 3000);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { FloorplanCard } from './components/floorplan-card/floorplan-card';
2 | import { FloorplanPanel } from './components/floorplan-panel/floorplan-panel';
3 |
4 | export { FloorplanCard, FloorplanPanel };
5 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/common/dom/fire_event.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 | // Polymer legacy event helpers used courtesy of the Polymer project.
4 | //
5 | // Copyright (c) 2017 The Polymer Authors. All rights reserved.
6 | //
7 | // Redistribution and use in source and binary forms, with or without
8 | // modification, are permitted provided that the following conditions are
9 | // met:
10 | //
11 | // * Redistributions of source code must retain the above copyright
12 | // notice, this list of conditions and the following disclaimer.
13 | // * Redistributions in binary form must reproduce the above
14 | // copyright notice, this list of conditions and the following disclaimer
15 | // in the documentation and/or other materials provided with the
16 | // distribution.
17 | // * Neither the name of Google Inc. nor the names of its
18 | // contributors may be used to endorse or promote products derived from
19 | // this software without specific prior written permission.
20 | //
21 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 |
33 | declare global {
34 | // eslint-disable-next-line
35 | interface HASSDomEvents {}
36 | }
37 |
38 | export type ValidHassDomEvent = keyof HASSDomEvents;
39 |
40 | export interface HASSDomEvent extends Event {
41 | detail: T;
42 | }
43 |
44 | /**
45 | * Dispatches a custom event with an optional detail value.
46 | *
47 | * @param {string} type Name of event type.
48 | * @param {*=} detail Detail value containing event-specific
49 | * payload.
50 | * @param {{ bubbles: (boolean|undefined),
51 | * cancelable: (boolean|undefined),
52 | * composed: (boolean|undefined) }=}
53 | * options Object specifying options. These may include:
54 | * `bubbles` (boolean, defaults to `true`),
55 | * `cancelable` (boolean, defaults to false), and
56 | * `node` on which to fire the event (HTMLElement, defaults to `this`).
57 | * @return {Event} The new event that was fired.
58 | */
59 | export const fireEvent = (
60 | node: HTMLElement | Window,
61 | type: HassEvent,
62 | detail?: HASSDomEvents[HassEvent],
63 | options?: {
64 | bubbles?: boolean;
65 | cancelable?: boolean;
66 | composed?: boolean;
67 | }
68 | ) => {
69 | options = options || {};
70 | detail = (detail === null || detail === undefined ? {} : detail) as any;
71 | const event = new Event(type, {
72 | bubbles: options.bubbles === undefined ? true : options.bubbles,
73 | cancelable: Boolean(options.cancelable),
74 | composed: options.composed === undefined ? true : options.composed,
75 | });
76 | (event as any).detail = detail;
77 | node.dispatchEvent(event);
78 | return event;
79 | };
80 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/common/navigate.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 | import { fireEvent } from './dom/fire_event';
4 |
5 | declare global {
6 | // for fire event
7 | interface HASSDomEvents {
8 | 'location-changed': {
9 | replace: boolean;
10 | };
11 | }
12 | }
13 |
14 | export const navigate = (_node: any, path: string, replace = false) => {
15 | const __DEMO__ = false;
16 |
17 | if (__DEMO__) {
18 | if (replace) {
19 | history.replaceState(null, '', `${location.pathname}#${path}`);
20 | } else {
21 | window.location.hash = path;
22 | }
23 | } else if (replace) {
24 | history.replaceState(null, '', path);
25 | } else {
26 | history.pushState(null, '', path);
27 | }
28 | fireEvent(window, 'location-changed', {
29 | replace,
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/data/entity.ts:
--------------------------------------------------------------------------------
1 | export const UNAVAILABLE = 'unavailable';
2 | export const UNKNOWN = 'unknown';
3 |
4 | export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN];
5 |
6 | export const ENTITY_COMPONENT_DOMAINS = [
7 | 'air_quality',
8 | 'alarm_control_panel',
9 | 'alert',
10 | 'automation',
11 | 'binary_sensor',
12 | 'calendar',
13 | 'camera',
14 | 'counter',
15 | 'cover',
16 | 'dominos',
17 | 'fan',
18 | 'geo_location',
19 | 'group',
20 | 'image_processing',
21 | 'input_boolean',
22 | 'input_datetime',
23 | 'input_number',
24 | 'input_select',
25 | 'input_text',
26 | 'light',
27 | 'lock',
28 | 'mailbox',
29 | 'media_player',
30 | 'person',
31 | 'plant',
32 | 'remember_the_milk',
33 | 'remote',
34 | 'scene',
35 | 'script',
36 | 'sensor',
37 | 'switch',
38 | 'timer',
39 | 'utility_meter',
40 | 'vacuum',
41 | 'weather',
42 | 'wink',
43 | 'zha',
44 | 'zwave',
45 | ];
46 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/data/haptics.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 | /**
3 | * Broadcast haptic feedback requests
4 | */
5 |
6 | import { fireEvent, HASSDomEvent } from '../common/dom/fire_event';
7 |
8 | // Allowed types are from iOS HIG.
9 | // https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics
10 | // Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible.
11 | export type HapticType =
12 | | 'success'
13 | | 'warning'
14 | | 'failure'
15 | | 'light'
16 | | 'medium'
17 | | 'heavy'
18 | | 'selection';
19 |
20 | declare global {
21 | // for fire event
22 | interface HASSDomEvents {
23 | haptic: HapticType;
24 | }
25 |
26 | interface GlobalEventHandlersEventMap {
27 | haptic: HASSDomEvent;
28 | }
29 | }
30 |
31 | export const forwardHaptic = (hapticType: HapticType) => {
32 | fireEvent(window, 'haptic', hapticType);
33 | };
34 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/dialogs/more-info/ha-more-info-dialog.ts:
--------------------------------------------------------------------------------
1 | export interface MoreInfoDialogParams {
2 | entityId: string | null;
3 | }
4 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/cards/hui-error-card.ts:
--------------------------------------------------------------------------------
1 | import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
2 | import { HomeAssistant } from '../../../types';
3 | import { LovelaceCard } from '../types';
4 | import { ErrorCardConfig } from './types';
5 | import { dump } from 'js-yaml';
6 |
7 | // @customElement('hui-error-card')
8 | export class HuiErrorCard extends LitElement implements LovelaceCard {
9 | public hass?: HomeAssistant;
10 |
11 | /* @internalProperty() */ private _config?: ErrorCardConfig;
12 |
13 | public getCardSize(): number {
14 | return 4;
15 | }
16 |
17 | public setConfig(config: ErrorCardConfig): void {
18 | this._config = config;
19 | }
20 |
21 | protected render(): TemplateResult {
22 | if (!this._config) {
23 | return html``;
24 | }
25 |
26 | return html`
27 | ${this._config.error}
28 | ${this._config.origConfig
29 | ? html`${dump(this._config.origConfig)} `
30 | : ''}
31 | `;
32 | }
33 |
34 | static get styles(): CSSResult {
35 | return css`
36 | :host {
37 | display: block;
38 | background-color: var(--error-color);
39 | color: var(--color-on-error, white);
40 | padding: 8px;
41 | font-weight: 500;
42 | user-select: text;
43 | cursor: default;
44 | }
45 | pre {
46 | font-family: var(--code-font-family, monospace);
47 | }
48 | `;
49 | }
50 | }
51 |
52 | declare global {
53 | interface HTMLElementTagNameMap {
54 | 'hui-error-card': HuiErrorCard;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/common/validate-condition.ts:
--------------------------------------------------------------------------------
1 | import { HomeAssistant } from '../../../types';
2 | import { UNAVAILABLE } from '../../../data/entity';
3 |
4 | export interface Condition {
5 | entity: string;
6 | state?: string;
7 | state_not?: string;
8 | }
9 |
10 | export function checkConditionsMet(
11 | conditions: Condition[],
12 | hass: HomeAssistant
13 | ): boolean {
14 | return conditions.every((c) => {
15 | const state = hass.states[c.entity]
16 | ? hass.states[c.entity].state
17 | : UNAVAILABLE;
18 |
19 | return c.state ? state === c.state : state !== c.state_not;
20 | });
21 | }
22 |
23 | export function validateConditionalConfig(conditions: Condition[]): boolean {
24 | return conditions.every(
25 | (c) => (c.entity && (c.state || c.state_not)) as unknown as boolean
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/editor/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import {
3 | any,
4 | array,
5 | boolean,
6 | number,
7 | object,
8 | optional,
9 | string,
10 | union,
11 | } from 'superstruct';
12 | import {
13 | ActionConfig,
14 | LovelaceCardConfig,
15 | LovelaceViewConfig,
16 | ShowViewConfig,
17 | } from '../../../data/lovelace';
18 | import { EntityConfig, LovelaceRowConfig } from '../entity-rows/types';
19 | import { LovelaceHeaderFooterConfig } from '../header-footer/types';
20 |
21 | export interface YamlChangedEvent extends Event {
22 | detail: {
23 | yaml: string;
24 | };
25 | }
26 |
27 | export interface GUIModeChangedEvent {
28 | guiMode: boolean;
29 | guiModeAvailable: boolean;
30 | }
31 |
32 | export interface ViewEditEvent extends Event {
33 | detail: {
34 | config: LovelaceViewConfig;
35 | };
36 | }
37 |
38 | export interface ViewVisibilityChangeEvent {
39 | visible: ShowViewConfig[];
40 | }
41 |
42 | export interface ConfigValue {
43 | format: 'json' | 'yaml';
44 | value?: string | LovelaceCardConfig;
45 | }
46 |
47 | export interface ConfigError {
48 | type: string;
49 | message: string;
50 | }
51 |
52 | export interface EntitiesEditorEvent {
53 | detail?: {
54 | entities?: EntityConfig[];
55 | item?: any;
56 | };
57 | target?: EventTarget;
58 | }
59 |
60 | export interface EditorTarget extends EventTarget {
61 | value?: string;
62 | index?: number;
63 | checked?: boolean;
64 | configValue?: string;
65 | type?: HTMLInputElement['type'];
66 | config: ActionConfig;
67 | }
68 |
69 | export interface Card {
70 | type: string;
71 | name?: string;
72 | description?: string;
73 | showElement?: boolean;
74 | isCustom?: boolean;
75 | }
76 |
77 | export interface HeaderFooter {
78 | type: string;
79 | icon?: string;
80 | }
81 |
82 | export interface CardPickTarget extends EventTarget {
83 | config: LovelaceCardConfig;
84 | }
85 |
86 | export interface SubElementEditorConfig {
87 | index?: number;
88 | elementConfig?: LovelaceRowConfig | LovelaceHeaderFooterConfig;
89 | type: string;
90 | }
91 |
92 | export interface EditSubElementEvent {
93 | subElementConfig: SubElementEditorConfig;
94 | }
95 |
96 | export const actionConfigStruct = object({
97 | action: string(),
98 | navigation_path: optional(string()),
99 | navigation_replace: optional(boolean()),
100 | url_path: optional(string()),
101 | service: optional(string()),
102 | service_data: optional(object()),
103 | });
104 |
105 | const buttonEntitiesRowConfigStruct = object({
106 | type: string(),
107 | name: string(),
108 | action_name: optional(string()),
109 | tap_action: actionConfigStruct,
110 | hold_action: optional(actionConfigStruct),
111 | double_tap_action: optional(actionConfigStruct),
112 | });
113 |
114 | const castEntitiesRowConfigStruct = object({
115 | type: string(),
116 | view: union([string(), number()]),
117 | dashboard: optional(string()),
118 | name: optional(string()),
119 | icon: optional(string()),
120 | hide_if_unavailable: optional(boolean()),
121 | });
122 |
123 | const callServiceEntitiesRowConfigStruct = object({
124 | type: string(),
125 | name: string(),
126 | service: string(),
127 | icon: optional(string()),
128 | action_name: optional(string()),
129 | service_data: optional(any()),
130 | });
131 |
132 | const conditionalEntitiesRowConfigStruct = object({
133 | type: string(),
134 | row: any(),
135 | conditions: array(
136 | object({
137 | entity: string(),
138 | state: optional(string()),
139 | state_not: optional(string()),
140 | })
141 | ),
142 | });
143 |
144 | const dividerEntitiesRowConfigStruct = object({
145 | type: string(),
146 | style: optional(any()),
147 | });
148 |
149 | const sectionEntitiesRowConfigStruct = object({
150 | type: string(),
151 | label: optional(string()),
152 | });
153 |
154 | const webLinkEntitiesRowConfigStruct = object({
155 | type: string(),
156 | url: string(),
157 | name: optional(string()),
158 | icon: optional(string()),
159 | });
160 |
161 | const buttonsEntitiesRowConfigStruct = object({
162 | type: string(),
163 | entities: array(
164 | union([
165 | object({
166 | entity: string(),
167 | icon: optional(string()),
168 | image: optional(string()),
169 | name: optional(string()),
170 | }),
171 | string(),
172 | ])
173 | ),
174 | });
175 |
176 | const attributeEntitiesRowConfigStruct = object({
177 | type: string(),
178 | entity: string(),
179 | attribute: string(),
180 | prefix: optional(string()),
181 | suffix: optional(string()),
182 | name: optional(string()),
183 | });
184 |
185 | export const entitiesConfigStruct = union([
186 | object({
187 | entity: string(),
188 | name: optional(string()),
189 | icon: optional(string()),
190 | image: optional(string()),
191 | secondary_info: optional(string()),
192 | format: optional(string()),
193 | state_color: optional(boolean()),
194 | tap_action: optional(actionConfigStruct),
195 | hold_action: optional(actionConfigStruct),
196 | double_tap_action: optional(actionConfigStruct),
197 | }),
198 | string(),
199 | buttonEntitiesRowConfigStruct,
200 | castEntitiesRowConfigStruct,
201 | conditionalEntitiesRowConfigStruct,
202 | dividerEntitiesRowConfigStruct,
203 | sectionEntitiesRowConfigStruct,
204 | webLinkEntitiesRowConfigStruct,
205 | buttonsEntitiesRowConfigStruct,
206 | attributeEntitiesRowConfigStruct,
207 | callServiceEntitiesRowConfigStruct,
208 | ]);
209 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/elements/types.ts:
--------------------------------------------------------------------------------
1 | import { ActionConfig } from '../../../data/lovelace';
2 | import { HomeAssistant } from '../../../types';
3 | import { Condition } from '../common/validate-condition';
4 |
5 | interface LovelaceElementConfigBase {
6 | type: string;
7 | style: Record;
8 | }
9 |
10 | export type LovelaceElementConfig =
11 | | ConditionalElementConfig
12 | | IconElementConfig
13 | | ImageElementConfig
14 | | ServiceButtonElementConfig
15 | | StateBadgeElementConfig
16 | | StateIconElementConfig
17 | | StateLabelElementConfig;
18 |
19 | export interface LovelaceElement extends HTMLElement {
20 | hass?: HomeAssistant;
21 | setConfig(config: LovelaceElementConfig): void;
22 | }
23 |
24 | export interface ConditionalElementConfig extends LovelaceElementConfigBase {
25 | conditions: Condition[];
26 | elements: LovelaceElementConfigBase[];
27 | }
28 |
29 | export interface IconElementConfig extends LovelaceElementConfigBase {
30 | entity?: string;
31 | name?: string;
32 | tap_action?: ActionConfig;
33 | hold_action?: ActionConfig;
34 | double_tap_action?: ActionConfig;
35 | icon: string;
36 | }
37 |
38 | export interface ImageElementConfig extends LovelaceElementConfigBase {
39 | entity?: string;
40 | tap_action?: ActionConfig;
41 | hold_action?: ActionConfig;
42 | double_tap_action?: ActionConfig;
43 | image?: string;
44 | state_image?: string;
45 | camera_image?: string;
46 | filter?: string;
47 | state_filter?: string;
48 | aspect_ratio?: string;
49 | }
50 |
51 | export interface ServiceButtonElementConfig extends LovelaceElementConfigBase {
52 | title?: string;
53 | service?: string;
54 | service_data?: Record;
55 | }
56 |
57 | export interface StateBadgeElementConfig extends LovelaceElementConfigBase {
58 | entity: string;
59 | title?: string;
60 | tap_action?: ActionConfig;
61 | hold_action?: ActionConfig;
62 | double_tap_action?: ActionConfig;
63 | }
64 |
65 | export interface StateIconElementConfig extends LovelaceElementConfigBase {
66 | entity: string;
67 | tap_action?: ActionConfig;
68 | hold_action?: ActionConfig;
69 | double_tap_action?: ActionConfig;
70 | icon?: string;
71 | state_color?: boolean;
72 | }
73 |
74 | export interface StateLabelElementConfig extends LovelaceElementConfigBase {
75 | entity: string;
76 | attribute?: string;
77 | prefix?: string;
78 | suffix?: string;
79 | tap_action?: ActionConfig;
80 | hold_action?: ActionConfig;
81 | double_tap_action?: ActionConfig;
82 | }
83 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/entity-rows/types.ts:
--------------------------------------------------------------------------------
1 | import { ActionConfig } from '../../../data/lovelace';
2 | import { HomeAssistant } from '../../../types';
3 | import { Condition } from '../common/validate-condition';
4 |
5 | export interface EntityConfig {
6 | entity: string;
7 | type?: string;
8 | name?: string;
9 | icon?: string;
10 | image?: string;
11 | }
12 | export interface ActionRowConfig extends EntityConfig {
13 | action_name?: string;
14 | }
15 | export interface EntityFilterEntityConfig extends EntityConfig {
16 | state_filter?: Array<{ key: string } | string>;
17 | }
18 | export interface DividerConfig {
19 | type: 'divider';
20 | style: Record;
21 | }
22 | export interface SectionConfig {
23 | type: 'section';
24 | label: string;
25 | }
26 | export interface WeblinkConfig {
27 | type: 'weblink';
28 | name?: string;
29 | icon?: string;
30 | url: string;
31 | }
32 | export interface TextConfig {
33 | type: 'text';
34 | name: string;
35 | icon?: string;
36 | text: string;
37 | }
38 | export interface CallServiceConfig extends EntityConfig {
39 | type: 'call-service';
40 | service: string;
41 | service_data?: Record;
42 | action_name?: string;
43 | }
44 | export interface ButtonRowConfig extends EntityConfig {
45 | type: 'button';
46 | action_name?: string;
47 | tap_action?: ActionConfig;
48 | hold_action?: ActionConfig;
49 | double_tap_action?: ActionConfig;
50 | }
51 | export interface CastConfig {
52 | type: 'cast';
53 | icon?: string;
54 | name?: string;
55 | view: string | number;
56 | dashboard?: string;
57 | // Hide the row if either unsupported browser or no API available.
58 | hide_if_unavailable?: boolean;
59 | }
60 | export interface ButtonsRowConfig {
61 | type: 'buttons';
62 | entities: Array;
63 | }
64 | export type LovelaceRowConfig =
65 | | EntityConfig
66 | | DividerConfig
67 | | SectionConfig
68 | | WeblinkConfig
69 | | CallServiceConfig
70 | | CastConfig
71 | | ButtonRowConfig
72 | | ButtonsRowConfig
73 | | ConditionalRowConfig
74 | | AttributeRowConfig
75 | | TextConfig;
76 |
77 | export interface LovelaceRow extends HTMLElement {
78 | hass?: HomeAssistant;
79 | editMode?: boolean;
80 | setConfig(config: LovelaceRowConfig): void;
81 | }
82 |
83 | export interface ConditionalRowConfig extends EntityConfig {
84 | row: EntityConfig;
85 | conditions: Condition[];
86 | }
87 | export interface AttributeRowConfig extends EntityConfig {
88 | attribute: string;
89 | prefix?: string;
90 | suffix?: string;
91 | }
92 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/header-footer/types.ts:
--------------------------------------------------------------------------------
1 | import { ActionConfig } from '../../../data/lovelace';
2 | import { object, optional, union, string, number, array } from 'superstruct';
3 | import { actionConfigStruct, entitiesConfigStruct } from '../editor/types';
4 | import { EntityConfig } from '../entity-rows/types';
5 |
6 | export interface LovelaceHeaderFooterConfig {
7 | type: string;
8 | }
9 |
10 | export interface ButtonsHeaderFooterConfig extends LovelaceHeaderFooterConfig {
11 | entities: Array;
12 | }
13 |
14 | export interface GraphHeaderFooterConfig extends LovelaceHeaderFooterConfig {
15 | entity: string;
16 | detail?: number;
17 | hours_to_show?: number;
18 | }
19 |
20 | export interface PictureHeaderFooterConfig extends LovelaceHeaderFooterConfig {
21 | image: string;
22 | tap_action?: ActionConfig;
23 | hold_action?: ActionConfig;
24 | double_tap_action?: ActionConfig;
25 | }
26 |
27 | export const pictureHeaderFooterConfigStruct = object({
28 | type: string(),
29 | image: string(),
30 | tap_action: optional(actionConfigStruct),
31 | hold_action: optional(actionConfigStruct),
32 | double_tap_action: optional(actionConfigStruct),
33 | });
34 |
35 | export const buttonsHeaderFooterConfigStruct = object({
36 | type: string(),
37 | entities: array(entitiesConfigStruct),
38 | });
39 |
40 | export const graphHeaderFooterConfigStruct = object({
41 | type: string(),
42 | entity: string(),
43 | detail: optional(number()),
44 | hours_to_show: optional(number()),
45 | });
46 |
47 | export const headerFooterConfigStructs = union([
48 | pictureHeaderFooterConfigStruct,
49 | buttonsHeaderFooterConfigStruct,
50 | graphHeaderFooterConfigStruct,
51 | ]);
52 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/panels/lovelace/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import {
3 | LovelaceBadgeConfig,
4 | LovelaceCardConfig,
5 | LovelaceConfig,
6 | } from '../../data/lovelace';
7 | import { Constructor, HomeAssistant } from '../../types';
8 | import { LovelaceRow, LovelaceRowConfig } from './entity-rows/types';
9 | import { LovelaceHeaderFooterConfig } from './header-footer/types';
10 |
11 | declare global {
12 | // eslint-disable-next-line
13 | interface HASSDomEvents {
14 | 'll-rebuild': Record;
15 | 'll-badge-rebuild': Record;
16 | }
17 | }
18 |
19 | export interface Lovelace {
20 | config: LovelaceConfig;
21 | editMode: boolean;
22 | urlPath: string | null;
23 | mode: 'generated' | 'yaml' | 'storage';
24 | language: string;
25 | enableFullEditMode: () => void;
26 | setEditMode: (editMode: boolean) => void;
27 | saveConfig: (newConfig: LovelaceConfig) => Promise;
28 | deleteConfig: () => Promise;
29 | }
30 |
31 | export interface LovelaceBadge extends HTMLElement {
32 | hass?: HomeAssistant;
33 | setConfig(config: LovelaceBadgeConfig): void;
34 | }
35 |
36 | export interface LovelaceCard extends HTMLElement {
37 | hass?: HomeAssistant;
38 | isPanel?: boolean;
39 | editMode?: boolean;
40 | getCardSize(): number | Promise;
41 | setConfig(config: LovelaceCardConfig): void;
42 | }
43 |
44 | export interface LovelaceCardConstructor extends Constructor {
45 | getStubConfig?: (
46 | hass: HomeAssistant,
47 | entities: string[],
48 | entitiesFallback: string[]
49 | ) => LovelaceCardConfig;
50 | getConfigElement?: () => LovelaceCardEditor;
51 | }
52 |
53 | export interface LovelaceHeaderFooterConstructor
54 | extends Constructor {
55 | getStubConfig?: (
56 | hass: HomeAssistant,
57 | entities: string[],
58 | entitiesFallback: string[]
59 | ) => LovelaceHeaderFooterConfig;
60 | getConfigElement?: () => LovelaceHeaderFooterEditor;
61 | }
62 |
63 | export interface LovelaceRowConstructor extends Constructor {
64 | getConfigElement?: () => LovelaceRowEditor;
65 | }
66 |
67 | export interface LovelaceHeaderFooter extends HTMLElement {
68 | hass?: HomeAssistant;
69 | getCardSize(): number | Promise;
70 | setConfig(config: LovelaceHeaderFooterConfig): void;
71 | }
72 |
73 | export interface LovelaceCardEditor extends LovelaceGenericElementEditor {
74 | setConfig(config: LovelaceCardConfig): void;
75 | }
76 |
77 | export interface LovelaceHeaderFooterEditor
78 | extends LovelaceGenericElementEditor {
79 | setConfig(config: LovelaceHeaderFooterConfig): void;
80 | }
81 |
82 | export interface LovelaceRowEditor extends LovelaceGenericElementEditor {
83 | setConfig(config: LovelaceRowConfig): void;
84 | }
85 |
86 | export interface LovelaceGenericElementEditor extends HTMLElement {
87 | hass?: HomeAssistant;
88 | lovelace?: LovelaceConfig;
89 | setConfig(config: any): void;
90 | refreshYamlEditor?: (focus: boolean) => void;
91 | }
92 |
--------------------------------------------------------------------------------
/src/lib/homeassistant/state/more-info-mixin.ts:
--------------------------------------------------------------------------------
1 | import type { MoreInfoDialogParams } from '../dialogs/more-info/ha-more-info-dialog';
2 |
3 | declare global {
4 | // for fire event
5 | interface HASSDomEvents {
6 | 'hass-more-info': MoreInfoDialogParams;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/e2e/README.md:
--------------------------------------------------------------------------------
1 | # Using playwright
2 |
3 | ## Preparation
4 |
5 | Start by installing the Playwright Test utils with: `npx playwright install`
6 |
7 | Hereafter, run `npm run test:e2e` from the project root.
--------------------------------------------------------------------------------
/tests/jest/jest-common-utils.ts:
--------------------------------------------------------------------------------
1 | export const sleep = (ms: number) =>
2 | new Promise((resolve) => setTimeout(resolve, ms));
3 |
4 | /**
5 | * Retry a function until it succeeds or the maximum number of retries is reached.
6 | * @param fn The function to retry.
7 | * @param retries The maximum number of retries.
8 | * @param delay The delay between retries in milliseconds.
9 | * @returns The result of the function if it succeeds.
10 | * @throws An error if the function does not succeed within the maximum retries.
11 | */
12 | export const retry = async (
13 | fn: () => Promise,
14 | retries: number,
15 | delay: number
16 | ): Promise => {
17 | let attempt = 0;
18 | while (attempt < retries) {
19 | try {
20 | return await fn();
21 | } catch (error) {
22 | attempt++;
23 | if (attempt >= retries) {
24 | throw error;
25 | }
26 | await sleep(delay);
27 | }
28 | }
29 | throw new Error('Retry function failed unexpectedly');
30 | };
--------------------------------------------------------------------------------
/tests/jest/jest-floorplan-utils.ts:
--------------------------------------------------------------------------------
1 | import { retry } from './jest-common-utils';
2 | import { FloorplanExampleElement } from '../../src/components/floorplan-examples/floorplan-example';
3 | import { FloorplanCard } from '../../src/components/floorplan-card/floorplan-card';
4 | import { FloorplanElement } from '../../src/components/floorplan/floorplan-element';
5 | type QuerySelector = {
6 | selector: string;
7 | errorMessage: string;
8 | instanceOf: new (...args: any[]) => T;
9 | };
10 |
11 | export const global_document_object_key = '__floorplanTest' as string;
12 |
13 | async function getElement(
14 | query: QuerySelector,
15 | root: ParentNode = document,
16 | retries = 5,
17 | delay = 1000
18 | ): Promise {
19 | return retry(async () => {
20 | const element = root.querySelector(query.selector) as T;
21 | if (!element) {
22 | throw new Error(query.errorMessage);
23 | }
24 | expect(element).toBeInstanceOf(query.instanceOf);
25 | return element;
26 | }, retries, delay);
27 | }
28 |
29 | export async function getFloorplanExampleElement(
30 | retries = 5,
31 | delay = 1000
32 | ): Promise {
33 | return getElement(
34 | {
35 | selector: 'floorplan-example',
36 | errorMessage: 'floorplan-example not found',
37 | instanceOf: FloorplanExampleElement,
38 | },
39 | document,
40 | retries,
41 | delay
42 | );
43 | }
44 |
45 | export async function getFloorplanCard(
46 | retries = 5,
47 | delay = 1000
48 | ): Promise {
49 | const floorplanElement = await getFloorplanExampleElement(retries, delay);
50 | if (!floorplanElement.shadowRoot) throw new Error('Shadow root not found');
51 |
52 | return getElement(
53 | {
54 | selector: 'floorplan-card',
55 | errorMessage: 'floorplan-card not found inside floorplan-example',
56 | instanceOf: FloorplanCard,
57 | },
58 | floorplanElement.shadowRoot,
59 | retries,
60 | delay
61 | );
62 | }
63 |
64 | export async function getFloorplanElement(
65 | retries = 5,
66 | delay = 1000
67 | ): Promise {
68 | const card = await getFloorplanCard(retries, delay);
69 | if (!card.shadowRoot) throw new Error('Shadow root not found');
70 | return getElement(
71 | {
72 | selector: 'floorplan-element',
73 | errorMessage: 'floorplan-element not found inside floorplan-card',
74 | instanceOf: FloorplanElement,
75 | },
76 | card.shadowRoot,
77 | retries,
78 | delay
79 | );
80 | }
81 |
82 | export async function getFloorplanSvg(
83 | retries = 5,
84 | delay = 1000
85 | ): Promise {
86 | const floorplanElement = await getFloorplanElement(retries, delay);
87 | if (!floorplanElement.shadowRoot) throw new Error('Shadow root not found');
88 |
89 | return getElement(
90 | {
91 | selector: 'svg',
92 | errorMessage: 'SVG element not found inside floorplan-element',
93 | instanceOf: SVGElement,
94 | },
95 | floorplanElement.shadowRoot,
96 | retries,
97 | delay
98 | );
99 | }
100 |
101 | export function createFloorplanExampleElement(
102 | exampleConfig: {
103 | name: string;
104 | dir: string;
105 | configFile?: string;
106 | configYaml?: string;
107 | simulationFile?: string;
108 | simulationYaml?: string;
109 | isCard: boolean;
110 | },
111 | examplespath: string,
112 | isDemo: boolean,
113 | notify: () => void
114 | ): FloorplanExampleElement {
115 | if (!exampleConfig.configFile && !exampleConfig.configYaml) {
116 | throw new Error('Either configFile or configYaml must be set.');
117 | }
118 |
119 | const floorplanExampleElement = document.createElement(
120 | 'floorplan-example'
121 | ) as FloorplanExampleElement;
122 |
123 | // Set properties
124 | floorplanExampleElement.examplespath = examplespath;
125 | floorplanExampleElement.example = exampleConfig;
126 | floorplanExampleElement.isDemo = isDemo;
127 | floorplanExampleElement.notify = notify;
128 |
129 | document.body.appendChild(floorplanExampleElement);
130 | return floorplanExampleElement;
131 | }
132 |
133 | // Helper to pull the result of a given execute statement, which are saved to the window dom called floorplanTest
134 | export async function getFloorplanHelper(helper_key: string): Promise {
135 | // Use the utility function to get the floorplan element
136 | const floorplan_element = await getFloorplanElement();
137 | expect(floorplan_element).toBeInstanceOf(FloorplanElement);
138 |
139 | const floorplan_view = floorplan_element?.shadowRoot?.host?.ownerDocument
140 | ?.defaultView as any;
141 | const document = floorplan_view?._document as Document;
142 |
143 | let global_helper_obj;
144 | if (global_document_object_key in document)
145 | global_helper_obj = (document as any)[global_document_object_key] as any;
146 |
147 | // Check that the window.${test_obj_ref} exists
148 | expect(global_helper_obj).toBeDefined();
149 |
150 | // Check if helper_to_test is part of test_obj
151 | expect(helper_key in global_helper_obj);
152 |
153 | return global_helper_obj[helper_key];
154 | };
155 |
--------------------------------------------------------------------------------
/tests/jest/tests/disabled/README.md:
--------------------------------------------------------------------------------
1 | This is a folder to temporary store tests, you'd not like to run.
2 |
3 | There's plenty of other ways to to this; but this is simple stupid.
--------------------------------------------------------------------------------
/tests/jest/tests/floorplan-configuration.test.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import '../../../src/components/floorplan-examples/floorplan-examples';
3 | import { FloorplanElement } from '../../../src/components/floorplan/floorplan-element';
4 | import {
5 | createFloorplanExampleElement,
6 | getFloorplanElement,
7 | getFloorplanSvg,
8 | } from '../jest-floorplan-utils';
9 | import { retry } from '../jest-common-utils';
10 | import { jest } from '@jest/globals';
11 |
12 | describe('Configuration', () => {
13 | beforeEach(() => {
14 | });
15 |
16 | afterEach(() => {
17 | // Remove the floorplan-example element from the DOM
18 | const element = document.querySelector('floorplan-example');
19 | if (element) element.remove();
20 | });
21 |
22 | it('Will initiate ha-floorplan if no rules and stylesheet', async () => {
23 | const simulatedEntity = 'sensor.temperature_living_area';
24 | const targetSvgElementId = 'radar-toggle-btn-text';
25 |
26 | createFloorplanExampleElement(
27 | {
28 | name: 'TestPlate',
29 | dir: 'test_plate',
30 | configYaml: `title: TestPlate
31 | config:
32 | image: /local/floorplan/examples/test_plate/test_plate.svg
33 | `,
34 | simulationFile: 'simulations.yaml',
35 | isCard: true,
36 | },
37 | 'examples',
38 | true,
39 | () => {}
40 | );
41 |
42 | // Use the utility function to get the floorplan element
43 | const floorplanElementInstance = await getFloorplanElement();
44 | expect(floorplanElementInstance).toBeInstanceOf(FloorplanElement);
45 |
46 | // Get the svg
47 | const svg = await getFloorplanSvg();
48 | expect(svg).toBeInstanceOf(SVGElement);
49 |
50 | // Validate that our entity is part of the states
51 | expect(
52 | floorplanElementInstance?.hass?.states?.[simulatedEntity]
53 | ).toBeDefined();
54 |
55 | // Now we expect that the text has changed
56 | await retry(
57 | async () => {
58 | const targetEl = svg.querySelector(
59 | `#${targetSvgElementId}`
60 | ) as SVGElement;
61 |
62 | // Expect the innerHTML to match the text
63 | expect(targetEl.innerHTML).toMatch('Click to hide radar');
64 | },
65 | 10,
66 | 700 // This should not match the emulator time sequence
67 | );
68 | });
69 |
70 | it('Will not initiate ha-floorplan if no image', async () => {
71 | // Spy on console.error and save the logs
72 | const consoleErrorSpy = jest
73 | .spyOn(console, 'error')
74 | .mockImplementation((message, ...optionalParams) => {
75 | // Save the logs to an array for later assertions
76 | capturedLogs.push({ message, optionalParams });
77 | });
78 |
79 | // Array to store captured logs
80 | const capturedLogs: { message: any; optionalParams: any[] }[] = [];
81 |
82 | createFloorplanExampleElement(
83 | {
84 | name: 'TestPlate',
85 | dir: 'test_plate',
86 | configYaml: `title: TestPlate
87 | config:
88 | stylesheet: /local/floorplan/examples/test_plate/test_plate.css
89 | `,
90 | simulationFile: 'simulations.yaml',
91 | isCard: true,
92 | },
93 | 'examples',
94 | true,
95 | () => {}
96 | );
97 |
98 | // Use the utility function to get the floorplan element
99 | const floorplanElementInstance = await getFloorplanElement();
100 | expect(floorplanElementInstance).toBeInstanceOf(FloorplanElement);
101 |
102 | // Get the svg
103 | await expect(async () => {
104 | await getFloorplanSvg();
105 | }).rejects.toThrow('SVG element not found inside floorplan-element');
106 |
107 | return await retry(
108 | async () => {
109 | // Check if console.log was called
110 | expect(consoleErrorSpy).toHaveBeenCalled();
111 |
112 | // Restore the original console.log implementation
113 | consoleErrorSpy.mockRestore();
114 |
115 | // Expect captured logs to contain a "no image provided" message
116 | expect(
117 | capturedLogs.some((log) => log.message.includes('No image provided'))
118 | ).toBe(true);
119 | },
120 | 10,
121 | 700
122 | );
123 | });
124 |
125 | it.todo('Test image.sizes');
126 | it.todo('Test image_mobile');
127 | it.todo('Test log_level');
128 | it.todo('Test console_log_level');
129 | it.todo('Test defaults');
130 | });
--------------------------------------------------------------------------------
/tests/jest/tests/floorplan-events.test.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import '../../../src/components/floorplan-examples/floorplan-examples';
3 | import { FloorplanElement } from '../../../src/components/floorplan/floorplan-element';
4 | import {
5 | createFloorplanExampleElement,
6 | getFloorplanElement,
7 | getFloorplanSvg,
8 | } from '../jest-floorplan-utils';
9 | import { retry } from '../jest-common-utils';
10 | import { dispatchFloorplanActionCallEvent } from '../../../src/components/floorplan/lib/events';
11 |
12 | describe('Events', () => {
13 | beforeEach(() => {
14 | });
15 |
16 | afterEach(() => {
17 | // Remove the floorplan-example element from the DOM
18 | const element = document.querySelector('floorplan-example');
19 | if (element) element.remove();
20 | });
21 |
22 | it('Trigger Service Call', async () => {
23 | const simulatedEntity = 'sensor.temperature_living_area';
24 | const targetSvgElementId = 'radar-toggle-btn-text';
25 | const textToSet = "Hello there";
26 |
27 | createFloorplanExampleElement(
28 | {
29 | name: 'TestPlate',
30 | dir: 'test_plate',
31 | configYaml: `title: TestPlate
32 | config:
33 | image: /local/floorplan/examples/test_plate/test_plate.svg
34 | `,
35 | simulationFile: 'simulations.yaml',
36 | isCard: true,
37 | },
38 | 'examples',
39 | true,
40 | () => {}
41 | );
42 |
43 | // Use the utility function to get the floorplan element
44 | const floorplanElementInstance = await getFloorplanElement();
45 | expect(floorplanElementInstance).toBeInstanceOf(FloorplanElement);
46 |
47 | // Get the svg
48 | const svg = await getFloorplanSvg();
49 | expect(svg).toBeInstanceOf(SVGElement);
50 |
51 | // Validate that our entity is part of the states
52 | expect(floorplanElementInstance?.hass?.states?.[simulatedEntity]).toBeDefined();
53 |
54 | // Now we expect that the text has changed
55 | await retry(
56 | async () => {
57 | const element = svg.querySelector(
58 | `#${targetSvgElementId}`
59 | ) as SVGElement;
60 |
61 | expect(element).toBeInstanceOf(SVGElement);
62 |
63 | // Try and fire custom element
64 | dispatchFloorplanActionCallEvent(element, {
65 | actionConfig: [
66 | {
67 | action: 'call-service',
68 | service: 'floorplan.text_set',
69 | service_data: {
70 | element: targetSvgElementId,
71 | text: textToSet,
72 | },
73 | },
74 | ],
75 | entityId: simulatedEntity,
76 | /* text_set can handle a situation without svgElementInfo, but it's added for good measure */
77 | svgElementInfo:
78 | floorplanElementInstance.entityInfos?.[simulatedEntity]?.ruleInfos[0]
79 | ?.svgElementInfos[targetSvgElementId],
80 | ruleInfo:
81 | floorplanElementInstance.entityInfos?.[simulatedEntity]?.ruleInfos,
82 | });
83 |
84 | const targetEl = svg.querySelector(
85 | `#${targetSvgElementId}`
86 | ) as SVGElement;
87 |
88 | // Expect the innerHTML to match the text
89 | expect(targetEl.innerHTML).toMatch(textToSet);
90 | },
91 | 10,
92 | 700 // This should not match the emulator time sequence
93 | );
94 | });
95 | });
--------------------------------------------------------------------------------
/tests/jest/tests/floorplan-services-text_set.test.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import '../../../src/components/floorplan-examples/floorplan-examples';
3 | import { FloorplanElement } from '../../../src/components/floorplan/floorplan-element';
4 | import {
5 | createFloorplanExampleElement,
6 | getFloorplanElement,
7 | getFloorplanSvg,
8 | } from '../jest-floorplan-utils';
9 | import {SVGElementWithStyle} from '../../types/svg';
10 | import { retry } from '../jest-common-utils';
11 |
12 | describe('Services: text_set', () => {
13 | beforeEach(() => {
14 | });
15 |
16 | afterEach(() => {
17 | // Remove the floorplan-example element from the DOM
18 | const element = document.querySelector('floorplan-example');
19 | if (element) element.remove();
20 | });
21 |
22 | it('will render empty string (not null)', async () => {
23 | const simulatedEntity = 'sensor.empty_text';
24 | const targetSvgElementId = 'entity-2-state';
25 | const textToEqual = '';
26 | const textNotAllowed = [0, '0'];
27 |
28 | createFloorplanExampleElement(
29 | {
30 | name: 'TestPlate',
31 | dir: 'test_plate',
32 | configYaml: `title: TestPlate
33 | config:
34 | image: /local/floorplan/examples/test_plate/test_plate.svg
35 | stylesheet: /local/floorplan/examples/test_plate/test_plate.css
36 | rules:
37 | - entity: ${simulatedEntity}
38 | element: ${targetSvgElementId}
39 | state_action:
40 | action: call-service
41 | service: floorplan.text_set
42 | service_data: '\${entity.state}' # Adding anything to the state text, will not generate the right result
43 | `,
44 | simulationFile: 'simulations.yaml',
45 | isCard: true,
46 | },
47 | 'examples',
48 | true,
49 | () => {}
50 | );
51 |
52 | // Use the utility function to get the floorplan element
53 | const floorplanElementInstance = await getFloorplanElement();
54 | expect(floorplanElementInstance).toBeInstanceOf(FloorplanElement);
55 |
56 | // Get the svg
57 | const svg = await getFloorplanSvg();
58 | expect(svg).toBeInstanceOf(SVGElement);
59 |
60 | // Validate that our entity is part of the states
61 | expect(floorplanElementInstance?.hass?.states?.[simulatedEntity]).toBeDefined();
62 |
63 | await retry(
64 | async () => {
65 | const targetEl = svg.querySelector(
66 | `#${targetSvgElementId}`
67 | ) as SVGElementWithStyle;
68 |
69 | expect(targetEl.id).toEqual(targetSvgElementId);
70 |
71 | // Expect the text content to match the expected value
72 | const textContent = targetEl.textContent;
73 |
74 | // test_set previously had a bug, where it rendered 0, if state was a "empty string" ("") and added like: service_data: '${entity.state}'
75 | expect(textContent).toBeDefined();
76 |
77 | // Do not expect 0, as the bug is fixed
78 | expect(textNotAllowed).not.toContain(textContent);
79 | expect(textContent).toEqual(textToEqual);
80 | },
81 | 10,
82 | 700 // This should not match the emulator time sequence
83 | );
84 | });
85 | });
--------------------------------------------------------------------------------
/tests/setup_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Install Playwright using npx
4 | npx playwright install
--------------------------------------------------------------------------------
/tests/types/svg.ts:
--------------------------------------------------------------------------------
1 | export type SVGElementWithStyle = SVGElement & {
2 | style: CSSStyleDeclaration;
3 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "strict": true,
6 | "noImplicitReturns": true,
7 | "noImplicitAny": true,
8 | "module": "ESNext",
9 | "moduleResolution": "Node",
10 | "target": "ES2020",
11 | "allowJs": true,
12 | "experimentalDecorators": true,
13 | "resolveJsonModule": true,
14 | "allowSyntheticDefaultImports": true,
15 | "esModuleInterop": true,
16 | "isolatedModules": true, // Required for ESM
17 | "types": ["jest"], // Include Jest types
18 | "typeRoots": [
19 | "./types",
20 | "./node_modules/@types"
21 | ]
22 | },
23 | "include": [
24 | "./src/**/*",
25 | "./tests/**/*"
26 | ]
27 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { fileURLToPath } from 'url';
3 | import TerserPlugin from 'terser-webpack-plugin';
4 | import CopyPlugin from 'copy-webpack-plugin';
5 | import webpack from 'webpack'; // Import the default export
6 | import packageInfo from './package.json' with { type: 'json' };
7 |
8 | const { DefinePlugin } = webpack; // Destructure DefinePlugin from the default export
9 |
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = path.dirname(__filename);
12 |
13 | export default (env) => {
14 | const isProduction = env.production;
15 |
16 | const plugins = isProduction
17 | ? [
18 | new CopyPlugin({
19 | patterns: [
20 | {
21 | from: path.resolve(__dirname, 'dist', 'floorplan-examples.js'),
22 | to: path.resolve(__dirname, 'docs', '_docs', 'floorplan'),
23 | force: true,
24 | noErrorOnMissing: true, // Prevent errors if the file doesn't exist yet
25 | },
26 | ],
27 | options: {
28 | concurrency: 100, // Workaround to ensure copying is done after the build
29 | },
30 | }),
31 | ]
32 | : [];
33 |
34 | return {
35 | mode: isProduction ? 'production' : 'development',
36 | entry: {
37 | floorplan: './src/index.ts',
38 | 'floorplan-examples': './src/components/floorplan-examples/floorplan-examples.ts',
39 | },
40 | devtool: isProduction ? undefined : 'inline-source-map',
41 | module: {
42 | rules: [
43 | {
44 | test: /\.tsx?$/,
45 | use: 'ts-loader',
46 | exclude: /node_modules/,
47 | },
48 | ],
49 | },
50 | resolve: {
51 | extensions: ['.tsx', '.ts', '.js'],
52 | mainFields: ['browser', 'module', 'main'], // Ensure ES Modules are prioritized
53 | },
54 | plugins: [
55 | new DefinePlugin({
56 | NAME: JSON.stringify(packageInfo.name),
57 | DESCRIPTION: JSON.stringify(packageInfo.description),
58 | VERSION: JSON.stringify(packageInfo.version),
59 | }),
60 | ...plugins,
61 | ],
62 | output: {
63 | filename: '[name].js',
64 | path: path.resolve(__dirname, isProduction ? 'dist' : 'dist_local'),
65 | clean: true,
66 | libraryTarget: 'module', // Use ES Module output
67 | },
68 | experiments: {
69 | outputModule: true, // Enable ES Module output
70 | },
71 | optimization: {
72 | minimize: true,
73 | minimizer: [
74 | new TerserPlugin({
75 | extractComments: false,
76 | }),
77 | ],
78 | },
79 | performance: {
80 | hints: false,
81 | maxEntrypointSize: 512000,
82 | maxAssetSize: 512000,
83 | },
84 | devServer: {
85 | static: {
86 | directory: path.join(__dirname, 'docs/_docs/floorplan'),
87 | },
88 | compress: true,
89 | },
90 | };
91 | };
--------------------------------------------------------------------------------