├── .env ├── .github ├── FUNDING.yml ├── workflows │ ├── build.yml │ └── release.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── .devcontainer ├── configuration.yaml ├── ui-lovelace.yaml └── devcontainer.json ├── docs ├── Screenshot_1.png ├── Screenshot_2.png ├── Screenshot_3.png ├── Screenshot_4.png ├── Screenshot_5.png ├── Screenshot_6.png ├── Screenshot_7.png ├── Screenshot_8.png ├── Screenshot_9.png ├── Screenshot_10.png ├── Screenshot_11.png └── Screenshot_12.png ├── hacs.json ├── .prettierrc.js ├── src ├── hacs.json ├── localize │ ├── languages │ │ ├── fr.json │ │ └── en.json │ └── localize.ts ├── humidifier.ts ├── types.ts ├── midea-humidifier-card-editor.ts └── midea-humidifier-card.ts ├── .vscode ├── tasks.json └── extensions.json ├── tsconfig.json ├── .eslintrc.js ├── rollup.config.dev.js ├── LICENSE.md ├── rollup.config.js ├── package.json ├── README.md └── dist └── midea-humidifier-card.js /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["sicknesz"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.rpt2_cache/ 3 | package-lock.json 4 | *error*.log -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | lovelace: 3 | mode: yaml 4 | demo: -------------------------------------------------------------------------------- /docs/Screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_1.png -------------------------------------------------------------------------------- /docs/Screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_2.png -------------------------------------------------------------------------------- /docs/Screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_3.png -------------------------------------------------------------------------------- /docs/Screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_4.png -------------------------------------------------------------------------------- /docs/Screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_5.png -------------------------------------------------------------------------------- /docs/Screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_6.png -------------------------------------------------------------------------------- /docs/Screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_7.png -------------------------------------------------------------------------------- /docs/Screenshot_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_8.png -------------------------------------------------------------------------------- /docs/Screenshot_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_9.png -------------------------------------------------------------------------------- /docs/Screenshot_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_10.png -------------------------------------------------------------------------------- /docs/Screenshot_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_11.png -------------------------------------------------------------------------------- /docs/Screenshot_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicknesz/midea-humidifier-card/HEAD/docs/Screenshot_12.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Midea Humidifier Card", 3 | "render_readme": true, 4 | "filename": "midea-humidifier-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; -------------------------------------------------------------------------------- /src/hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Midea (De)humidifier Card", 3 | "content_in_root": false, 4 | "filename": "midea-humidifier-card.js", 5 | "render_readme": true 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "problemMatcher": [], 8 | "label": "npm: start", 9 | "detail": "rollup -c --watch" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.devcontainer/ui-lovelace.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - url: http://127.0.0.1:5000/boilerplate-card.js 3 | type: module 4 | views: 5 | - cards: 6 | - type: custom:boilerplate-card 7 | name: Boilerplate Card Development 8 | entity: sun.sun 9 | test_gui: true 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "github.vscode-pull-request-github", 4 | "eamodio.gitlens", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "bierner.lit-html", 8 | "runem.lit-plugin", 9 | "auchenberg.vscode-browser-preview", 10 | "davidanson.vscode-markdownlint", 11 | "redhat.vscode-yaml" 12 | ] 13 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Test build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Build 18 | run: | 19 | npm install 20 | npm run build 21 | -------------------------------------------------------------------------------- /src/localize/languages/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "version": "Version", 4 | "invalid_configuration": "Configuration invalide", 5 | "tank_is_full": "Attention : Le réservoir est plein", 6 | "change_airfilter": "Attention : Changer le filtre à air", 7 | "defrosting": "Attention : Dégivrage en cours...", 8 | "tank": "Réservoir", 9 | "filter": "Filtre", 10 | "defrost": "Dégivrage" 11 | } 12 | } -------------------------------------------------------------------------------- /src/localize/languages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "version": "Version", 4 | "invalid_configuration": "Invalid configuration", 5 | "tank_is_full": "Caution : Water tank is full, please empty it", 6 | "change_airfilter": "Caution : Please change the air filter", 7 | "defrosting": "Caution : Please wait, currently defrosting...", 8 | "tank": "Tank", 9 | "filter": "Filter", 10 | "defrost": "Defrost" 11 | } 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2017", 8 | "dom", 9 | "dom.iterable" 10 | ], 11 | "noEmit": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "experimentalDecorators": true 20 | } 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module" // Allows for the use of imports 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | ], 10 | rules: { 11 | "no-explicit-any": 0, 12 | "no-non-null-assertion": 0, 13 | "explicit-module-boundary-types" : 0, 14 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 15 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 16 | } 17 | }; -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import babel from "rollup-plugin-babel"; 4 | import serve from "rollup-plugin-serve"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import json from '@rollup/plugin-json'; 7 | 8 | export default { 9 | input: ["src/midea-humidifier-card.ts"], 10 | output: { 11 | dir: "./dist", 12 | format: "es", 13 | }, 14 | plugins: [ 15 | resolve(), 16 | typescript(), 17 | json(), 18 | babel({ 19 | exclude: "node_modules/**", 20 | }), 21 | terser(), 22 | serve({ 23 | contentBase: "./dist", 24 | host: "0.0.0.0", 25 | port: 5000, 26 | allowCrossOrigin: true, 27 | headers: { 28 | "Access-Control-Allow-Origin": "*", 29 | }, 30 | }), 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Prepare release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | # Build 15 | - name: Build the file 16 | run: | 17 | cd /home/runner/work/boilerplate-card/boilerplate-card 18 | npm install 19 | npm run build 20 | 21 | # Upload build file to the releas as an asset. 22 | - name: Upload zip to release 23 | uses: svenstaro/upload-release-action@v1-release 24 | 25 | with: 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} 27 | file: /home/runner/work/boilerplate-card/boilerplate-card/dist/boilerplate-card.js 28 | asset_name: boilerplate-card.js 29 | tag: ${{ github.ref }} 30 | overwrite: true -------------------------------------------------------------------------------- /src/localize/localize.ts: -------------------------------------------------------------------------------- 1 | import * as en from './languages/en.json'; 2 | import * as fr from './languages/fr.json'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const languages: any = { 6 | en: en, 7 | fr: fr, 8 | }; 9 | 10 | export function localize(string: string, search = '', replace = ''): string { 11 | const lang = (localStorage.getItem('selectedLanguage') || 'en').replace(/['"]+/g, '').replace('-', '_'); 12 | 13 | let translated: string; 14 | 15 | try { 16 | translated = string.split('.').reduce((o, i) => o[i], languages[lang]); 17 | } catch (e) { 18 | translated = string.split('.').reduce((o, i) => o[i], languages['en']); 19 | } 20 | 21 | if (translated === undefined) translated = string.split('.').reduce((o, i) => o[i], languages['en']); 22 | 23 | if (search !== '' && replace !== '') { 24 | translated = translated.replace(search, replace); 25 | } 26 | return translated; 27 | } 28 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Boilerplate Card Development", 4 | "image": "ludeeus/container:monster", 5 | "context": "..", 6 | "appPort": ["5000:5000", "9123:8123"], 7 | "postCreateCommand": "npm install", 8 | "runArgs": [ 9 | "-v", 10 | "${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" // This is added so you can push from inside the container 11 | ], 12 | "extensions": [ 13 | "github.vscode-pull-request-github", 14 | "eamodio.gitlens", 15 | "dbaeumer.vscode-eslint", 16 | "esbenp.prettier-vscode", 17 | "bierner.lit-html", 18 | "runem.lit-plugin", 19 | "auchenberg.vscode-browser-preview", 20 | "davidanson.vscode-markdownlint", 21 | "redhat.vscode-yaml" 22 | ], 23 | "settings": { 24 | "files.eol": "\n", 25 | "editor.tabSize": 4, 26 | "terminal.integrated.shell.linux": "/bin/bash", 27 | "editor.formatOnPaste": false, 28 | "editor.formatOnSave": true, 29 | "editor.formatOnType": true, 30 | "files.trimTrailingWhitespace": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Custom cards for Home Assistant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import babel from 'rollup-plugin-babel'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import serve from 'rollup-plugin-serve'; 7 | import json from '@rollup/plugin-json'; 8 | import dotenv from 'dotenv'; 9 | 10 | const dev = process.env.ROLLUP_WATCH; 11 | dotenv.config(); 12 | 13 | const serveopts = { 14 | contentBase: ['./dist'], 15 | host: '0.0.0.0', 16 | port: 5000, 17 | allowCrossOrigin: true, 18 | headers: { 19 | 'Access-Control-Allow-Origin': '*', 20 | }, 21 | }; 22 | 23 | const plugins = [ 24 | nodeResolve({}), 25 | commonjs(), 26 | typescript(), 27 | json(), 28 | babel({ 29 | exclude: 'node_modules/**', 30 | }), 31 | dev && serve(serveopts), 32 | !dev && terser({ format: { comments: false } }), 33 | ]; 34 | 35 | export default [ 36 | { 37 | input: 'src/midea-humidifier-card.ts', 38 | output: { 39 | dir: 'dist', 40 | format: 'es', 41 | inlineDynamicImports: true, 42 | }, 43 | plugins: [...plugins], 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | **Checklist:** 16 | 17 | - [ ] I updated to the latest version available 18 | - [ ] I cleared the cache of my browser 19 | 20 | **Release with the issue:** 21 | 22 | **Last working release (if known):** 23 | 24 | **Browser and Operating System:** 25 | 26 | 29 | 30 | **Description of problem:** 31 | 32 | 35 | 36 | **Javascript errors shown in the web inspector (if applicable):** 37 | 38 | ``` 39 | 40 | ``` 41 | 42 | **Additional information:** 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midea-dehumidifier-card", 3 | "version": "1.4.1", 4 | "description": "Lovelace midea-dehumidifier-card", 5 | "keywords": [ 6 | "home-assistant", 7 | "homeassistant", 8 | "hass", 9 | "automation", 10 | "lovelace", 11 | "custom-cards" 12 | ], 13 | "module": "midea-dehumidifier-card.js", 14 | "repository": "git@github.com:sicknesz/midea-dehumidifier-card.git", 15 | "author": "Sylain Roccaserra (sicknesz) ", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@mdi/js": "^6.5.95", 19 | "@thomasloven/round-slider": "^0.5.4", 20 | "custom-card-helpers": "^1.7.2", 21 | "home-assistant-js-websocket": "^5.11.1", 22 | "lit": "^2.0.0-rc.2" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.15.0", 26 | "@babel/plugin-proposal-class-properties": "^7.14.5", 27 | "@babel/plugin-proposal-decorators": "^7.14.5", 28 | "@rollup/plugin-json": "^4.1.0", 29 | "@typescript-eslint/eslint-plugin": "^4.33.0", 30 | "@typescript-eslint/parser": "^4.33.0", 31 | "dotenv": "^10.0.0", 32 | "eslint": "^7.32.0", 33 | "eslint-config-airbnb-base": "^14.2.1", 34 | "eslint-config-prettier": "^8.3.0", 35 | "eslint-plugin-import": "^2.24.0", 36 | "eslint-plugin-prettier": "^4.0.0", 37 | "prettier": "^2.4.1", 38 | "rollup": "^2.58.0", 39 | "rollup-plugin-babel": "^4.4.0", 40 | "rollup-plugin-commonjs": "^10.1.0", 41 | "rollup-plugin-node-resolve": "^5.2.0", 42 | "rollup-plugin-serve": "^1.1.0", 43 | "rollup-plugin-terser": "^7.0.2", 44 | "rollup-plugin-typescript2": "^0.30.0", 45 | "rollup-plugin-uglify": "^6.0.4", 46 | "typescript": "^4.4.3" 47 | }, 48 | "scripts": { 49 | "start": "rollup -c rollup.config.dev.js --watch", 50 | "build": "npm run lint && npm run rollup", 51 | "lint": "eslint src/*.ts", 52 | "rollup": "rollup -c" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/humidifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | 6 | export type PowerStatus = 7 | | 'on' 8 | | 'off' 9 | 10 | export type HumidifierMode = 11 | | "off" 12 | | "set" 13 | | "smart" 14 | | "continuous" 15 | | "dry"; 16 | 17 | export type HumidifierFanMode = 18 | | "low" 19 | | "silent" 20 | | "medium" 21 | | "turbo" 22 | | "high" 23 | 24 | export type HumidifierProblems = 25 | | "TankIsFull" 26 | | "ReplaceAirFilter" 27 | | "Defrosting" 28 | 29 | 30 | export type BinaryEntity = HassEntityBase& { 31 | attributes: HassEntityAttributeBase & { 32 | friendly_name?: string; 33 | }; 34 | }; 35 | 36 | export type HumidifierEntity = HassEntityBase & { 37 | attributes: HassEntityAttributeBase & { 38 | current_humidity?: number; 39 | humidity?: number; 40 | min_humidity?: number; 41 | max_humidity?: number; 42 | mode: HumidifierMode; 43 | available_modes: HumidifierMode[]; 44 | fan_speed_mode?: string[]; 45 | fan_speed?: number; 46 | tank_show?: boolean; 47 | }; 48 | }; 49 | 50 | export type HumidifierFanEntity = HassEntityBase & { 51 | attributes: HassEntityAttributeBase & { 52 | preset_modes: HumidifierFanMode[]; 53 | preset_mode: HumidifierFanMode; 54 | }; 55 | }; 56 | 57 | export const HUMIDIFIER_SUPPORT_MODES = 1; 58 | 59 | export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier"; 60 | export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier"; 61 | 62 | const humidifierFanModeOrdering: { [key in HumidifierFanMode]: number } = { 63 | low: 0, 64 | silent: 0, 65 | medium: 1, 66 | turbo: 2, 67 | high: 2 68 | }; 69 | 70 | const humidifierModeOrdering: { [key in HumidifierMode]: number } = { 71 | off: 0, 72 | set: 1, 73 | smart: 2, 74 | continuous: 3, 75 | dry: 4, 76 | }; 77 | 78 | export const compareClimateHumidifierModes = (mode1: HumidifierMode, mode2: HumidifierMode) => 79 | humidifierModeOrdering[mode1] - humidifierModeOrdering[mode2]; 80 | 81 | export const compareClimateFanHumidifierModes = (fanMode1: HumidifierFanMode, fanMode2: HumidifierFanMode) => 82 | humidifierFanModeOrdering[fanMode1] - humidifierFanModeOrdering[fanMode2]; 83 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { HomeAssistant, ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers'; 3 | 4 | declare global { 5 | interface HTMLElementTagNameMap { 6 | 'midea-inventor-humidifier-card-editor': LovelaceCardEditor; 7 | 'hui-error-card': LovelaceCard; 8 | } 9 | } 10 | 11 | // TODO Add your configuration elements here for type-checking 12 | export interface HumidifierCardConfig extends LovelaceCardConfig { 13 | entity: string; 14 | fan_entity: string; 15 | tank_entity: string; 16 | defrost_entity: string; 17 | filter_entity: string; 18 | humidity_entity: string; 19 | temperature_entity: string; 20 | ion_entity: string; 21 | show_ion_toggle: boolean; 22 | swap_target_and_current_humidity: boolean; 23 | theme?: string; 24 | name?: string; 25 | } 26 | 27 | export interface ProblemConfig { 28 | tank: boolean; 29 | defrost: boolean; 30 | filter: boolean; 31 | } 32 | 33 | export const TIMESTAMP_RENDERING_FORMATS = [ 34 | "relative", 35 | "total", 36 | "date", 37 | "time", 38 | "datetime", 39 | ] as const; 40 | 41 | export type TimestampRenderingFormat = 42 | typeof TIMESTAMP_RENDERING_FORMATS[number]; 43 | 44 | export interface EntityConfig { 45 | entity: string; 46 | type?: string; 47 | name?: string; 48 | icon?: string; 49 | image?: string; 50 | } 51 | export interface ActionRowConfig extends EntityConfig { 52 | action_name?: string; 53 | } 54 | export interface EntityFilterEntityConfig extends EntityConfig { 55 | state_filter?: Array<{ key: string } | string>; 56 | } 57 | export interface DividerConfig { 58 | type: "divider"; 59 | style?: Record; 60 | } 61 | export interface SectionConfig { 62 | type: "section"; 63 | label: string; 64 | } 65 | export interface WeblinkConfig { 66 | type: "weblink"; 67 | name?: string; 68 | icon?: string; 69 | url: string; 70 | new_tab?: boolean; 71 | download?: boolean; 72 | } 73 | export interface TextConfig { 74 | type: "text"; 75 | name: string; 76 | icon?: string; 77 | text: string; 78 | } 79 | export interface CallServiceConfig extends EntityConfig { 80 | type: "call-service"; 81 | service: string; 82 | service_data?: Record; 83 | action_name?: string; 84 | } 85 | export interface ButtonRowConfig extends EntityConfig { 86 | type: "button"; 87 | action_name?: string; 88 | tap_action?: ActionConfig; 89 | hold_action?: ActionConfig; 90 | double_tap_action?: ActionConfig; 91 | } 92 | export interface CastConfig { 93 | type: "cast"; 94 | icon?: string; 95 | name?: string; 96 | view: string | number; 97 | dashboard?: string; 98 | // Hide the row if either unsupported browser or no API available. 99 | hide_if_unavailable?: boolean; 100 | } 101 | export interface ButtonsRowConfig { 102 | type: "buttons"; 103 | entities: Array; 104 | } 105 | export type LovelaceRowConfig = 106 | | EntityConfig 107 | | DividerConfig 108 | | SectionConfig 109 | | WeblinkConfig 110 | | CallServiceConfig 111 | | CastConfig 112 | | ButtonRowConfig 113 | | ButtonsRowConfig 114 | | ConditionalRowConfig 115 | | AttributeRowConfig 116 | | TextConfig; 117 | 118 | export interface LovelaceRow extends HTMLElement { 119 | hass?: HomeAssistant; 120 | editMode?: boolean; 121 | setConfig(config: LovelaceRowConfig); 122 | } 123 | 124 | 125 | export interface Condition { 126 | entity: string; 127 | state?: string; 128 | state_not?: string; 129 | } 130 | 131 | export interface ConditionalRowConfig extends EntityConfig { 132 | row: EntityConfig; 133 | conditions: Condition[]; 134 | } 135 | 136 | export interface AttributeRowConfig extends EntityConfig { 137 | attribute: string; 138 | prefix?: string; 139 | suffix?: string; 140 | format?: TimestampRenderingFormat; 141 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Midea Humidifier Card 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) 4 | [![maintenance-shield](https://img.shields.io/maintenance/yes/2022.svg?style=for-the-badge)]() 5 | 6 | A companion card to the Midea Humidifier Lan integration that added tons of options comparing to previous integrations, so much that the classic 7 | humidifier card was kinda lacking alot, so i made this card specifically for that (Midea/Inventor EVA Pro (De)Humifidier Appliances) 8 | 9 | ## Changelogs 10 | 11 | *v1.0.7* - Latest current version - Fixed fan mode name change in homeassistant-midea-air-appliances-lan v0.6.0 12 | 13 | *v1.0.6* - Updated readme.md 14 | 15 | *v1.0.5* - Changed the ion icon color to match all others (current mode color) 16 | 17 | *v1.0.3* - Added missing ion_entity from card editor and autoconfig 18 | 19 | *v1.0.2* - Added card editor, single file output and handling of all warning (defrost, tank_full, filter) 20 | 21 | *v1.0.1* - Added power buttons, all fan preset modes and ion toggle 22 | 23 | # Installation 24 | 25 | ## Install 26 | 27 | ### HACS Install 28 | 29 | 1. Go to the section frontend. 30 | 2. Click on the 3 dots in the top right corner. 31 | 3. Select "Custom repositories" 32 | 4. Add the [URL](https://github.com/sicknesz/midea-humidifier-card/) to the repository. 33 | 5. Select the correct category (lovelace) 34 | 6. Click the "ADD" button. 35 | 7. Install 36 | 37 | ### Manual install 38 | 39 | 1. Download and copy `midea-humidifier-card.js` from the [my repo](https://raw.githubusercontent.com/sicknesz/midea-inventor-card/master/dist/midea-humidifier-card.js) into your `config/www` directory. 40 | 41 | 2. Add the resource reference as decribed below. 42 | 43 | ### CLI install 44 | 45 | 1. Move into your `config/www` directory. 46 | 47 | 2. Grab `midea-humidifier-card.js`: 48 | 49 | ``` 50 | wget https://raw.githubusercontent.com/sicknesz/midea-inventor-card/master/dist/midea-humidifier-card.js 51 | ``` 52 | 53 | 3. Add the resource reference as decribed below. 54 | 55 | ### Add resource reference 56 | 57 | If you configure Lovelace via YAML, add a reference to `midea-humidifier-card.js` inside your `configuration.yaml`: 58 | 59 | ```yaml 60 | resources: 61 | - url: /hacsfiles/midea-humidifier-card.js?v=1.0.7 62 | type: module 63 | ``` 64 | 65 | Else, if you prefer the graphical editor, use the menu to add the resource: 66 | 67 | 1. Make sure, advanced mode is enabled in your user profile (click on your user name to get there) 68 | 2. Navigate to Configuration -> Lovelace Dashboards -> Resources Tab. Hit orange (+) icon 69 | 3. Enter URL `/hacsfiles/midea-humidifier-card.js` and select type "JavaScript Module". 70 | 4. Restart Home Assistant. 71 | 72 | ## Usage 73 | 74 | Click to add an element to your lovelace view, select the midea-humidifier-card, it will open the visual card editor 75 | but everything should be autoconfigured properly unless you have more than one humidifier, in that case just manually click and set all fields in the editor. 76 | 77 | ### Troubleshooting 78 | 79 | PS : If you do not see the filter and defrost entities, it's because they're disabled by default on the integration, to enable them 80 | navigate to /config/integrations choose the midea humidifier lan integration, click on "11 entities", 81 | search for defrost and enable the defrost and filter entities (if they're not visible look at the entity filter and disable it) 82 | then you'll be able to add them to the card. 83 | 84 | ``` 85 | type: 'custom:midea-humidifier-card' 86 | entity: humidifier.dehumidifier_ 87 | humidity_entity: sensor.dehumidifier__humidity 88 | temperature_entity: sensor.dehumidifier__temperature 89 | fan_entity: fan.dehumidifier__fan 90 | filter_entity: binary_sensor.dehumidifier__replace_filter 91 | defrost_entity: binary_sensor.dehumidifier__defrosting 92 | tank_entity: binary_sensor.dehumidifier__tank_full 93 | ion_entity: switch.dehumidifier__ion_mode 94 | show_ion_toggle: true 95 | swap_target_and_current_humidity: true 96 | ``` 97 | 98 | ## Options 99 | 100 | | Name | Type | Requirement | Description | Default | 101 | | -------------------------------- | ------- | ------------ | ------------------------------------------- | -------------------- | 102 | | type | string | **Required** | `custom:midea-humidifier-card` | 103 | | name | string | **Optional** | Card name | `Midea Humidifier` | 104 | | entity | string | **Required** | Humidifier entity ID. | `humidifier.` | 105 | | fan_entity | string | **Required** | Humidifiers fan entity ID. | `fan.` | 106 | | humidity_entity | string | **Required** | Humidifiers humidity sensor entity ID. | `sensor.` | 107 | | temperature_entity | string | **Required** | Humidifiers temperature sensor entity ID. | `sensor.` | 108 | | tank_entity | string | **Optional** | Humidifiers tank binary sensor entity | `binary_sensor.` | 109 | | filter_entity | string | **Optional** | Humidifiers filter binary sensor entity | `binary_sensor.` | 110 | | defrost_entity | string | **Optional** | Humidifiers defrost binary sensor entity | `binary_sensor.` | 111 | | ion_entity | string | **Optional** | Humidifiers ion switch entity | `switch.` | 112 | | show_ion_toggle | boolean | **Optional** | Display the ion switch toggle icon. | true | 113 | | swap_target_and_current_humidity | boolean | **Optional** | Swap current and target humidity display | true | 114 | 115 | ## Screenshots 116 | 117 | ![Screenshot #1]() 118 | ![Screenshot #2]() 119 | ![Screenshot #3]() 120 | ![Screenshot #4]() 121 | ![Screenshot #5]() 122 | ![Screenshot #6]() 123 | ![Screenshot #7]() 124 | ![Screenshot #8]() 125 | ![Screenshot #9]() 126 | ![Screenshot #10]() 127 | ![Screenshot #11]() 128 | ![Screenshot #12]() 129 | 130 | ## Support 131 | 132 | Hey dude! Help me out for a couple of :beers: or a :coffee: ! 133 | 134 | Salut Mec! Tu peu me soutenir en m'offrant quelques :beers: ou un :coffee: ! 135 | 136 | [![coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/sicknesz) 137 | -------------------------------------------------------------------------------- /src/midea-humidifier-card-editor.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { LitElement, html, TemplateResult, css, CSSResultGroup } from 'lit'; 4 | import { HomeAssistant, fireEvent, LovelaceCard, LovelaceCardEditor } from 'custom-card-helpers' 5 | import { localize } from './localize/localize'; 6 | 7 | import { HumidifierCardConfig } from './types'; 8 | 9 | 10 | import { customElement, property, state } from 'lit/decorators'; 11 | 12 | const CARD_NAME = "midea-humidifier-card-editor" 13 | 14 | // const debug = input => JSON.stringify(input, null, 2) 15 | @customElement(CARD_NAME) 16 | export class MideaHumidifierCardEditor extends LitElement implements LovelaceCardEditor { 17 | @property({ attribute: false }) public hass?: HomeAssistant; 18 | @state() private _config?: HumidifierCardConfig; 19 | @state() private _helpers?: any; 20 | private _initialized = false; 21 | 22 | public setConfig(config: HumidifierCardConfig): void { 23 | // console.warn(`[${CARD_NAME}::setConfig]: config : ${debug(config)}`) 24 | this._config = config; 25 | 26 | this.loadCardHelpers(); 27 | } 28 | 29 | protected shouldUpdate(): boolean { 30 | if (!this._initialized) { 31 | this._initialize(); 32 | } 33 | 34 | return true; 35 | } 36 | 37 | get _name(): string { 38 | return this._config?.name || ''; 39 | } 40 | 41 | get _entity(): string { 42 | return this._config?.entity || ''; 43 | } 44 | 45 | get _fan_entity(): string { 46 | return this._config?.fan_entity || ''; 47 | } 48 | 49 | get _humidity_entity(): string { 50 | return this._config?.humidity_entity || ''; 51 | } 52 | 53 | get _temperature_entity(): string { 54 | return this._config?.temperature_entity || ''; 55 | } 56 | 57 | get _ion_entity(): string { 58 | return this._config?.ion_entity || ''; 59 | } 60 | 61 | get _defrost_entity(): string { 62 | return this._config?.defrost_entity || ''; 63 | } 64 | 65 | get _filter_entity(): string { 66 | return this._config?.filter_entity || ''; 67 | } 68 | 69 | get _tank_entity(): string { 70 | return this._config?.tank_entity || ''; 71 | } 72 | 73 | get _show_ion_toggle(): boolean { 74 | return this._config?.show_ion_toggle || false; 75 | } 76 | 77 | get _swap_target_and_current_humidity(): boolean { 78 | return this._config?.swap_target_and_current_humidity || false; 79 | } 80 | 81 | protected render(): TemplateResult | void { 82 | if (!this.hass || !this._helpers) { 83 | return html``; 84 | } 85 | 86 | // The climate more-info has ha-switch and paper-dropdown-menu elements that are lazy loaded unless explicitly done here 87 | this._helpers.importMoreInfoControl('climate'); 88 | 89 | // You can restrict on domain type 90 | const humidifierDomains = ["humidifier"]; 91 | const sensorDomains = ["sensor"]; 92 | const fanDomains = ["fan"]; 93 | const binarySensorDomains = ["binary_sensor"]; 94 | const switchDomains = ["switch"]; 95 | 96 | return html` 97 |
98 | 113 | 128 | 143 | 158 | 169 | 180 | 191 | 202 | 203 | 208 | 209 | 210 | 215 | 216 |
217 | `; 218 | } 219 | 220 | private _initialize(): void { 221 | if (this.hass === undefined) return; 222 | if (this._config === undefined) return; 223 | if (this._helpers === undefined) return; 224 | this._initialized = true; 225 | } 226 | 227 | private async loadCardHelpers(): Promise { 228 | this._helpers = await (window as any).loadCardHelpers(); 229 | } 230 | 231 | 232 | /* -- imported -- 233 | private _valueChanged(ev: EntitiesEditorEvent): void { 234 | if (!this._config || !this.hass) { 235 | return; 236 | } 237 | const target = ev.target! as EditorTarget; 238 | 239 | if (this[`_${target.configValue}`] === target.value) { 240 | return; 241 | } 242 | if (target.configValue) { 243 | if (target.value === "") { 244 | this._config = { ...this._config }; 245 | delete this._config[target.configValue!]; 246 | } else { 247 | this._config = { ...this._config, [target.configValue!]: target.value }; 248 | } 249 | } 250 | fireEvent(this, "config-changed", { config: this._config }); 251 | } 252 | */ 253 | 254 | private _valueChanged(ev): void { 255 | if (!this._config || !this.hass) { 256 | return; 257 | } 258 | const target = ev.target; 259 | if (this[`_${target.configValue}`] === target.value) { 260 | return; 261 | } 262 | if (target.configValue) { 263 | if (target.value === '') { 264 | const tmpConfig = { ...this._config }; 265 | delete tmpConfig[target.configValue]; 266 | this._config = tmpConfig; 267 | } else { 268 | this._config = { 269 | ...this._config, 270 | [target.configValue]: target.checked !== undefined ? target.checked : target.value, 271 | }; 272 | } 273 | } 274 | // console.log(`[${CARD_NAME}::this._valueChanged]: new config : ${debug(this._config)}`) 275 | fireEvent(this, 'config-changed', { config: this._config }); 276 | } 277 | 278 | static get styles(): CSSResultGroup { 279 | return css` 280 | .option { 281 | padding: 4px 0px; 282 | cursor: pointer; 283 | } 284 | .row { 285 | display: flex; 286 | margin-bottom: -14px; 287 | pointer-events: none; 288 | } 289 | .title { 290 | padding-left: 16px; 291 | margin-top: -6px; 292 | pointer-events: none; 293 | } 294 | .secondary { 295 | padding-left: 40px; 296 | color: var(--secondary-text-color); 297 | pointer-events: none; 298 | } 299 | .values { 300 | padding-left: 16px; 301 | background: var(--secondary-background-color); 302 | display: grid; 303 | } 304 | ha-formfield { 305 | padding-bottom: 8px; 306 | } 307 | `; 308 | } 309 | } 310 | 311 | declare global { 312 | interface HTMLElementTagNameMap { 313 | [CARD_NAME]: MideaHumidifierCardEditor; 314 | 'hui-error-card': LovelaceCard; 315 | } 316 | } -------------------------------------------------------------------------------- /src/midea-humidifier-card.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | 5 | // process.traceDeprecation = true; 6 | const CARD_NAME = "midea-humidifier-card"; 7 | const version = "1.0.7"; 8 | 9 | console.info( 10 | `%c ${CARD_NAME} %c ${version}`, 11 | 'color: cyan; background: black; font-weight: bold;', 12 | 'color: darkblue; background: white; font-weight: bold;', 13 | ); 14 | (window as any).customCards = (window as any).customCards || []; 15 | (window as any).customCards.push({ 16 | type: CARD_NAME, 17 | name: 'Midea (De)humidifier Card', 18 | description: 'Midea/Inventor (De)humidifier lovelace UI card', 19 | }); 20 | 21 | 22 | import { 23 | mdiAutorenew, 24 | mdiAirHumidifier, 25 | mdiAirHumidifierOff, 26 | mdiAirPurifier, 27 | mdiHeadLightbulbOutline, 28 | mdiTshirtCrew, 29 | mdiDotsVertical, 30 | mdiFanSpeed1, 31 | mdiFanSpeed2, 32 | mdiFanSpeed3, 33 | mdiPower, 34 | mdiPowerCycle, 35 | mdiSnowflakeMelt 36 | } from "@mdi/js"; 37 | // import "@thomasloven/round-slider"; 38 | import { HassEntity, STATE_NOT_RUNNING } from "home-assistant-js-websocket"; 39 | import { localize } from "./localize/localize"; 40 | import { 41 | css, 42 | CSSResultGroup, 43 | html, 44 | LitElement, 45 | PropertyValues, 46 | svg, 47 | TemplateResult, 48 | } from "lit"; 49 | import { customElement, property, state } from "lit/decorators"; 50 | import { classMap } from "lit/directives/class-map"; 51 | import { 52 | HomeAssistant, 53 | LovelaceCard, 54 | // LovelaceCardEditor, 55 | applyThemesOnElement, 56 | fireEvent, 57 | EntityConfig, 58 | computeRTLDirection, 59 | computeDomain, 60 | } from 'custom-card-helpers'; 61 | 62 | import { 63 | HumidifierEntity, 64 | HumidifierFanEntity, 65 | BinaryEntity, 66 | compareClimateHumidifierModes, 67 | compareClimateFanHumidifierModes, 68 | HumidifierMode, 69 | HumidifierFanMode, 70 | PowerStatus 71 | } from "./humidifier"; 72 | 73 | import { LovelaceRowConfig, HumidifierCardConfig, ProblemConfig } from "./types" 74 | 75 | // Re-imported stuff from original humidifier card into custom card 76 | const arrayFilter = ( 77 | array: any[], 78 | conditions: Array<(value: any) => boolean>, 79 | maxSize: number 80 | ) => { 81 | if (!maxSize || maxSize > array.length) { 82 | maxSize = array.length; 83 | } 84 | 85 | const filteredArray: any[] = []; 86 | 87 | for (let i = 0; i < array.length && filteredArray.length < maxSize; i++) { 88 | let meetsConditions = true; 89 | 90 | for (const condition of conditions) { 91 | if (!condition(array[i])) { 92 | meetsConditions = false; 93 | break; 94 | } 95 | } 96 | 97 | if (meetsConditions) { 98 | filteredArray.push(array[i]); 99 | } 100 | } 101 | 102 | return filteredArray; 103 | }; 104 | 105 | export const findEntities = ( 106 | hass: HomeAssistant, 107 | maxEntities: number, 108 | entities: string[], 109 | entitiesFallback: string[], 110 | includeDomains?: string[], 111 | entityFilter?: (stateObj: HassEntity) => boolean 112 | ) => { 113 | const conditions: Array<(value: string) => boolean> = []; 114 | 115 | if (includeDomains?.length) { 116 | conditions.push((eid) => includeDomains!.includes(computeDomain(eid))); 117 | } 118 | 119 | if (entityFilter) { 120 | conditions.push( 121 | (eid) => hass.states[eid] && entityFilter(hass.states[eid]) 122 | ); 123 | } 124 | 125 | const entityIds = arrayFilter(entities, conditions, maxEntities); 126 | 127 | if (entityIds.length < maxEntities && entitiesFallback.length) { 128 | const fallbackEntityIds = findEntities( 129 | hass, 130 | maxEntities - entityIds.length, 131 | entitiesFallback, 132 | [], 133 | includeDomains, 134 | entityFilter 135 | ); 136 | 137 | entityIds.push(...fallbackEntityIds); 138 | } 139 | 140 | return entityIds; 141 | }; 142 | 143 | 144 | export const computeStateName = (stateObj: HassEntity): string => 145 | stateObj.attributes.friendly_name === undefined 146 | ? computeObjectId(stateObj.entity_id).replace(/_/g, " ") 147 | : stateObj.attributes.friendly_name || ""; 148 | 149 | /** Compute the object ID of a state. */ 150 | export const computeObjectId = (entityId: string): string => entityId.substr(entityId.indexOf(".") + 1); 151 | 152 | const validEntityId = /^(\w+)\.(\w+)$/; 153 | 154 | export const isValidEntityId = (entityId: string) => validEntityId.test(entityId); 155 | 156 | 157 | export const processConfigEntities = < 158 | T extends EntityConfig | LovelaceRowConfig 159 | >( 160 | entities: Array, 161 | checkEntityId = true 162 | ): T[] => { 163 | if (!entities || !Array.isArray(entities)) { 164 | throw new Error("Entities need to be an array"); 165 | } 166 | 167 | return entities.map((entityConf, index): T => { 168 | if ( 169 | typeof entityConf === "object" && 170 | !Array.isArray(entityConf) && 171 | entityConf.type 172 | ) { 173 | return entityConf; 174 | } 175 | 176 | let config: T; 177 | 178 | if (typeof entityConf === "string") { 179 | config = { entity: entityConf } as T; 180 | } else if (typeof entityConf === "object" && !Array.isArray(entityConf)) { 181 | if (!("entity" in entityConf)) { 182 | throw new Error( 183 | `Entity object at position ${index} is missing entity field.` 184 | ); 185 | } 186 | config = entityConf as T; 187 | } else { 188 | throw new Error(`Invalid entity specified at position ${index}.`); 189 | } 190 | 191 | if (checkEntityId && !isValidEntityId((config as EntityConfig).entity!)) { 192 | throw new Error( 193 | `Invalid entity ID at position ${index}: ${ 194 | (config as EntityConfig).entity 195 | }` 196 | ); 197 | } 198 | 199 | return config; 200 | }); 201 | }; 202 | 203 | 204 | function hasConfigChanged(element: any, changedProps: PropertyValues): boolean { 205 | if (changedProps.has("_config")) { 206 | return true; 207 | } 208 | 209 | const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 210 | if (!oldHass) { 211 | return true; 212 | } 213 | 214 | if ( 215 | oldHass.connected !== element.hass!.connected || 216 | oldHass.themes !== element.hass!.themes || 217 | oldHass.locale !== element.hass!.locale || 218 | oldHass.localize !== element.hass.localize || 219 | oldHass.config.state !== element.hass.config.state 220 | ) { 221 | return true; 222 | } 223 | return false; 224 | } 225 | 226 | function hasConfigOrEntitiesChanged( 227 | element: any, 228 | changedProps: PropertyValues 229 | ): boolean { 230 | if (hasConfigChanged(element, changedProps)) { 231 | return true; 232 | } 233 | 234 | const oldHass = changedProps.get("hass") as HomeAssistant; 235 | 236 | const entities = processConfigEntities(element._config!.entities, false); 237 | 238 | return entities.some( 239 | (entity) => 240 | "entity" in entity && 241 | oldHass.states[entity.entity] !== element.hass!.states[entity.entity] 242 | ); 243 | } 244 | 245 | const createEntityNotFoundWarning = ( 246 | hass: HomeAssistant, 247 | entityId: string 248 | ) => 249 | hass.config.state !== STATE_NOT_RUNNING 250 | ? hass.localize( 251 | "ui.panel.lovelace.warning.entity_not_found", 252 | "entity", 253 | entityId || "[empty]" 254 | ) 255 | : hass.localize("ui.panel.lovelace.warning.starting"); 256 | 257 | const UNAVAILABLE = "unavailable"; 258 | const UNKNOWN = "unknown"; 259 | const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN]; 260 | 261 | const modeIcons: { [mode in HumidifierMode]: string } = { 262 | off: mdiAirHumidifierOff, 263 | set: mdiAirHumidifier, 264 | continuous: mdiAutorenew, 265 | smart: mdiHeadLightbulbOutline, 266 | dry: mdiTshirtCrew 267 | }; 268 | 269 | const powerIcons: { [status in PowerStatus]: string } = { 270 | off: mdiPower, 271 | on: mdiPowerCycle /* mdiPower*/, // colored version 272 | }; 273 | 274 | const fanModeIcons: { [fanMode in HumidifierFanMode]: string } = { 275 | silent: mdiFanSpeed1, 276 | low: mdiFanSpeed1, 277 | medium: mdiFanSpeed2, 278 | turbo: mdiFanSpeed3, 279 | high: mdiFanSpeed3 280 | } 281 | 282 | // const debug = (input: any) => JSON.stringify(input, null ,2) 283 | 284 | @customElement(CARD_NAME) 285 | export class MideaHumidifierCard extends LitElement implements LovelaceCard { 286 | public static async getConfigElement(): Promise { 287 | await import(`./midea-humidifier-card-editor`); 288 | return document.createElement(`midea-humidifier-card-editor`); 289 | } 290 | 291 | public static getStubConfig( 292 | hass: HomeAssistant, 293 | entities: string[], 294 | entitiesFallback: string[] 295 | ): HumidifierCardConfig { 296 | const includeDomains = ["humidifier"]; 297 | const maxEntities = 1; 298 | const foundEntities = findEntities( 299 | hass, 300 | maxEntities, 301 | entities, 302 | entitiesFallback, 303 | includeDomains 304 | ) || []; 305 | 306 | 307 | if(!foundEntities || !foundEntities[0].includes(".")) { 308 | return { 309 | type: "'custom:midea-humidifier-card'", 310 | entity: ``, 311 | fan_entity: ``, 312 | tank_entity: ``, 313 | defrost_entity: ``, 314 | filter_entity: ``, 315 | humidity_entity: ``, 316 | temperature_entity: ``, 317 | ion_entity: ``, 318 | swap_target_and_current_humidity: true, 319 | show_ion_toggle: true 320 | } 321 | } 322 | 323 | const humidifierEntity = foundEntities[0].split(".")[1] 324 | // console.info(`[${CARD_NAME}::getStubConfig]: trying to prefill config automatically, found entity: ${humidifierEntity}`) 325 | 326 | // zeroconf editor entities autofill 327 | return { 328 | type: "custom:midea-humidifier-card", 329 | entity: `humidifier.${humidifierEntity}`, 330 | fan_entity: `fan.${humidifierEntity}_fan`, 331 | tank_entity: `binary_sensor.${humidifierEntity}_tank_full`, 332 | defrost_entity: `binary_sensor.${humidifierEntity}_defrosting`, 333 | filter_entity: `binary_sensor.${humidifierEntity}_replace_filter`, 334 | humidity_entity: `sensor.${humidifierEntity}_humidity`, 335 | temperature_entity: `sensor.${humidifierEntity}_temperature`, 336 | ion_entity: `switch.${humidifierEntity}_ion_mode`, 337 | swap_target_and_current_humidity: true, 338 | show_ion_toggle: true 339 | }; 340 | } 341 | @property({ attribute: false }) public hass?: HomeAssistant; 342 | 343 | @state() private _config?: HumidifierCardConfig; 344 | 345 | @state() private _targetHumidity?: number; 346 | 347 | @state() private _currentHumidity?: number; 348 | 349 | public getCardSize(): number { 350 | return 6; 351 | } 352 | 353 | public setConfig(config: HumidifierCardConfig): void { 354 | if (!config.entity || config.entity.split(".")[0] !== "humidifier") { 355 | throw new Error("Specify an entity from within the humidifier domain"); 356 | } 357 | if (!config.fan_entity || config.fan_entity.split(".")[0] !== "fan") { 358 | throw new Error("Specify a fan_entity from within the fan domain"); 359 | } 360 | if (!config.humidity_entity || !config.temperature_entity) { 361 | throw new Error("Humidity and temperature sensors entity are required for this card to function properly"); 362 | } 363 | // console.info(`[${CARD_NAME}::setConfig]: Got new config : ${debug(config)}`) 364 | this._config = this.autoWatchEntities(config); 365 | } 366 | 367 | 368 | private autoWatchEntities(config: HumidifierCardConfig) : HumidifierCardConfig { 369 | // console.info(`[${CARD_NAME}::autoWatchEntities]: dumping original config ${debug(config)}`) 370 | const { entities = [] } = config 371 | Object.keys(config).map(key => { 372 | if(key.includes("entity")) { 373 | // console.info(`[${CARD_NAME}::autoWatchEntities]: found entity to add to entities[] ${config[key]}`) 374 | if(!entities.includes(config[key])) { 375 | entities.push(config[key]) 376 | } 377 | } 378 | }) 379 | const newConfig = { 380 | ...config, 381 | entities 382 | } 383 | // console.info(`[${CARD_NAME}::autoWatchEntities]: dumping modified newConfig ${debug(newConfig)}`) 384 | return newConfig 385 | } 386 | 387 | private _lower(string: string) { 388 | return string?.toLowerCase() 389 | } 390 | 391 | private _renderPowerIcon(currentPowerStatus: string, currentStatus: string): TemplateResult { 392 | 393 | if (!powerIcons[this._lower(currentPowerStatus)]) { 394 | return html``; 395 | } 396 | 397 | const isSelected = !!(currentPowerStatus === currentStatus) 398 | 399 | // //// console.info(`[Humidifier-card]: fan icon : _renderPowerIcon : ${currentPowerStatus} current : ${currentStatus} isSelected: ${isSelected}`) 400 | 401 | return html` 402 | 410 | ${currentPowerStatus} 411 | 412 | `; 413 | } 414 | 415 | 416 | private _renderFanIcon(fanMode: string, currentFanMode: string | undefined, poweredOff: boolean): TemplateResult { 417 | 418 | const isSelected = !!(currentFanMode === fanMode) 419 | // //// console.info(`[Humidifier-card]: fan icon : render : ${fanMode} current : ${currentFanMode} poweredOff: ${poweredOff} isSelected: ${isSelected}`) 420 | 421 | if (!fanModeIcons[this._lower(fanMode)]) { 422 | return html``; 423 | } 424 | return html` 425 | 433 | 434 | `; 435 | } 436 | 437 | private _renderIcon(mode: string, currentMode: string, poweredOff: boolean): TemplateResult { 438 | 439 | const isSelected = !!(this._lower(currentMode) === this._lower(mode)) 440 | 441 | if (!modeIcons[this._lower(mode)]) { 442 | return html``; 443 | } 444 | // //// console.info(`[Humidifier-card]: mode icon : render : ${mode} current : ${currentMode} poweredOff: ${poweredOff} isSelected: ${isSelected}`) 445 | return html` 446 | 454 | 455 | `; 456 | } 457 | 458 | private _handlePowerAction(e: MouseEvent): void { 459 | const callService = ((e.currentTarget as any).status === "on") ? "turn_on" : "turn_off" 460 | //// console.info(`[Humidifier-card]: calling service humidifier.${callService} with value ${(e.currentTarget as any).status}`) 461 | this.hass!.callService("humidifier", callService, { 462 | entity_id: this._config!.entity 463 | }); 464 | } 465 | 466 | private _handleFanAction(e: MouseEvent): void { 467 | //// console.info(`[Humidifier-card]: calling service fan.set_preset_mode with value ${(e.currentTarget as any).mode}`) 468 | this.hass!.callService("fan", "set_preset_mode", { 469 | entity_id: this._config!.fan_entity, 470 | preset_mode: (e.currentTarget as any).mode, 471 | }); 472 | } 473 | 474 | private _handleToggleIonModeAction(): void { 475 | //// console.info(`[Humidifier-card]: calling service switch.toggle with value ${(e.currentTarget as any).status}`) 476 | this.hass!.callService("switch", "toggle", { 477 | entity_id: this._config!.ion_entity 478 | }); 479 | } 480 | 481 | private _handleAction(e: MouseEvent): void { 482 | //// console.info(`[Humidifier-card]: calling service humidifier.set_mode with value ${(e.currentTarget as any).mode}`) 483 | this.hass!.callService("humidifier", "set_mode", { 484 | entity_id: this._config!.entity, 485 | mode: (e.currentTarget as any).mode, 486 | }); 487 | } 488 | 489 | 490 | private _getWarningText(problems: ProblemConfig): TemplateResult { 491 | return html`
492 | ${problems.tank ? html`${localize("common.tank_is_full")}` : html``} 493 | ${problems.defrost ? html`${localize("common.defrosting")}` : html``} 494 | ${problems.filter ? html`${localize("common.change_airfilter")}` : html``} 495 |
` 496 | } 497 | 498 | 499 | private _get(nestedObj, pathArr) { 500 | return pathArr.reduce((obj, key) => ((obj && obj[key] !== 'undefined') 501 | ? obj[key] 502 | : undefined 503 | ), nestedObj) 504 | } 505 | 506 | private _readHassState(configEntry: string) : HassEntity | undefined { 507 | const stateEntry : boolean | undefined = this._get(this,["_config", configEntry]) 508 | // console.log(`[${CARD_NAME}::this._readHassState]: canRead : ${stateEntry} - configEntry: ${configEntry}`, this.hass) 509 | if(stateEntry) { 510 | const value = this._get(this, ["hass","states",stateEntry]) 511 | // console.log(`[${CARD_NAME}::this._readHassState]: read value : ${debug(value)}`, this.hass) 512 | return value 513 | } 514 | return undefined 515 | } 516 | 517 | protected render(): TemplateResult { 518 | if (!this.hass || !this._config) { 519 | return html``; 520 | } 521 | 522 | // reading the global hass states 523 | const stateObj = this._readHassState("entity") as HumidifierEntity 524 | const fanStateObj = this._readHassState("fan_entity") as HumidifierFanEntity; 525 | const tankStateObj = this._readHassState("tank_entity") as BinaryEntity; 526 | const filterStateObj = this._readHassState("filter_entity") as BinaryEntity; 527 | const defrostStateObj = this._readHassState("defrost_entity") as BinaryEntity; 528 | const ionStateObj = this._readHassState("ion_entity") as BinaryEntity; 529 | const currentMode = this._lower(stateObj?.attributes?.mode) in modeIcons ? stateObj?.attributes?.mode : "unknown-mode" as string; 530 | const currentFanMode = this._lower(fanStateObj?.attributes?.preset_mode) in fanModeIcons ? fanStateObj?.attributes?.preset_mode : "unknown-fan-mode" as string; 531 | const currentHumidityString = this.hass.states[this._config.humidity_entity].state; 532 | const currentPowerStatus = stateObj.state 533 | const isPoweredOff = !!(currentPowerStatus === "off") 534 | const currentTemperatureString = this.hass.states[this._config.temperature_entity].state; 535 | const currentTemperature = parseFloat(currentTemperatureString) as number; 536 | const unitOfMeasurement = this.hass.states[this._config.temperature_entity].attributes?.unit_of_measurement ?? '°C'; 537 | 538 | if (!stateObj) { 539 | return html` 540 | 541 | ${createEntityNotFoundWarning(this.hass, this._config.entity)} 542 | 543 | `; 544 | } 545 | 546 | 547 | const swapTargetAndCurrent = (this._config.swap_target_and_current_humidity === true) 548 | 549 | const name = 550 | this._config!.name || 551 | computeStateName(this.hass!.states[this._config!.entity]); 552 | 553 | const targetHumidity = 554 | stateObj.attributes.humidity !== null && 555 | Number.isFinite(Number(stateObj.attributes.humidity)) 556 | ? stateObj.attributes.humidity 557 | : stateObj.attributes.min_humidity; 558 | 559 | const rtlDirection = computeRTLDirection(this.hass); 560 | const sliderDisabled = (UNAVAILABLE_STATES.includes(stateObj.state) || isPoweredOff) 561 | 562 | const slider = sliderDisabled 563 | ? html` 564 | 569 | ` 570 | : html` 571 | 579 | 580 | `; 581 | 582 | const targetHumiditySvg = svg` 583 | 584 | 591 | ${this._targetHumidity && !isNaN(this._targetHumidity) ? 592 | svg`${swapTargetAndCurrent ? html`${this._currentHumidity?.toFixed()}` : html`${this._targetHumidity.toFixed()}`} 593 | 594 | % 595 | 596 | ` 597 | : 598 | "" 599 | } 600 | 601 | 602 | `; 603 | 604 | 605 | const setValues = svg` 606 | 607 | 608 | 609 | ${ 610 | UNAVAILABLE_STATES.includes(currentHumidityString) || 611 | this._currentHumidity === undefined || 612 | this._currentHumidity === null 613 | ? "" 614 | : svg` 615 | ${swapTargetAndCurrent ? this._targetHumidity?.toFixed() : this._currentHumidity.toFixed()}% - ${currentTemperatureString ? currentTemperature : ""}${unitOfMeasurement} 616 | ` 617 | } 618 | 619 | 624 | ${this.hass!.localize(`state.default.${stateObj.state}`)} 625 | ${ 626 | stateObj.attributes.mode && 627 | !UNAVAILABLE_STATES.includes(stateObj.state) 628 | ? html` 629 | - 630 | ${this.hass!.localize( 631 | `state_attributes.humidifier.mode.${stateObj.attributes.mode}` 632 | ) || stateObj.attributes.mode} 633 | ` 634 | : "" 635 | } 636 | 637 | 638 | 639 | `; 640 | 641 | const problems = { 642 | tank: (tankStateObj?.state === 'on'), 643 | defrost: (defrostStateObj?.state === 'on'), // not sure how to report this 644 | filter: (filterStateObj?.state === 'on') 645 | } 646 | 647 | const hasIssues : boolean = ((tankStateObj?.state === 'on')||(defrostStateObj?.state === 'on')||(filterStateObj?.state === 'on')) || false 648 | 649 | return html` 650 | 655 | 662 |
663 |
664 |
665 | ${slider} 666 |
667 |
${targetHumiditySvg} ${setValues}
668 |
669 |
670 |
671 |
672 |
673 | ${(stateObj.attributes.available_modes || []) 674 | .concat() 675 | .sort(compareClimateHumidifierModes) 676 | .map((modeItem) => this._renderIcon(modeItem, currentMode, isPoweredOff))} 677 | ${problems?.defrost 678 | ? html`` : html``} 684 |
685 |
686 | ${(this._config?.show_ion_toggle === true) 687 | ? html`` : html``} 694 | ${(fanStateObj?.attributes?.preset_modes || []) 695 | .concat() 696 | .sort(compareClimateFanHumidifierModes) 697 | .map((modeItem) => this._renderFanIcon(modeItem, currentFanMode, isPoweredOff))} 698 | ${this._renderPowerIcon(currentPowerStatus === 'off' ? "on" : "off", currentPowerStatus)} 699 |
700 | ${name} 701 |
702 |
703 |
704 | ${hasIssues ? this._getWarningText(problems) : html``} 705 |
706 |
707 | `; 708 | } 709 | 710 | protected shouldUpdate(changedProps: PropertyValues): boolean { 711 | return hasConfigOrEntitiesChanged(this, changedProps); 712 | } 713 | 714 | protected updated(changedProps: PropertyValues): void { 715 | super.updated(changedProps); 716 | 717 | 718 | if ( 719 | !this._config || 720 | !this.hass || 721 | (!changedProps.has("hass") && !changedProps.has("_config")) 722 | ) { 723 | return; 724 | } 725 | 726 | const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 727 | const oldConfig = changedProps.get("_config") as 728 | | HumidifierCardConfig 729 | | undefined; 730 | 731 | 732 | if ( 733 | !oldHass || 734 | !oldConfig || 735 | oldHass.themes !== this.hass.themes || 736 | oldConfig.theme !== this._config.theme 737 | ) { 738 | applyThemesOnElement(this, this.hass.themes, this._config.theme); 739 | } 740 | 741 | 742 | const stateObj = this.hass.states[this._config.entity]; 743 | if (!stateObj) { 744 | return; 745 | } 746 | 747 | const fanStateObj = this.hass.states[this._config.fan_entity]; 748 | if (!fanStateObj) { 749 | return; 750 | } 751 | 752 | if (!oldHass || oldHass.states[this._config.entity] !== stateObj || oldHass.states[this._config.fan_entity] !== fanStateObj) { 753 | this._rescale_svg(); 754 | } 755 | } 756 | 757 | public willUpdate(changedProps: PropertyValues) { 758 | if (!this.hass || !this._config || !changedProps.has("hass")) { 759 | return; 760 | } 761 | 762 | const stateObj = this.hass.states[this._config.entity]; 763 | if (!stateObj) { 764 | return; 765 | } 766 | 767 | const humidityStateObj = this.hass.states[this._config.humidity_entity]; 768 | if (!humidityStateObj) { 769 | return; 770 | } 771 | 772 | const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 773 | 774 | if (!oldHass || oldHass.states[this._config.entity] !== stateObj) { 775 | this._targetHumidity = this._getTargetHumidity(stateObj); 776 | } 777 | 778 | if (!oldHass || oldHass.states[this._config.humidity_entity] !== stateObj) { 779 | this._currentHumidity = this._getCurrentHumidity(humidityStateObj); 780 | } 781 | } 782 | 783 | 784 | 785 | // } 786 | private _rescale_svg() { 787 | // Set the viewbox of the SVG containing the set humidity to perfectly 788 | // fit the text 789 | // That way it will auto-scale correctly 790 | // This is not done to the SVG containing the current humidity, because 791 | // it should not be centered on the text, but only on the value 792 | if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) { 793 | ( 794 | this.shadowRoot.querySelector("ha-card") as LitElement 795 | ).updateComplete.then(() => { 796 | const svgRoot = this.shadowRoot!.querySelector("#set-values"); 797 | const box = svgRoot!.querySelector("g")!.getBBox(); 798 | //// console.warn(`[${CARD_NAME}::_rescale_svg]: box ${debug(box)}`) 799 | svgRoot!.setAttribute( 800 | "viewBox", 801 | `${box!.x} ${box!.y} ${box!.width} ${box!.height}` 802 | ); 803 | svgRoot!.setAttribute("width", `${box!.width}`); 804 | svgRoot!.setAttribute("height", `${box!.height}`); 805 | }); 806 | } 807 | } 808 | 809 | private _getTargetHumidity(stateObj: HassEntity): undefined | number { 810 | 811 | if (UNAVAILABLE_STATES.includes(stateObj.state)) { 812 | //// console.warn(`[${CARD_NAME}::_getTargetHumidity]: returning undefined ${stateObj.state}`) 813 | return undefined; 814 | } 815 | //// console.warn(`[${CARD_NAME}::_getTargetHumidity]: returning ${stateObj.attributes.humidity}`) 816 | return stateObj.attributes.humidity; 817 | } 818 | 819 | private _dragEvent(e): void { 820 | this._targetHumidity = e.detail.value; 821 | //// console.warn(`[${CARD_NAME}::_dragEvent]: returning ${this._targetHumidity}`) 822 | } 823 | 824 | private _setTargetHumidity(e): void { 825 | //// console.warn(`[${CARD_NAME}::_targetHumidityidity]: setting ${e.detail.value} - this._targetHumidity: ${this._targetHumidity}`) 826 | this.hass!.callService("humidifier", "set_humidity", { 827 | entity_id: this._config!.entity, 828 | humidity: e.detail.value, 829 | }); 830 | } 831 | 832 | private _getCurrentHumidity(stateObj: HassEntity): undefined | number { 833 | if (UNAVAILABLE_STATES.includes(stateObj.state) || isNaN(parseFloat(stateObj.state))) { 834 | //// console.warn(`[${CARD_NAME}::_getTargetHumidity]: returning undefined ${stateObj.state}`) 835 | return undefined; 836 | } 837 | return parseFloat(stateObj.state); 838 | } 839 | 840 | 841 | private _handleMoreInfo() { 842 | fireEvent(this, "hass-more-info", { 843 | entityId: this._config!.entity, 844 | }); 845 | } 846 | 847 | static get styles(): CSSResultGroup { 848 | return css` 849 | :host { 850 | display: block; 851 | } 852 | 853 | ha-card { 854 | height: 100%; 855 | position: relative; 856 | overflow: hidden; 857 | --name-font-size: 1.2rem; 858 | --brightness-font-size: 1.2rem; 859 | --rail-border-color: transparent; 860 | } 861 | .Off { 862 | --mode-color: var(--state-climate-idle-color); 863 | } 864 | .Dry { 865 | --mode-color: var(--state-climate-heat-color); 866 | } 867 | .Smart { 868 | --mode-color: var(--state-climate-cool-color); 869 | } 870 | .Continuous { 871 | --mode-color: var(--state-climate-eco-color); 872 | } 873 | .Set { 874 | --mode-color: var(--state-climate-manual-color); 875 | } 876 | .unknown-mode { 877 | --mode-color: var(--state-unknown-color); 878 | } 879 | .error { 880 | --mode-color: var(--error-color); 881 | } 882 | 883 | .more-info { 884 | position: absolute; 885 | cursor: pointer; 886 | top: 0; 887 | right: 0; 888 | border-radius: 100%; 889 | color: var(--secondary-text-color); 890 | z-index: 1; 891 | } 892 | 893 | .content { 894 | height: 100%; 895 | display: flex; 896 | flex-direction: column; 897 | justify-content: center; 898 | } 899 | 900 | #controls { 901 | display: flex; 902 | justify-content: center; 903 | padding: 16px; 904 | position: relative; 905 | } 906 | 907 | #slider { 908 | height: 100%; 909 | width: 100%; 910 | position: relative; 911 | max-width: 250px; 912 | min-width: 100px; 913 | } 914 | 915 | round-slider { 916 | --round-slider-path-color: var(--slider-track-color); 917 | --round-slider-bar-color: var(--mode-color); 918 | padding-bottom: 10%; 919 | } 920 | 921 | #slider-center { 922 | position: absolute; 923 | width: calc(100% - 40px); 924 | height: calc(100% - 40px); 925 | box-sizing: border-box; 926 | border-radius: 100%; 927 | left: 20px; 928 | top: 20px; 929 | text-align: center; 930 | overflow-wrap: break-word; 931 | pointer-events: none; 932 | } 933 | 934 | #humidity { 935 | position: absolute; 936 | transform: translate(-50%, -50%); 937 | width: 100%; 938 | height: 50%; 939 | top: 45%; 940 | left: 50%; 941 | } 942 | 943 | #set-values { 944 | max-width: 80%; 945 | transform: translate(0, -50%); 946 | font-size: 20px; 947 | } 948 | 949 | #set-mode { 950 | fill: var(--secondary-text-color); 951 | font-size: 16px; 952 | } 953 | 954 | #warnings { 955 | padding: 16px; 956 | } 957 | 958 | #info { 959 | display: flex-vertical; 960 | justify-content: center; 961 | text-align: center; 962 | padding: 16px; 963 | margin-top: -60px; 964 | font-size: var(--name-font-size); 965 | } 966 | 967 | #modes > * { 968 | color: var(--disabled-text-color); 969 | cursor: pointer; 970 | display: inline-block; 971 | } 972 | 973 | #modes .selected-icon { 974 | color: var(--mode-color); 975 | } 976 | 977 | #modes .defrost-icon { 978 | color: var(--state-climate-cool-color); 979 | } 980 | 981 | #modes .ion-icon { 982 | color: var(--mode-color); 983 | } 984 | 985 | text { 986 | fill: var(--primary-text-color); 987 | } 988 | `; 989 | } 990 | } 991 | 992 | declare global { 993 | interface HTMLElementTagNameMap { 994 | [CARD_NAME]: MideaHumidifierCard; 995 | } 996 | } 997 | 998 | 999 | -------------------------------------------------------------------------------- /dist/midea-humidifier-card.js: -------------------------------------------------------------------------------- 1 | function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,n);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(r=(o<3?s(r):o>3?s(e,i,r):s(e,i))||r);return o>3&&r&&Object.defineProperty(e,i,r),r}var e="M22.1 21.5L2.4 1.7L1.1 3L3.8 5.7C3.3 6.3 3 7.1 3 8V22H18V19.9L20.8 22.7L22.1 21.5M9.6 11.5L12.4 14.3C12.1 14.7 11.6 15 11 15C9.9 15 9 14.1 9 13C9 12.4 9.3 11.9 9.6 11.5M16 17.9V20H5V8C5 7.7 5.1 7.4 5.2 7.1L8.2 10.1C7.5 10.8 7 11.9 7 13C7 15.2 8.8 17 11 17C12.1 17 13.2 16.5 13.9 15.8L16 17.9M17 13.8C17.1 12.5 19 10.5 19 10.5S21 12.7 21 14C21 15 20.2 15.9 19.2 16L17 13.8M9.2 6L7.2 4H14C16.2 4 18 5.8 18 8V9H16V8C16 6.9 15.1 6 14 6H9.2Z",i="M13 19C13 17.59 13.5 16.3 14.3 15.28C14.17 14.97 14.03 14.65 13.86 14.34C14.26 14 14.57 13.59 14.77 13.11C15.26 13.21 15.78 13.39 16.25 13.67C17.07 13.25 18 13 19 13C20.05 13 21.03 13.27 21.89 13.74C21.95 13.37 22 12.96 22 12.5C22 8.92 18.03 8.13 14.33 10.13C14 9.73 13.59 9.42 13.11 9.22C13.3 8.29 13.74 7.24 14.73 6.75C17.09 5.57 17 2 12.5 2C8.93 2 8.14 5.96 10.13 9.65C9.72 9.97 9.4 10.39 9.21 10.87C8.28 10.68 7.23 10.25 6.73 9.26C5.56 6.89 2 7 2 11.5C2 15.07 5.95 15.85 9.64 13.87C9.96 14.27 10.39 14.59 10.88 14.79C10.68 15.71 10.24 16.75 9.26 17.24C6.9 18.42 7 22 11.5 22C12.31 22 13 21.78 13.5 21.41C13.19 20.67 13 19.86 13 19M12 13C11.43 13 11 12.55 11 12S11.43 11 12 11C12.54 11 13 11.45 13 12S12.54 13 12 13M17 15V17H18V23H20V15H17Z",n="M13 19C13 17.59 13.5 16.3 14.3 15.28C14.17 14.97 14.03 14.65 13.86 14.34C14.26 14 14.57 13.59 14.77 13.11C15.26 13.21 15.78 13.39 16.25 13.67C17.07 13.25 18 13 19 13C20.05 13 21.03 13.27 21.89 13.74C21.95 13.37 22 12.96 22 12.5C22 8.92 18.03 8.13 14.33 10.13C14 9.73 13.59 9.42 13.11 9.22C13.3 8.29 13.74 7.24 14.73 6.75C17.09 5.57 17 2 12.5 2C8.93 2 8.14 5.96 10.13 9.65C9.72 9.97 9.4 10.39 9.21 10.87C8.28 10.68 7.23 10.25 6.73 9.26C5.56 6.89 2 7 2 11.5C2 15.07 5.95 15.85 9.64 13.87C9.96 14.27 10.39 14.59 10.88 14.79C10.68 15.71 10.24 16.75 9.26 17.24C6.9 18.42 7 22 11.5 22C12.31 22 13 21.78 13.5 21.41C13.19 20.67 13 19.86 13 19M12 13C11.43 13 11 12.55 11 12S11.43 11 12 11C12.54 11 13 11.45 13 12S12.54 13 12 13M21 21V20.5C21 19.67 20.33 19 19.5 19C20.33 19 21 18.33 21 17.5V17C21 15.89 20.1 15 19 15H16V17H19V18H17V20H19V21H16V23H19C20.11 23 21 22.11 21 21";var s={version:"Version",invalid_configuration:"Invalid configuration",tank_is_full:"Caution : Water tank is full, please empty it",change_airfilter:"Caution : Please change the air filter",defrosting:"Caution : Please wait, currently defrosting...",tank:"Tank",filter:"Filter",defrost:"Defrost"},o={common:s},r={version:"Version",invalid_configuration:"Configuration invalide",tank_is_full:"Attention : Le réservoir est plein",change_airfilter:"Attention : Changer le filtre à air",defrosting:"Attention : Dégivrage en cours...",tank:"Réservoir",filter:"Filtre",defrost:"Dégivrage"},a={common:r};const l={en:Object.freeze({__proto__:null,common:s,default:o}),fr:Object.freeze({__proto__:null,common:r,default:a})};function h(t,e="",i=""){const n=(localStorage.getItem("selectedLanguage")||"en").replace(/['"]+/g,"").replace("-","_");let s;try{s=t.split(".").reduce(((t,e)=>t[e]),l[n])}catch(e){s=t.split(".").reduce(((t,e)=>t[e]),l.en)}return void 0===s&&(s=t.split(".").reduce(((t,e)=>t[e]),l.en)),""!==e&&""!==i&&(s=s.replace(e,i)),s}const d=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,c=Symbol(),u=new Map;class _{constructor(t,e){if(this._$cssResult$=!0,e!==c)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){let t=u.get(this.cssText);return d&&void 0===t&&(u.set(this.cssText,t=new CSSStyleSheet),t.replaceSync(this.cssText)),t}toString(){return this.cssText}}const f=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[n+1]),t[0]);return new _(i,c)},p=d?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new _("string"==typeof t?t:t+"",c))(e)})(t):t;var m,v;const g={toAttribute(t,e){switch(e){case Boolean:t=t?"":null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},y=(t,e)=>e!==t&&(e==e||t==t),$={attribute:!0,type:String,converter:g,reflect:!1,hasChanged:y};class b extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o()}static addInitializer(t){var e;null!==(e=this.l)&&void 0!==e||(this.l=[]),this.l.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const n=this._$Eh(i,e);void 0!==n&&(this._$Eu.set(n,i),t.push(n))})),t}static createProperty(t,e=$){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,n=this.getPropertyDescriptor(t,i,e);void 0!==n&&Object.defineProperty(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(n){const s=this[t];this[e]=n,this.requestUpdate(t,s,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||$}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this._$Eu=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(p(t))}else void 0!==t&&e.push(p(t));return e}static _$Eh(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}o(){var t;this._$Ev=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Ep(),this.requestUpdate(),null===(t=this.constructor.l)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$Em)&&void 0!==e?e:this._$Em=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$Em)||void 0===e||e.splice(this._$Em.indexOf(t)>>>0,1)}_$Ep(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Et.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,e)=>{d?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),n=window.litNonce;void 0!==n&&i.setAttribute("nonce",n),i.textContent=e.cssText,t.appendChild(i)}))})(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Em)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Em)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$Eg(t,e,i=$){var n,s;const o=this.constructor._$Eh(t,i);if(void 0!==o&&!0===i.reflect){const r=(null!==(s=null===(n=i.converter)||void 0===n?void 0:n.toAttribute)&&void 0!==s?s:g.toAttribute)(e,i.type);this._$Ei=t,null==r?this.removeAttribute(o):this.setAttribute(o,r),this._$Ei=null}}_$AK(t,e){var i,n,s;const o=this.constructor,r=o._$Eu.get(t);if(void 0!==r&&this._$Ei!==r){const t=o.getPropertyOptions(r),a=t.converter,l=null!==(s=null!==(n=null===(i=a)||void 0===i?void 0:i.fromAttribute)&&void 0!==n?n:"function"==typeof a?a:null)&&void 0!==s?s:g.fromAttribute;this._$Ei=r,this[r]=l(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let n=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||y)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$ES&&(this._$ES=new Map),this._$ES.set(t,i))):n=!1),!this.isUpdatePending&&n&&(this._$Ev=this._$EC())}async _$EC(){this.isUpdatePending=!0;try{await this._$Ev}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,e)=>this[e]=t)),this._$Et=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$Em)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$ET()}catch(t){throw e=!1,this._$ET(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$Em)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$ET(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ev}shouldUpdate(t){return!0}update(t){void 0!==this._$ES&&(this._$ES.forEach(((t,e)=>this._$Eg(e,this[e],t))),this._$ES=void 0),this._$ET()}updated(t){}firstUpdated(t){}}var C,A;b.finalized=!0,b.elementProperties=new Map,b.elementStyles=[],b.shadowRootOptions={mode:"open"},null===(m=globalThis.reactiveElementPolyfillSupport)||void 0===m||m.call(globalThis,{ReactiveElement:b}),(null!==(v=globalThis.reactiveElementVersions)&&void 0!==v?v:globalThis.reactiveElementVersions=[]).push("1.0.0");const w=globalThis.trustedTypes,S=w?w.createPolicy("lit-html",{createHTML:t=>t}):void 0,x=`lit$${(Math.random()+"").slice(9)}$`,H="?"+x,E=`<${H}>`,k=document,V=(t="")=>k.createComment(t),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,L=Array.isArray,T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,z=/-->/g,P=/>/g,O=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,N=/'/g,U=/"/g,R=/^(?:script|style|textarea)$/i,D=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),I=D(1),j=D(2),B=Symbol.for("lit-noChange"),F=Symbol.for("lit-nothing"),q=new WeakMap,Z=k.createTreeWalker(k,129,null,!1),W=(t,e)=>{const i=t.length-1,n=[];let s,o=2===e?"":"",r=T;for(let e=0;e"===l[0]?(r=null!=s?s:T,h=-1):void 0===l[1]?h=-2:(h=r.lastIndex-l[2].length,a=l[1],r=void 0===l[3]?O:'"'===l[3]?U:N):r===U||r===N?r=O:r===z||r===P?r=T:(r=O,s=void 0);const c=r===O&&t[e+1].startsWith("/>")?" ":"";o+=r===T?i+E:h>=0?(n.push(a),i.slice(0,h)+"$lit$"+i.slice(h)+x+c):i+x+(-2===h?(n.push(void 0),e):c)}const a=o+(t[i]||"")+(2===e?"":"");return[void 0!==S?S.createHTML(a):a,n]};class J{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let s=0,o=0;const r=t.length-1,a=this.parts,[l,h]=W(t,e);if(this.el=J.createElement(l,i),Z.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(n=Z.nextNode())&&a.length0){n.textContent=w?w.emptyScript:"";for(let i=0;i{var e;return L(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])})(t)?this.M(t):this.$(t)}A(t,e=this._$AB){return this._$AA.parentNode.insertBefore(t,e)}S(t){this._$AH!==t&&(this._$AR(),this._$AH=this.A(t))}$(t){this._$AH!==F&&M(this._$AH)?this._$AA.nextSibling.data=t:this.S(k.createTextNode(t)),this._$AH=t}T(t){var e;const{values:i,_$litType$:n}=t,s="number"==typeof n?this._$AC(t):(void 0===n.el&&(n.el=J.createElement(n.h,this.options)),n);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===s)this._$AH.m(i);else{const t=new G(s,this),e=t.p(this.options);t.m(i),this.S(e),this._$AH=t}}_$AC(t){let e=q.get(t.strings);return void 0===e&&q.set(t.strings,e=new J(t)),e}M(t){L(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,n=0;for(const s of t)n===e.length?e.push(i=new Q(this.A(V()),this.A(V()),this,this.options)):i=e[n],i._$AI(s),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=F}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,n){const s=this.strings;let o=!1;if(void 0===s)t=K(this,t,e,0),o=!M(t)||t!==this._$AH&&t!==B,o&&(this._$AH=t);else{const n=t;let r,a;for(t=s[0],r=0;r{var n,s;const o=null!==(n=null==i?void 0:i.renderBefore)&&void 0!==n?n:e;let r=o._$litPart$;if(void 0===r){const t=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:null;o._$litPart$=r=new Q(e.insertBefore(V(),t),t,void 0,null!=i?i:{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!1)}render(){return B}}rt.finalized=!0,rt._$litElement$=!0,null===(nt=globalThis.litElementHydrateSupport)||void 0===nt||nt.call(globalThis,{LitElement:rt}),null===(st=globalThis.litElementPolyfillSupport)||void 0===st||st.call(globalThis,{LitElement:rt}),(null!==(ot=globalThis.litElementVersions)&&void 0!==ot?ot:globalThis.litElementVersions=[]).push("3.0.0");const at=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:n}=e;return{kind:i,elements:n,finisher(e){window.customElements.define(t,e)}}})(t,e),lt=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function ht(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):lt(t,e)}function dt(t){return ht({...t,state:!0})}const ct=1;const ut=(t=>(...e)=>({_$litDirective$:t,values:e}))(class extends class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}{constructor(t){var e;if(super(t),t.type!==ct||"class"!==t.name||(null===(e=t.strings)||void 0===e?void 0:e.length)>2)throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.")}render(t){return" "+Object.keys(t).filter((e=>t[e])).join(" ")+" "}update(t,[e]){var i,n;if(void 0===this.st){this.st=new Set,void 0!==t.strings&&(this.et=new Set(t.strings.join(" ").split(/\s/).filter((t=>""!==t))));for(const t in e)e[t]&&!(null===(i=this.et)||void 0===i?void 0:i.has(t))&&this.st.add(t);return this.render(e)}const s=t.element.classList;this.st.forEach((t=>{t in e||(s.remove(t),this.st.delete(t))}));for(const t in e){const i=!!e[t];i===this.st.has(t)||(null===(n=this.et)||void 0===n?void 0:n.has(t))||(i?(s.add(t),this.st.add(t)):(s.remove(t),this.st.delete(t)))}return B}});var _t="[^\\s]+";function ft(t,e){for(var i=[],n=0,s=t.length;n-1?s:null}};function mt(t){for(var e=[],i=1;i3?0:(t-t%10!=10?1:0)*t%10]}},bt=(mt({},$t),function(t){return+t-1}),Ct=[null,"[1-9]\\d?"],At=[null,_t],wt=["isPm",_t,function(t,e){var i=t.toLowerCase();return i===e.amPm[0]?0:i===e.amPm[1]?1:null}],St=["timezoneOffset","[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z?",function(t){var e=(t+"").match(/([+-]|\d\d)/gi);if(e){var i=60*+e[1]+parseInt(e[2],10);return"+"===e[0]?i:-i}return 0}];pt("monthNamesShort"),pt("monthNames");function xt(){return(xt=Object.assign||function(t){for(var e=1;eMt[t]-Mt[e],Tt=(t,e)=>Vt[t]-Vt[e];console.info("%c midea-humidifier-card %c 1.0.7","color: cyan; background: black; font-weight: bold;","color: darkblue; background: white; font-weight: bold;"),window.customCards=window.customCards||[],window.customCards.push({type:"midea-humidifier-card",name:"Midea (De)humidifier Card",description:"Midea/Inventor (De)humidifier lovelace UI card"});const zt=(t,e,i,n,s,o)=>{const r=[];(null==s?void 0:s.length)&&r.push((t=>s.includes(function(t){return t.substr(0,t.indexOf("."))}(t)))),o&&r.push((e=>t.states[e]&&o(t.states[e])));const a=((t,e,i)=>{(!i||i>t.length)&&(i=t.length);const n=[];for(let s=0;svoid 0===t.attributes.friendly_name?Ot(t.entity_id).replace(/_/g," "):t.attributes.friendly_name||"",Ot=t=>t.substr(t.indexOf(".")+1),Nt=/^(\w+)\.(\w+)$/,Ut=t=>Nt.test(t),Rt=(t,e=!0)=>{if(!t||!Array.isArray(t))throw new Error("Entities need to be an array");return t.map(((t,i)=>{if("object"==typeof t&&!Array.isArray(t)&&t.type)return t;let n;if("string"==typeof t)n={entity:t};else{if("object"!=typeof t||Array.isArray(t))throw new Error(`Invalid entity specified at position ${i}.`);if(!("entity"in t))throw new Error(`Entity object at position ${i} is missing entity field.`);n=t}if(e&&!Ut(n.entity))throw new Error(`Invalid entity ID at position ${i}: ${n.entity}`);return n}))};const Dt=["unavailable","unknown"],It={off:e,set:"M11 9C8.79 9 7 10.79 7 13S8.79 17 11 17 15 15.21 15 13 13.21 9 11 9M11 15C9.9 15 9 14.11 9 13S9.9 11 11 11 13 11.9 13 13 12.11 15 11 15M7 4H14C16.21 4 18 5.79 18 8V9H16V8C16 6.9 15.11 6 14 6H7C5.9 6 5 6.9 5 8V20H16V18H18V22H3V8C3 5.79 4.79 4 7 4M19 10.5C19 10.5 21 12.67 21 14C21 15.1 20.1 16 19 16S17 15.1 17 14C17 12.67 19 10.5 19 10.5",continuous:"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z",smart:"M13 3C16.9 3 20 6.1 20 10C20 12.8 18.4 15.2 16 16.3V21H9V18H8C6.9 18 6 17.1 6 16V13H4.5C4.1 13 3.8 12.5 4.1 12.2L6 9.7C6.2 5.9 9.2 3 13 3M13 1C8.4 1 4.6 4.4 4.1 8.9L2.5 11C1.9 11.8 1.9 12.8 2.3 13.6C2.7 14.3 3.3 14.8 4 14.9V16C4 17.9 5.3 19.4 7 19.9V23H18V17.5C20.5 15.8 22 13.1 22 10C22 5 18 1 13 1M14 14H12V13H14V14M15.6 9.5C15.3 9.9 15 10.3 14.5 10.6V12H11.5V10.6C10.1 9.8 9.6 7.9 10.4 6.5S13.1 4.6 14.5 5.4 16.4 8.1 15.6 9.5Z",dry:"M16,21H8A1,1 0 0,1 7,20V12.07L5.7,13.07C5.31,13.46 4.68,13.46 4.29,13.07L1.46,10.29C1.07,9.9 1.07,9.27 1.46,8.88L7.34,3H9C9,4.1 10.34,5 12,5C13.66,5 15,4.1 15,3H16.66L22.54,8.88C22.93,9.27 22.93,9.9 22.54,10.29L19.71,13.12C19.32,13.5 18.69,13.5 18.3,13.12L17,12.12V20A1,1 0 0,1 16,21"},jt={off:"M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13",on:"M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M12,19A7,7 0 0,1 5,12A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19M13,17H11V7H13V17Z"},Bt={silent:i,low:i,medium:"M13 19C13 17.59 13.5 16.3 14.3 15.28C14.17 14.97 14.03 14.65 13.86 14.34C14.26 14 14.57 13.59 14.77 13.11C15.26 13.21 15.78 13.39 16.25 13.67C17.07 13.25 18 13 19 13C20.05 13 21.03 13.27 21.89 13.74C21.95 13.37 22 12.96 22 12.5C22 8.92 18.03 8.13 14.33 10.13C14 9.73 13.59 9.42 13.11 9.22C13.3 8.29 13.74 7.24 14.73 6.75C17.09 5.57 17 2 12.5 2C8.93 2 8.14 5.96 10.13 9.65C9.72 9.97 9.4 10.39 9.21 10.87C8.28 10.68 7.23 10.25 6.73 9.26C5.56 6.89 2 7 2 11.5C2 15.07 5.95 15.85 9.64 13.87C9.96 14.27 10.39 14.59 10.88 14.79C10.68 15.71 10.24 16.75 9.26 17.24C6.9 18.42 7 22 11.5 22C12.31 22 13 21.78 13.5 21.41C13.19 20.67 13 19.86 13 19M12 13C11.43 13 11 12.55 11 12S11.43 11 12 11C12.54 11 13 11.45 13 12S12.54 13 12 13M16 15V17H19V18H18C16.9 18 16 18.9 16 20V23H21V21H18V20H19C20.11 20 21 19.11 21 18V17C21 15.9 20.11 15 19 15H16Z",turbo:n,high:n};let Ft=class extends rt{static async getConfigElement(){return await Promise.resolve().then((function(){return Zt})),document.createElement("midea-humidifier-card-editor")}static getStubConfig(t,e,i){const n=zt(t,1,e,i,["humidifier"])||[];if(!n||!n[0].includes("."))return{type:"'custom:midea-humidifier-card'",entity:"",fan_entity:"",tank_entity:"",defrost_entity:"",filter_entity:"",humidity_entity:"",temperature_entity:"",ion_entity:"",swap_target_and_current_humidity:!0,show_ion_toggle:!0};const s=n[0].split(".")[1];return{type:"custom:midea-humidifier-card",entity:`humidifier.${s}`,fan_entity:`fan.${s}_fan`,tank_entity:`binary_sensor.${s}_tank_full`,defrost_entity:`binary_sensor.${s}_defrosting`,filter_entity:`binary_sensor.${s}_replace_filter`,humidity_entity:`sensor.${s}_humidity`,temperature_entity:`sensor.${s}_temperature`,ion_entity:`switch.${s}_ion_mode`,swap_target_and_current_humidity:!0,show_ion_toggle:!0}}getCardSize(){return 6}setConfig(t){if(!t.entity||"humidifier"!==t.entity.split(".")[0])throw new Error("Specify an entity from within the humidifier domain");if(!t.fan_entity||"fan"!==t.fan_entity.split(".")[0])throw new Error("Specify a fan_entity from within the fan domain");if(!t.humidity_entity||!t.temperature_entity)throw new Error("Humidity and temperature sensors entity are required for this card to function properly");this._config=this.autoWatchEntities(t)}autoWatchEntities(t){const{entities:e=[]}=t;Object.keys(t).map((i=>{i.includes("entity")&&(e.includes(t[i])||e.push(t[i]))}));return Object.assign(Object.assign({},t),{entities:e})}_lower(t){return null==t?void 0:t.toLowerCase()}_renderPowerIcon(t,e){if(!jt[this._lower(t)])return I``;return I` 2 | 10 | ${t} 11 | 12 | `}_renderFanIcon(t,e,i){const n=!(e!==t);return Bt[this._lower(t)]?I` 13 | 21 | 22 | `:I``}_renderIcon(t,e,i){const n=!(this._lower(e)!==this._lower(t));return It[this._lower(t)]?I` 23 | 31 | 32 | `:I``}_handlePowerAction(t){const e="on"===t.currentTarget.status?"turn_on":"turn_off";this.hass.callService("humidifier",e,{entity_id:this._config.entity})}_handleFanAction(t){this.hass.callService("fan","set_preset_mode",{entity_id:this._config.fan_entity,preset_mode:t.currentTarget.mode})}_handleToggleIonModeAction(){this.hass.callService("switch","toggle",{entity_id:this._config.ion_entity})}_handleAction(t){this.hass.callService("humidifier","set_mode",{entity_id:this._config.entity,mode:t.currentTarget.mode})}_getWarningText(t){return I`
33 | ${t.tank?I`${h("common.tank_is_full")}`:I``} 34 | ${t.defrost?I`${h("common.defrosting")}`:I``} 35 | ${t.filter?I`${h("common.change_airfilter")}`:I``} 36 |
`}_get(t,e){return e.reduce(((t,e)=>t&&"undefined"!==t[e]?t[e]:void 0),t)}_readHassState(t){const e=this._get(this,["_config",t]);if(e){return this._get(this,["hass","states",e])}}render(){var t,i,n,s,o,r,a,l,h,d;if(!this.hass||!this._config)return I``;const c=this._readHassState("entity"),u=this._readHassState("fan_entity"),_=this._readHassState("tank_entity"),f=this._readHassState("filter_entity"),p=this._readHassState("defrost_entity"),m=this._readHassState("ion_entity"),v=this._lower(null===(t=null==c?void 0:c.attributes)||void 0===t?void 0:t.mode)in It?null===(i=null==c?void 0:c.attributes)||void 0===i?void 0:i.mode:"unknown-mode",g=this._lower(null===(n=null==u?void 0:u.attributes)||void 0===n?void 0:n.preset_mode)in Bt?null===(s=null==u?void 0:u.attributes)||void 0===s?void 0:s.preset_mode:"unknown-fan-mode",y=this.hass.states[this._config.humidity_entity].state,$=c.state,b=!("off"!==$),C=this.hass.states[this._config.temperature_entity].state,A=parseFloat(C),w=null!==(r=null===(o=this.hass.states[this._config.temperature_entity].attributes)||void 0===o?void 0:o.unit_of_measurement)&&void 0!==r?r:"°C";if(!c)return I` 37 | 38 | ${S=this.hass,x=this._config.entity,"NOT_RUNNING"!==S.config.state?S.localize("ui.panel.lovelace.warning.entity_not_found","entity",x||"[empty]"):S.localize("ui.panel.lovelace.warning.starting")} 39 | 40 | `;var S,x;const H=!0===this._config.swap_target_and_current_humidity,E=this._config.name||Pt(this.hass.states[this._config.entity]),k=null!==c.attributes.humidity&&Number.isFinite(Number(c.attributes.humidity))?c.attributes.humidity:c.attributes.min_humidity,V=function(t){return function(t){var e=t.locale.language||"en";return t.translationMetadata.translations[e]&&t.translationMetadata.translations[e].isRTL||!1}(t)?"rtl":"ltr"}(this.hass),M=Dt.includes(c.state)||b?I` 41 | 46 | `:I` 47 | 55 | 56 | `,L=j` 57 | 58 | 65 | ${this._targetHumidity&&!isNaN(this._targetHumidity)?j`${H?I`${null===(a=this._currentHumidity)||void 0===a?void 0:a.toFixed()}`:I`${this._targetHumidity.toFixed()}`} 66 | 67 | % 68 | 69 | `:""} 70 | 71 | 72 | `,T=j` 73 | 74 | 75 | 76 | ${Dt.includes(y)||void 0===this._currentHumidity||null===this._currentHumidity?"":j` 77 | ${H?null===(l=this._targetHumidity)||void 0===l?void 0:l.toFixed():this._currentHumidity.toFixed()}% - ${C?A:""}${w} 78 | `} 79 | 80 | 85 | ${this.hass.localize(`state.default.${c.state}`)} 86 | ${c.attributes.mode&&!Dt.includes(c.state)?I` 87 | - 88 | ${this.hass.localize(`state_attributes.humidifier.mode.${c.attributes.mode}`)||c.attributes.mode} 89 | `:""} 90 | 91 | 92 | 93 | `,z={tank:"on"===(null==_?void 0:_.state),defrost:"on"===(null==p?void 0:p.state),filter:"on"===(null==f?void 0:f.state)},P="on"===(null==_?void 0:_.state)||"on"===(null==p?void 0:p.state)||"on"===(null==f?void 0:f.state)||!1;return I` 94 | 97 | 104 |
105 |
106 |
107 | ${M} 108 |
109 |
${L} ${T}
110 |
111 |
112 |
113 |
114 |
115 | ${(c.attributes.available_modes||[]).concat().sort(Lt).map((t=>this._renderIcon(t,v,b)))} 116 | ${(null==z?void 0:z.defrost)?I``:I``} 122 |
123 |
124 | ${!0===(null===(h=this._config)||void 0===h?void 0:h.show_ion_toggle)?I``:I``} 131 | ${((null===(d=null==u?void 0:u.attributes)||void 0===d?void 0:d.preset_modes)||[]).concat().sort(Tt).map((t=>this._renderFanIcon(t,g,b)))} 132 | ${this._renderPowerIcon("off"===$?"on":"off",$)} 133 |
134 | ${E} 135 |
136 |
137 |
138 | ${P?this._getWarningText(z):I``} 139 |
140 |
141 | `}shouldUpdate(t){return function(t,e){if(function(t,e){if(e.has("_config"))return!0;const i=e.get("hass");return!i||i.connected!==t.hass.connected||i.themes!==t.hass.themes||i.locale!==t.hass.locale||i.localize!==t.hass.localize||i.config.state!==t.hass.config.state}(t,e))return!0;const i=e.get("hass");return Rt(t._config.entities,!1).some((e=>"entity"in e&&i.states[e.entity]!==t.hass.states[e.entity]))}(this,t)}updated(t){if(super.updated(t),!this._config||!this.hass||!t.has("hass")&&!t.has("_config"))return;const e=t.get("hass"),i=t.get("_config");e&&i&&e.themes===this.hass.themes&&i.theme===this._config.theme||function(t,e,i,n){void 0===n&&(n=!1),t._themes||(t._themes={});var s=e.default_theme;("default"===i||i&&e.themes[i])&&(s=i);var o=xt({},t._themes);if("default"!==s){var r=e.themes[s];Object.keys(r).forEach((function(e){var i="--"+e;t._themes[i]="",o[i]=r[e]}))}if(t.updateStyles?t.updateStyles(o):window.ShadyCSS&&window.ShadyCSS.styleSubtree(t,o),n){var a=document.querySelector("meta[name=theme-color]");if(a){a.hasAttribute("default-content")||a.setAttribute("default-content",a.getAttribute("content"));var l=o["--primary-color"]||a.getAttribute("default-content");a.setAttribute("content",l)}}}(this,this.hass.themes,this._config.theme);const n=this.hass.states[this._config.entity];if(!n)return;const s=this.hass.states[this._config.fan_entity];s&&(e&&e.states[this._config.entity]===n&&e.states[this._config.fan_entity]===s||this._rescale_svg())}willUpdate(t){if(!this.hass||!this._config||!t.has("hass"))return;const e=this.hass.states[this._config.entity];if(!e)return;const i=this.hass.states[this._config.humidity_entity];if(!i)return;const n=t.get("hass");n&&n.states[this._config.entity]===e||(this._targetHumidity=this._getTargetHumidity(e)),n&&n.states[this._config.humidity_entity]===e||(this._currentHumidity=this._getCurrentHumidity(i))}_rescale_svg(){this.shadowRoot&&this.shadowRoot.querySelector("ha-card")&&this.shadowRoot.querySelector("ha-card").updateComplete.then((()=>{const t=this.shadowRoot.querySelector("#set-values"),e=t.querySelector("g").getBBox();t.setAttribute("viewBox",`${e.x} ${e.y} ${e.width} ${e.height}`),t.setAttribute("width",`${e.width}`),t.setAttribute("height",`${e.height}`)}))}_getTargetHumidity(t){if(!Dt.includes(t.state))return t.attributes.humidity}_dragEvent(t){this._targetHumidity=t.detail.value}_setTargetHumidity(t){this.hass.callService("humidifier","set_humidity",{entity_id:this._config.entity,humidity:t.detail.value})}_getCurrentHumidity(t){if(!Dt.includes(t.state)&&!isNaN(parseFloat(t.state)))return parseFloat(t.state)}_handleMoreInfo(){kt(this,"hass-more-info",{entityId:this._config.entity})}static get styles(){return f` 142 | :host { 143 | display: block; 144 | } 145 | 146 | ha-card { 147 | height: 100%; 148 | position: relative; 149 | overflow: hidden; 150 | --name-font-size: 1.2rem; 151 | --brightness-font-size: 1.2rem; 152 | --rail-border-color: transparent; 153 | } 154 | .Off { 155 | --mode-color: var(--state-climate-idle-color); 156 | } 157 | .Dry { 158 | --mode-color: var(--state-climate-heat-color); 159 | } 160 | .Smart { 161 | --mode-color: var(--state-climate-cool-color); 162 | } 163 | .Continuous { 164 | --mode-color: var(--state-climate-eco-color); 165 | } 166 | .Set { 167 | --mode-color: var(--state-climate-manual-color); 168 | } 169 | .unknown-mode { 170 | --mode-color: var(--state-unknown-color); 171 | } 172 | .error { 173 | --mode-color: var(--error-color); 174 | } 175 | 176 | .more-info { 177 | position: absolute; 178 | cursor: pointer; 179 | top: 0; 180 | right: 0; 181 | border-radius: 100%; 182 | color: var(--secondary-text-color); 183 | z-index: 1; 184 | } 185 | 186 | .content { 187 | height: 100%; 188 | display: flex; 189 | flex-direction: column; 190 | justify-content: center; 191 | } 192 | 193 | #controls { 194 | display: flex; 195 | justify-content: center; 196 | padding: 16px; 197 | position: relative; 198 | } 199 | 200 | #slider { 201 | height: 100%; 202 | width: 100%; 203 | position: relative; 204 | max-width: 250px; 205 | min-width: 100px; 206 | } 207 | 208 | round-slider { 209 | --round-slider-path-color: var(--slider-track-color); 210 | --round-slider-bar-color: var(--mode-color); 211 | padding-bottom: 10%; 212 | } 213 | 214 | #slider-center { 215 | position: absolute; 216 | width: calc(100% - 40px); 217 | height: calc(100% - 40px); 218 | box-sizing: border-box; 219 | border-radius: 100%; 220 | left: 20px; 221 | top: 20px; 222 | text-align: center; 223 | overflow-wrap: break-word; 224 | pointer-events: none; 225 | } 226 | 227 | #humidity { 228 | position: absolute; 229 | transform: translate(-50%, -50%); 230 | width: 100%; 231 | height: 50%; 232 | top: 45%; 233 | left: 50%; 234 | } 235 | 236 | #set-values { 237 | max-width: 80%; 238 | transform: translate(0, -50%); 239 | font-size: 20px; 240 | } 241 | 242 | #set-mode { 243 | fill: var(--secondary-text-color); 244 | font-size: 16px; 245 | } 246 | 247 | #warnings { 248 | padding: 16px; 249 | } 250 | 251 | #info { 252 | display: flex-vertical; 253 | justify-content: center; 254 | text-align: center; 255 | padding: 16px; 256 | margin-top: -60px; 257 | font-size: var(--name-font-size); 258 | } 259 | 260 | #modes > * { 261 | color: var(--disabled-text-color); 262 | cursor: pointer; 263 | display: inline-block; 264 | } 265 | 266 | #modes .selected-icon { 267 | color: var(--mode-color); 268 | } 269 | 270 | #modes .defrost-icon { 271 | color: var(--state-climate-cool-color); 272 | } 273 | 274 | #modes .ion-icon { 275 | color: var(--mode-color); 276 | } 277 | 278 | text { 279 | fill: var(--primary-text-color); 280 | } 281 | `}};t([ht({attribute:!1})],Ft.prototype,"hass",void 0),t([dt()],Ft.prototype,"_config",void 0),t([dt()],Ft.prototype,"_targetHumidity",void 0),t([dt()],Ft.prototype,"_currentHumidity",void 0),Ft=t([at("midea-humidifier-card")],Ft);let qt=class extends rt{constructor(){super(...arguments),this._initialized=!1}setConfig(t){this._config=t,this.loadCardHelpers()}shouldUpdate(){return this._initialized||this._initialize(),!0}get _name(){var t;return(null===(t=this._config)||void 0===t?void 0:t.name)||""}get _entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.entity)||""}get _fan_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.fan_entity)||""}get _humidity_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.humidity_entity)||""}get _temperature_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.temperature_entity)||""}get _ion_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.ion_entity)||""}get _defrost_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.defrost_entity)||""}get _filter_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.filter_entity)||""}get _tank_entity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.tank_entity)||""}get _show_ion_toggle(){var t;return(null===(t=this._config)||void 0===t?void 0:t.show_ion_toggle)||!1}get _swap_target_and_current_humidity(){var t;return(null===(t=this._config)||void 0===t?void 0:t.swap_target_and_current_humidity)||!1}render(){if(!this.hass||!this._helpers)return I``;this._helpers.importMoreInfoControl("climate");const t=["sensor"],e=["binary_sensor"];return I` 282 |
283 | 292 | 301 | 310 | 319 | 328 | 337 | 346 | 355 | 356 | 361 | 362 | 363 | 368 | 369 |
370 | `}_initialize(){void 0!==this.hass&&void 0!==this._config&&void 0!==this._helpers&&(this._initialized=!0)}async loadCardHelpers(){this._helpers=await window.loadCardHelpers()}_valueChanged(t){if(!this._config||!this.hass)return;const e=t.target;if(this[`_${e.configValue}`]!==e.value){if(e.configValue)if(""===e.value){const t=Object.assign({},this._config);delete t[e.configValue],this._config=t}else this._config=Object.assign(Object.assign({},this._config),{[e.configValue]:void 0!==e.checked?e.checked:e.value});kt(this,"config-changed",{config:this._config})}}static get styles(){return f` 371 | .option { 372 | padding: 4px 0px; 373 | cursor: pointer; 374 | } 375 | .row { 376 | display: flex; 377 | margin-bottom: -14px; 378 | pointer-events: none; 379 | } 380 | .title { 381 | padding-left: 16px; 382 | margin-top: -6px; 383 | pointer-events: none; 384 | } 385 | .secondary { 386 | padding-left: 40px; 387 | color: var(--secondary-text-color); 388 | pointer-events: none; 389 | } 390 | .values { 391 | padding-left: 16px; 392 | background: var(--secondary-background-color); 393 | display: grid; 394 | } 395 | ha-formfield { 396 | padding-bottom: 8px; 397 | } 398 | `}};t([ht({attribute:!1})],qt.prototype,"hass",void 0),t([dt()],qt.prototype,"_config",void 0),t([dt()],qt.prototype,"_helpers",void 0),qt=t([at("midea-humidifier-card-editor")],qt);var Zt=Object.freeze({__proto__:null,get MideaHumidifierCardEditor(){return qt}});export{Ft as MideaHumidifierCard,Ot as computeObjectId,Pt as computeStateName,zt as findEntities,Ut as isValidEntityId,Rt as processConfigEntities}; 399 | --------------------------------------------------------------------------------