├── .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 | GitHub Build & Test on Master 15 | 16 | 17 | GitHub Issues 19 | 20 | 21 | GitHub Pull Requests 23 | 24 | 25 | GitHub Stars 27 | 28 | 29 | Current version 31 | 32 | 33 | HACS Default repository - Go to Quick Start in Docs 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 | SVG Preview 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 | 101 | 102 |
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 |
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 | 15 | 16 | 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`

${this.config?.title}

` 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 | }; --------------------------------------------------------------------------------