├── .hass_dev ├── scenes.yaml ├── scripts.yaml ├── automations.yaml ├── packages │ ├── demo.yaml │ ├── light.yaml │ ├── select.yaml │ ├── person.yaml │ ├── number.yaml │ ├── climate.yaml │ └── alarm_control_panel.yaml ├── www │ └── mushrooms.jpeg ├── lovelace.yaml ├── configuration.yaml ├── lovelace-mushroom-showcase.yaml └── views │ ├── title-view.yaml │ ├── lock-view.yaml │ ├── select-view.yaml │ ├── number-view.yaml │ └── humidifier-view.yaml ├── .gitattributes ├── src ├── cards │ ├── template-card │ │ └── const.ts │ ├── chips-card │ │ ├── const.ts │ │ └── chips │ │ │ ├── index.ts │ │ │ ├── spacer-chip.ts │ │ │ ├── back-chip-editor.ts │ │ │ ├── menu-chip-editor.ts │ │ │ ├── conditional-chip.ts │ │ │ ├── quickbar-chip-editor.ts │ │ │ ├── back-chip.ts │ │ │ └── menu-chip.ts │ ├── empty-card │ │ ├── const.ts │ │ ├── empty-card-config.ts │ │ ├── empty-card-editor.ts │ │ └── empty-card.ts │ ├── title-card │ │ ├── const.ts │ │ └── title-card-config.ts │ ├── entity-card │ │ ├── const.ts │ │ └── entity-card-config.ts │ ├── fan-card │ │ ├── const.ts │ │ ├── utils.ts │ │ ├── fan-card-config.ts │ │ └── controls │ │ │ ├── fan-direction-control.ts │ │ │ ├── fan-oscillate-control.ts │ │ │ └── fan-percentage-control.ts │ ├── lock-card │ │ ├── const.ts │ │ ├── utils.ts │ │ ├── lock-card-config.ts │ │ └── lock-card-editor.ts │ ├── cover-card │ │ ├── const.ts │ │ ├── utils.ts │ │ ├── cover-card-config.ts │ │ └── controls │ │ │ └── cover-position-control.ts │ ├── legacy-template-card │ │ ├── const.ts │ │ └── legacy-template-card-config.ts │ ├── light-card │ │ ├── const.ts │ │ ├── light-card-config.ts │ │ ├── utils.ts │ │ └── controls │ │ │ └── light-brightness-control.ts │ ├── vacuum-card │ │ ├── const.ts │ │ ├── utils.ts │ │ └── vacuum-card-config.ts │ ├── climate-card │ │ ├── const.ts │ │ ├── climate-card-config.ts │ │ ├── utils.ts │ │ └── controls │ │ │ └── climate-hvac-modes-control.ts │ ├── number-card │ │ ├── const.ts │ │ └── number-card-config.ts │ ├── person-card │ │ ├── const.ts │ │ ├── person-card-config.ts │ │ └── utils.ts │ ├── select-card │ │ ├── const.ts │ │ ├── utils.ts │ │ ├── select-card-config.ts │ │ └── controls │ │ │ └── select-option-control.ts │ ├── humidifier-card │ │ ├── const.ts │ │ ├── humidifier-card-config.ts │ │ └── controls │ │ │ └── humidifier-humidity-control.ts │ ├── media-player-card │ │ ├── const.ts │ │ ├── controls │ │ │ └── media-player-media-control.ts │ │ └── media-player-card-config.ts │ ├── update-card │ │ ├── utils.ts │ │ ├── const.ts │ │ └── update-card-config.ts │ └── alarm-control-panel-card │ │ ├── const.ts │ │ ├── alarm-control-panel-card-config.ts │ │ └── utils.ts ├── ha │ ├── common │ │ ├── entity │ │ │ ├── compute_state_display.ts │ │ │ ├── compute_domain.ts │ │ │ ├── compute_object_id.ts │ │ │ ├── compute_state_domain.ts │ │ │ ├── supports-feature.ts │ │ │ └── compute_state_name.ts │ │ ├── translations │ │ │ └── localize.ts │ │ ├── number │ │ │ ├── round.ts │ │ │ └── clamp.ts │ │ ├── dom │ │ │ └── get_main_window.ts │ │ ├── util │ │ │ ├── render-status.ts │ │ │ ├── debounce.ts │ │ │ └── compute_rtl.ts │ │ ├── string │ │ │ ├── has-template.ts │ │ │ └── compare.ts │ │ ├── const.ts │ │ ├── color │ │ │ ├── compute-color.ts │ │ │ └── hex.ts │ │ └── structs │ │ │ └── handle-errors.ts │ ├── data │ │ ├── main_window.ts │ │ ├── fan.ts │ │ ├── ws-templates.ts │ │ ├── ws-themes.ts │ │ ├── humidifier.ts │ │ ├── vacuum.ts │ │ ├── lock.ts │ │ ├── translation.ts │ │ ├── entity.ts │ │ ├── climate.ts │ │ └── cover.ts │ ├── panels │ │ └── lovelace │ │ │ ├── common │ │ │ ├── has-action.ts │ │ │ ├── validate-condition.ts │ │ │ ├── handle-actions.ts │ │ │ └── entity │ │ │ │ ├── turn-on-off-entity.ts │ │ │ │ └── turn-on-off-entities.ts │ │ │ ├── card-features │ │ │ └── types.ts │ │ │ └── types.ts │ ├── util.ts │ ├── index.ts │ └── resources │ │ └── ha-sortable-styles.ts ├── const.ts ├── shared │ ├── config │ │ ├── lovelace-badge-config.ts │ │ ├── entity-config.ts │ │ ├── lovelace-card-config.ts │ │ ├── actions-config.ts │ │ └── appearance-config.ts │ ├── badge-icon.ts │ ├── shape-avatar.ts │ ├── form │ │ └── mushroom-select.ts │ ├── button-group.ts │ ├── button.ts │ ├── state-info.ts │ ├── editor │ │ ├── icon-type-picker.ts │ │ ├── info-picker.ts │ │ ├── layout-picker.ts │ │ └── alignment-picker.ts │ └── state-item.ts ├── utils │ ├── layout.ts │ ├── lovelace │ │ ├── gui-support-error.ts │ │ ├── types.ts │ │ ├── chip-element-editor.ts │ │ └── chip │ │ │ └── chip-element.ts │ ├── form │ │ ├── generic-fields.ts │ │ └── custom │ │ │ ├── ha-selector-mushroom-color.ts │ │ │ ├── ha-selector-mushroom-layout.ts │ │ │ ├── ha-selector-mushroom-icon-type.ts │ │ │ ├── ha-selector-mushroom-alignment.ts │ │ │ └── ha-selector-mushroom-info.ts │ ├── cache-manager.ts │ ├── custom-cards.ts │ ├── custom-badges.ts │ ├── icons │ │ ├── weather-icon.ts │ │ └── cover-icon.ts │ ├── loader.ts │ ├── card-styles.ts │ ├── appearance.ts │ ├── base-element.ts │ ├── info.ts │ └── colors.ts ├── badges │ └── template │ │ └── template-badge-config.ts └── mushroom.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature_request.yml ├── FUNDING.yml ├── workflows │ ├── build.yml │ ├── validate.yaml │ └── release.yaml ├── release.yml └── PULL_REQUEST_TEMPLATE.md ├── .prettierrc.js ├── .browserslistrc ├── .prettierignore ├── docs ├── images │ ├── cover-dark.png │ ├── fan-dark.png │ ├── fan-light.png │ ├── light-dark.png │ ├── lock-dark.png │ ├── lock-light.png │ ├── title-dark.png │ ├── climate-dark.png │ ├── cover-light.png │ ├── editor-cards.png │ ├── editor-chips.png │ ├── entity-dark.png │ ├── entity-light.png │ ├── light-light.png │ ├── number-dark.png │ ├── number-light.png │ ├── person-dark.png │ ├── person-light.png │ ├── select-dark.png │ ├── select-light.png │ ├── title-light.png │ ├── update-dark.png │ ├── update-light.png │ ├── vacuum-dark.png │ ├── vacuum-light.png │ ├── chip-back-dark.png │ ├── chip-back-light.png │ ├── chip-light-dark.png │ ├── chip-menu-dark.png │ ├── chip-menu-light.png │ ├── climate-light.png │ ├── humidifier-dark.png │ ├── template-dark.png │ ├── template-light.png │ ├── chip-action-dark.png │ ├── chip-action-light.png │ ├── chip-entity-dark.png │ ├── chip-entity-light.png │ ├── chip-light-light.png │ ├── chip-spacer-dark.png │ ├── chip-spacer-light.png │ ├── chip-weather-dark.png │ ├── humidifier-light.png │ ├── media-player-dark.png │ ├── chip-quickbar-dark.png │ ├── chip-quickbar-light.png │ ├── chip-template-dark.png │ ├── chip-template-light.png │ ├── chip-weather-light.png │ ├── legacy-template-dark.png │ ├── media-player-light.png │ ├── template-badge-dark.png │ ├── template-badge-light.png │ ├── legacy-template-light.png │ ├── alarm-control-panel-dark.png │ ├── alarm-control-panel-light.png │ ├── chip-alarm-control-panel-dark.png │ └── chip-alarm-control-panel-light.png └── cards │ ├── empty.md │ └── title.md ├── hacs.json ├── .gitignore ├── tsconfig.json ├── rollup-plugins └── rollup-ignore-plugin.js ├── package.json └── rollup.config.mjs /.hass_dev/scenes.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.hass_dev/scripts.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.hass_dev/automations.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.hass_dev/packages/demo.yaml: -------------------------------------------------------------------------------- 1 | demo: -------------------------------------------------------------------------------- /src/cards/template-card/const.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ha/common/entity/compute_state_display.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const PREFIX_NAME = "mushroom"; 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | }; 4 | -------------------------------------------------------------------------------- /src/ha/data/main_window.ts: -------------------------------------------------------------------------------- 1 | export const MAIN_WINDOW_NAME = "ha-main-window"; 2 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | unreleased versions 2 | last 7 years 3 | >= 0.05% and supports websockets -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .hass_dev/* 4 | package-lock.json 5 | package.json -------------------------------------------------------------------------------- /docs/images/cover-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/cover-dark.png -------------------------------------------------------------------------------- /docs/images/fan-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/fan-dark.png -------------------------------------------------------------------------------- /docs/images/fan-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/fan-light.png -------------------------------------------------------------------------------- /docs/images/light-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/light-dark.png -------------------------------------------------------------------------------- /docs/images/lock-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/lock-dark.png -------------------------------------------------------------------------------- /docs/images/lock-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/lock-light.png -------------------------------------------------------------------------------- /docs/images/title-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/title-dark.png -------------------------------------------------------------------------------- /src/ha/common/translations/localize.ts: -------------------------------------------------------------------------------- 1 | export type LocalizeFunc = (key: string, ...args: any[]) => string; 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: piitaya 2 | buy_me_a_coffee: piitaya 3 | custom: ["https://www.paypal.me/pbottein"] 4 | -------------------------------------------------------------------------------- /.hass_dev/www/mushrooms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/.hass_dev/www/mushrooms.jpeg -------------------------------------------------------------------------------- /docs/images/climate-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/climate-dark.png -------------------------------------------------------------------------------- /docs/images/cover-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/cover-light.png -------------------------------------------------------------------------------- /docs/images/editor-cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/editor-cards.png -------------------------------------------------------------------------------- /docs/images/editor-chips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/editor-chips.png -------------------------------------------------------------------------------- /docs/images/entity-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/entity-dark.png -------------------------------------------------------------------------------- /docs/images/entity-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/entity-light.png -------------------------------------------------------------------------------- /docs/images/light-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/light-light.png -------------------------------------------------------------------------------- /docs/images/number-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/number-dark.png -------------------------------------------------------------------------------- /docs/images/number-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/number-light.png -------------------------------------------------------------------------------- /docs/images/person-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/person-dark.png -------------------------------------------------------------------------------- /docs/images/person-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/person-light.png -------------------------------------------------------------------------------- /docs/images/select-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/select-dark.png -------------------------------------------------------------------------------- /docs/images/select-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/select-light.png -------------------------------------------------------------------------------- /docs/images/title-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/title-light.png -------------------------------------------------------------------------------- /docs/images/update-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/update-dark.png -------------------------------------------------------------------------------- /docs/images/update-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/update-light.png -------------------------------------------------------------------------------- /docs/images/vacuum-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/vacuum-dark.png -------------------------------------------------------------------------------- /docs/images/vacuum-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/vacuum-light.png -------------------------------------------------------------------------------- /docs/images/chip-back-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-back-dark.png -------------------------------------------------------------------------------- /docs/images/chip-back-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-back-light.png -------------------------------------------------------------------------------- /docs/images/chip-light-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-light-dark.png -------------------------------------------------------------------------------- /docs/images/chip-menu-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-menu-dark.png -------------------------------------------------------------------------------- /docs/images/chip-menu-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-menu-light.png -------------------------------------------------------------------------------- /docs/images/climate-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/climate-light.png -------------------------------------------------------------------------------- /docs/images/humidifier-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/humidifier-dark.png -------------------------------------------------------------------------------- /docs/images/template-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/template-dark.png -------------------------------------------------------------------------------- /docs/images/template-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/template-light.png -------------------------------------------------------------------------------- /docs/images/chip-action-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-action-dark.png -------------------------------------------------------------------------------- /docs/images/chip-action-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-action-light.png -------------------------------------------------------------------------------- /docs/images/chip-entity-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-entity-dark.png -------------------------------------------------------------------------------- /docs/images/chip-entity-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-entity-light.png -------------------------------------------------------------------------------- /docs/images/chip-light-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-light-light.png -------------------------------------------------------------------------------- /docs/images/chip-spacer-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-spacer-dark.png -------------------------------------------------------------------------------- /docs/images/chip-spacer-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-spacer-light.png -------------------------------------------------------------------------------- /docs/images/chip-weather-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-weather-dark.png -------------------------------------------------------------------------------- /docs/images/humidifier-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/humidifier-light.png -------------------------------------------------------------------------------- /docs/images/media-player-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/media-player-dark.png -------------------------------------------------------------------------------- /docs/images/chip-quickbar-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-quickbar-dark.png -------------------------------------------------------------------------------- /docs/images/chip-quickbar-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-quickbar-light.png -------------------------------------------------------------------------------- /docs/images/chip-template-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-template-dark.png -------------------------------------------------------------------------------- /docs/images/chip-template-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-template-light.png -------------------------------------------------------------------------------- /docs/images/chip-weather-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-weather-light.png -------------------------------------------------------------------------------- /docs/images/legacy-template-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/legacy-template-dark.png -------------------------------------------------------------------------------- /docs/images/media-player-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/media-player-light.png -------------------------------------------------------------------------------- /docs/images/template-badge-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/template-badge-dark.png -------------------------------------------------------------------------------- /docs/images/template-badge-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/template-badge-light.png -------------------------------------------------------------------------------- /docs/images/legacy-template-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/legacy-template-light.png -------------------------------------------------------------------------------- /.hass_dev/packages/light.yaml: -------------------------------------------------------------------------------- 1 | light: 2 | - platform: switch 3 | name: Christmas Tree Lights 4 | entity_id: switch.decorative_lights -------------------------------------------------------------------------------- /docs/images/alarm-control-panel-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/alarm-control-panel-dark.png -------------------------------------------------------------------------------- /docs/images/alarm-control-panel-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/alarm-control-panel-light.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mushroom", 3 | "filename": "mushroom.js", 4 | "homeassistant": "2025.6", 5 | "render_readme": true 6 | } 7 | -------------------------------------------------------------------------------- /docs/images/chip-alarm-control-panel-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-alarm-control-panel-dark.png -------------------------------------------------------------------------------- /docs/images/chip-alarm-control-panel-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piitaya/lovelace-mushroom/HEAD/docs/images/chip-alarm-control-panel-light.png -------------------------------------------------------------------------------- /src/ha/common/entity/compute_domain.ts: -------------------------------------------------------------------------------- 1 | export const computeDomain = (entityId: string): string => 2 | entityId.substr(0, entityId.indexOf(".")); 3 | -------------------------------------------------------------------------------- /src/ha/common/number/round.ts: -------------------------------------------------------------------------------- 1 | export const round = (value: number, precision = 2): number => 2 | Math.round(value * 10 ** precision) / 10 ** precision; 3 | -------------------------------------------------------------------------------- /src/ha/data/fan.ts: -------------------------------------------------------------------------------- 1 | export const FAN_SUPPORT_SET_SPEED = 1; 2 | export const FAN_SUPPORT_OSCILLATE = 2; 3 | export const FAN_SUPPORT_DIRECTION = 4; 4 | export const FAN_SUPPORT_PRESET_MODE = 8; 5 | -------------------------------------------------------------------------------- /src/ha/common/entity/compute_object_id.ts: -------------------------------------------------------------------------------- 1 | /** Compute the object ID of a state. */ 2 | export const computeObjectId = (entityId: string): string => 3 | entityId.substr(entityId.indexOf(".") + 1); 4 | -------------------------------------------------------------------------------- /src/shared/config/lovelace-badge-config.ts: -------------------------------------------------------------------------------- 1 | import { any, object, string } from "superstruct"; 2 | 3 | export const lovelaceBadgeConfigStruct = object({ 4 | type: string(), 5 | visibility: any(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/cards/chips-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const CHIPS_CARD_NAME = `${PREFIX_NAME}-chips-card`; 4 | export const CHIPS_CARD_EDITOR_NAME = `${CHIPS_CARD_NAME}-editor`; 5 | -------------------------------------------------------------------------------- /src/cards/empty-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const EMPTY_CARD_NAME = `${PREFIX_NAME}-empty-card`; 4 | export const EMPTY_CARD_EDITOR_NAME = `${EMPTY_CARD_NAME}-editor`; 5 | -------------------------------------------------------------------------------- /src/cards/title-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const TITLE_CARD_NAME = `${PREFIX_NAME}-title-card`; 4 | export const TITLE_CARD_EDITOR_NAME = `${TITLE_CARD_NAME}-editor`; 5 | -------------------------------------------------------------------------------- /src/cards/entity-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const ENTITY_CARD_NAME = `${PREFIX_NAME}-entity-card`; 4 | export const ENTITY_CARD_EDITOR_NAME = `${ENTITY_CARD_NAME}-editor`; 5 | -------------------------------------------------------------------------------- /docs/cards/empty.md: -------------------------------------------------------------------------------- 1 | # Empty card 2 | 3 | ## Description 4 | 5 | A empty card can be used as placeholder on you dashboards and grid cards. 6 | 7 | ## Configuration variables 8 | 9 | This card has no config options. 10 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/common/has-action.ts: -------------------------------------------------------------------------------- 1 | import { ActionConfig } from "../../../data/lovelace"; 2 | 3 | export function hasAction(config?: ActionConfig): boolean { 4 | return config !== undefined && config.action !== "none"; 5 | } 6 | -------------------------------------------------------------------------------- /.hass_dev/lovelace.yaml: -------------------------------------------------------------------------------- 1 | mode: storage 2 | dashboards: 3 | lovelace-mushroom-showcase: 4 | mode: yaml 5 | title: Showcase 6 | icon: mdi:mushroom 7 | show_in_sidebar: true 8 | filename: lovelace-mushroom-showcase.yaml -------------------------------------------------------------------------------- /src/cards/fan-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const FAN_CARD_NAME = `${PREFIX_NAME}-fan-card`; 4 | export const FAN_CARD_EDITOR_NAME = `${FAN_CARD_NAME}-editor`; 5 | export const FAN_ENTITY_DOMAINS = ["fan"]; 6 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/card-features/types.ts: -------------------------------------------------------------------------------- 1 | export type LovelaceCardFeatureConfig = { 2 | type: string; 3 | } & Record; 4 | 5 | export interface LovelaceCardFeatureContext { 6 | entity_id?: string; 7 | area_id?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/lock-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const LOCK_CARD_NAME = `${PREFIX_NAME}-lock-card`; 4 | export const LOCK_CARD_EDITOR_NAME = `${LOCK_CARD_NAME}-editor`; 5 | export const LOCK_ENTITY_DOMAINS = ["lock"]; 6 | -------------------------------------------------------------------------------- /src/cards/cover-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const COVER_CARD_NAME = `${PREFIX_NAME}-cover-card`; 4 | export const COVER_CARD_EDITOR_NAME = `${COVER_CARD_NAME}-editor`; 5 | export const COVER_ENTITY_DOMAINS = ["cover"]; 6 | -------------------------------------------------------------------------------- /src/cards/legacy-template-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const LEGACY_TEMPLATE_CARD_NAME = `${PREFIX_NAME}-legacy-template-card`; 4 | export const LEGACY_TEMPLATE_CARD_EDITOR_NAME = `${LEGACY_TEMPLATE_CARD_NAME}-editor`; 5 | -------------------------------------------------------------------------------- /src/cards/light-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const LIGHT_CARD_NAME = `${PREFIX_NAME}-light-card`; 4 | export const LIGHT_CARD_EDITOR_NAME = `${LIGHT_CARD_NAME}-editor`; 5 | export const LIGHT_ENTITY_DOMAINS = ["light"]; 6 | -------------------------------------------------------------------------------- /src/ha/common/dom/get_main_window.ts: -------------------------------------------------------------------------------- 1 | import { MAIN_WINDOW_NAME } from "../../data/main_window"; 2 | 3 | export const mainWindow = 4 | window.name === MAIN_WINDOW_NAME 5 | ? window 6 | : parent.name === MAIN_WINDOW_NAME 7 | ? parent 8 | : top!; 9 | -------------------------------------------------------------------------------- /src/cards/vacuum-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const VACUUM_CARD_NAME = `${PREFIX_NAME}-vacuum-card`; 4 | export const VACUUM_CARD_EDITOR_NAME = `${VACUUM_CARD_NAME}-editor`; 5 | export const VACUUM_ENTITY_DOMAINS = ["vacuum"]; 6 | -------------------------------------------------------------------------------- /src/ha/common/entity/compute_state_domain.ts: -------------------------------------------------------------------------------- 1 | import type { HassEntity } from "home-assistant-js-websocket"; 2 | import { computeDomain } from "./compute_domain"; 3 | 4 | export const computeStateDomain = (stateObj: HassEntity) => 5 | computeDomain(stateObj.entity_id); 6 | -------------------------------------------------------------------------------- /src/cards/climate-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const CLIMATE_CARD_NAME = `${PREFIX_NAME}-climate-card`; 4 | export const CLIMATE_CARD_EDITOR_NAME = `${CLIMATE_CARD_NAME}-editor`; 5 | export const CLIMATE_ENTITY_DOMAINS = ["climate"]; 6 | -------------------------------------------------------------------------------- /src/utils/layout.ts: -------------------------------------------------------------------------------- 1 | import { literal, union } from "superstruct"; 2 | 3 | export type Layout = "vertical" | "horizontal" | "default"; 4 | 5 | export const layoutStruct = union([ 6 | literal("horizontal"), 7 | literal("vertical"), 8 | literal("default"), 9 | ]); 10 | -------------------------------------------------------------------------------- /src/cards/number-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const NUMBER_CARD_NAME = `${PREFIX_NAME}-number-card`; 4 | export const NUMBER_CARD_EDITOR_NAME = `${NUMBER_CARD_NAME}-editor`; 5 | export const NUMBER_ENTITY_DOMAINS = ["number", "input_number"]; 6 | -------------------------------------------------------------------------------- /src/cards/person-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const PERSON_CARD_NAME = `${PREFIX_NAME}-person-card`; 4 | export const PERSON_CARD_EDITOR_NAME = `${PERSON_CARD_NAME}-editor`; 5 | export const PERSON_ENTITY_DOMAINS = ["person", "device_tracker"]; 6 | -------------------------------------------------------------------------------- /src/cards/select-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const SELECT_CARD_NAME = `${PREFIX_NAME}-select-card`; 4 | export const SELECT_CARD_EDITOR_NAME = `${SELECT_CARD_NAME}-editor`; 5 | export const SELECT_ENTITY_DOMAINS = ["input_select", "select"]; 6 | -------------------------------------------------------------------------------- /src/ha/common/util/render-status.ts: -------------------------------------------------------------------------------- 1 | export const afterNextRender = (cb: (value: unknown) => void): void => { 2 | requestAnimationFrame(() => setTimeout(cb, 0)); 3 | }; 4 | 5 | export const nextRender = () => 6 | new Promise((resolve) => { 7 | afterNextRender(resolve); 8 | }); 9 | -------------------------------------------------------------------------------- /src/cards/humidifier-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const HUMIDIFIER_CARD_NAME = `${PREFIX_NAME}-humidifier-card`; 4 | export const HUMIDIFIER_CARD_EDITOR_NAME = `${HUMIDIFIER_CARD_NAME}-editor`; 5 | export const HUMIDIFIER_ENTITY_DOMAINS = ["humidifier"]; 6 | -------------------------------------------------------------------------------- /src/cards/empty-card/empty-card-config.ts: -------------------------------------------------------------------------------- 1 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | 4 | export type EmptyCardConfig = LovelaceCardConfig 5 | 6 | export const emptyCardConfigStruct = lovelaceCardConfigStruct; 7 | -------------------------------------------------------------------------------- /src/cards/media-player-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const MEDIA_PLAYER_CARD_NAME = `${PREFIX_NAME}-media-player-card`; 4 | export const MEDIA_PLAYER_CARD_EDITOR_NAME = `${MEDIA_PLAYER_CARD_NAME}-editor`; 5 | export const MEDIA_PLAYER_ENTITY_DOMAINS = ["media_player"]; 6 | -------------------------------------------------------------------------------- /.hass_dev/packages/select.yaml: -------------------------------------------------------------------------------- 1 | input_select: 2 | who_cooks: 3 | name: Who cooks today 4 | options: 5 | - Paulus 6 | - Anne Therese 7 | initial: Anne Therese 8 | icon: mdi:panda 9 | living_room_preset: 10 | options: 11 | - Visitors 12 | - Visitors with kids 13 | - Home Alone -------------------------------------------------------------------------------- /src/cards/select-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | 3 | export function getCurrentOption(stateObj: HassEntity) { 4 | return stateObj.state != null ? stateObj.state : undefined; 5 | } 6 | 7 | export function getOptions(stateObj: HassEntity) { 8 | return stateObj.attributes.options; 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/config/entity-config.ts: -------------------------------------------------------------------------------- 1 | import { Infer, object, optional, string } from "superstruct"; 2 | 3 | export const entitySharedConfigStruct = object({ 4 | entity: optional(string()), 5 | name: optional(string()), 6 | icon: optional(string()), 7 | }); 8 | 9 | export type EntitySharedConfig = Infer; 10 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/index.ts: -------------------------------------------------------------------------------- 1 | import "./entity-chip"; 2 | import "./weather-chip"; 3 | import "./back-chip"; 4 | import "./action-chip"; 5 | import "./menu-chip"; 6 | import "./quickbar-chip"; 7 | import "./template-chip"; 8 | import "./conditional-chip"; 9 | import "./light-chip"; 10 | import "./alarm-control-panel-chip"; 11 | import "./spacer-chip"; 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Install 12 | run: npm ci 13 | - name: Build 14 | run: npm run build 15 | -------------------------------------------------------------------------------- /src/shared/config/lovelace-card-config.ts: -------------------------------------------------------------------------------- 1 | import { any, number, object, optional, string } from "superstruct"; 2 | 3 | export const lovelaceCardConfigStruct = object({ 4 | index: optional(number()), 5 | view_index: optional(number()), 6 | view_layout: any(), 7 | type: string(), 8 | layout_options: any(), 9 | grid_options: any(), 10 | visibility: any(), 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/lovelace/gui-support-error.ts: -------------------------------------------------------------------------------- 1 | export class GUISupportError extends Error { 2 | public warnings?: string[]; 3 | 4 | public errors?: string[]; 5 | 6 | constructor(message: string, warnings?: string[], errors?: string[]) { 7 | super(message); 8 | this.name = "GUISupportError"; 9 | this.warnings = warnings; 10 | this.errors = errors; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/form/generic-fields.ts: -------------------------------------------------------------------------------- 1 | export const GENERIC_LABELS = [ 2 | "color", 3 | "icon_color", 4 | "layout", 5 | "fill_container", 6 | "primary_info", 7 | "secondary_info", 8 | "icon_type", 9 | "content_info", 10 | "use_entity_picture", 11 | "collapsible_controls", 12 | "icon_animation", 13 | "picture", 14 | ]; 15 | 16 | export const GENERIC_HELPERS = ["picture"]; 17 | -------------------------------------------------------------------------------- /src/cards/update-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_CARD_DEFAULT_STATE_COLOR, 3 | UPDATE_CARD_STATE_COLOR, 4 | } from "./const"; 5 | 6 | export function getStateColor(state: string, isInstalling: boolean): string { 7 | if (isInstalling) { 8 | return UPDATE_CARD_STATE_COLOR["installing"]; 9 | } else { 10 | return UPDATE_CARD_STATE_COLOR[state] || UPDATE_CARD_DEFAULT_STATE_COLOR; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: HACS validation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | validate: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: HACS validation 13 | uses: "hacs/action@main" 14 | with: 15 | category: "plugin" 16 | -------------------------------------------------------------------------------- /.hass_dev/packages/person.yaml: -------------------------------------------------------------------------------- 1 | person: 2 | - name: Anne Therese 3 | id: anne_therese 4 | device_trackers: 5 | - device_tracker.demo_anne_therese 6 | 7 | zone: 8 | - name: Office 9 | latitude: 52.37451608362128 10 | longitude: 4.888106097860146 11 | radius: 50 12 | icon: mdi:office-building 13 | 14 | homeassistant: 15 | customize: 16 | person.anne_therese: 17 | entity_picture: "/local/mushrooms.jpeg" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .hass_dev/* 4 | !.hass_dev/configuration.yaml 5 | !.hass_dev/lovelace.yaml 6 | !.hass_dev/lovelace-mushroom-showcase.yaml 7 | !.hass_dev/lovelace.yaml 8 | !.hass_dev/automations.yaml 9 | !.hass_dev/scripts.yaml 10 | !.hass_dev/scenes.yaml 11 | !.hass_dev/views 12 | !.hass_dev/www 13 | !.hass_dev/packages 14 | .hass_dev/www/community/* 15 | 16 | # Mac OS 17 | .DS_Store 18 | .vscode/settings.json 19 | .idea 20 | -------------------------------------------------------------------------------- /src/ha/common/number/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (value: number, min: number, max: number) => 2 | Math.min(Math.max(value, min), max); 3 | 4 | // Variant that only applies the clamping to a border if the border is defined 5 | export const conditionalClamp = (value: number, min?: number, max?: number) => { 6 | let result: number; 7 | result = min ? Math.max(value, min) : value; 8 | result = max ? Math.min(result, max) : result; 9 | return result; 10 | }; 11 | -------------------------------------------------------------------------------- /.hass_dev/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | homeassistant: 4 | packages: !include_dir_named packages/ 5 | 6 | tts: 7 | - platform: google_translate 8 | 9 | automation: !include automations.yaml 10 | script: !include scripts.yaml 11 | scene: !include scenes.yaml 12 | 13 | lovelace: !include lovelace.yaml 14 | 15 | frontend: 16 | themes: !include_dir_merge_named themes 17 | extra_module_url: 18 | - http://localhost:4000/mushroom.js 19 | # development_repo: /workspaces/home-assistant-frontend -------------------------------------------------------------------------------- /src/ha/common/entity/supports-feature.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | 3 | export const supportsFeature = ( 4 | stateObj: HassEntity, 5 | feature: number 6 | ): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature); 7 | 8 | export const supportsFeatureFromAttributes = ( 9 | attributes: { 10 | [key: string]: any; 11 | }, 12 | feature: number 13 | ): boolean => 14 | // eslint-disable-next-line no-bitwise 15 | (attributes.supported_features! & feature) !== 0; 16 | -------------------------------------------------------------------------------- /src/cards/update-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const UPDATE_CARD_NAME = `${PREFIX_NAME}-update-card`; 4 | export const UPDATE_CARD_EDITOR_NAME = `${UPDATE_CARD_NAME}-editor`; 5 | export const UPDATE_ENTITY_DOMAINS = ["update"]; 6 | 7 | export const UPDATE_CARD_DEFAULT_STATE_COLOR = "var(--rgb-grey)"; 8 | 9 | export const UPDATE_CARD_STATE_COLOR = { 10 | on: "var(--rgb-state-update-on)", 11 | off: "var(--rgb-state-update-off)", 12 | installing: "var(--rgb-state-update-installing)", 13 | }; 14 | -------------------------------------------------------------------------------- /src/ha/common/string/has-template.ts: -------------------------------------------------------------------------------- 1 | const isTemplateRegex = /{%|{{/; 2 | 3 | export const isTemplate = (value: string): boolean => 4 | isTemplateRegex.test(value); 5 | 6 | export const hasTemplate = (value: unknown): boolean => { 7 | if (!value) { 8 | return false; 9 | } 10 | if (typeof value === "string") { 11 | return isTemplate(value); 12 | } 13 | if (typeof value === "object") { 14 | const values = Array.isArray(value) ? value : Object.values(value!); 15 | return values.some((val) => val && hasTemplate(val)); 16 | } 17 | return false; 18 | }; 19 | -------------------------------------------------------------------------------- /src/ha/common/const.ts: -------------------------------------------------------------------------------- 1 | /** States that we consider "off". */ 2 | export const STATES_OFF = ["closed", "locked", "off"]; 3 | 4 | /** Binary States */ 5 | export const BINARY_STATE_ON = "on"; 6 | export const BINARY_STATE_OFF = "off"; 7 | 8 | /** Temperature units. */ 9 | export const UNIT_C = "°C"; 10 | export const UNIT_F = "°F"; 11 | 12 | /** Domains where we allow toggle in Lovelace. */ 13 | export const DOMAINS_TOGGLE = new Set([ 14 | "fan", 15 | "input_boolean", 16 | "light", 17 | "switch", 18 | "group", 19 | "automation", 20 | "humidifier", 21 | "valve", 22 | ]); 23 | -------------------------------------------------------------------------------- /src/ha/util.ts: -------------------------------------------------------------------------------- 1 | export const atLeastHaVersion = ( 2 | version: string, 3 | major: number, 4 | minor: number, 5 | patch?: number 6 | ): boolean => { 7 | const [haMajor, haMinor, haPatch] = version.split(".", 3); 8 | 9 | return ( 10 | Number(haMajor) > major || 11 | (Number(haMajor) === major && 12 | (patch === undefined 13 | ? Number(haMinor) >= minor 14 | : Number(haMinor) > minor)) || 15 | (patch !== undefined && 16 | Number(haMajor) === major && 17 | Number(haMinor) === minor && 18 | Number(haPatch) >= patch) 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/ha/common/entity/compute_state_name.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { computeObjectId } from "./compute_object_id"; 3 | 4 | export const computeStateNameFromEntityAttributes = ( 5 | entityId: string, 6 | attributes: { [key: string]: any } 7 | ): string => 8 | attributes.friendly_name === undefined 9 | ? computeObjectId(entityId).replace(/_/g, " ") 10 | : attributes.friendly_name || ""; 11 | 12 | export const computeStateName = (stateObj: HassEntity): string => 13 | computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); 14 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: Breaking Changes 🛠 9 | labels: 10 | - breaking-change 11 | - title: New Features 🎉 12 | labels: 13 | - enhancement 14 | - title: Fixes 🐛 15 | labels: 16 | - bug 17 | - title: Translations 🌍 18 | labels: 19 | - translations 20 | - title: Other Changes 21 | labels: 22 | - "*" 23 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/spacer-chip.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, LitElement } from "lit"; 2 | import { customElement } from "lit/decorators.js"; 3 | import { LovelaceChip } from "../../../utils/lovelace/chip/types"; 4 | import { computeChipComponentName } from "../../../utils/lovelace/chip/chip-element"; 5 | 6 | @customElement(computeChipComponentName("spacer")) 7 | export class SpacerChip extends LitElement implements LovelaceChip { 8 | public setConfig(): void {} 9 | 10 | static get styles(): CSSResultGroup { 11 | return css` 12 | :host { 13 | flex-grow: 1; 14 | } 15 | `; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es2017", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "lib": ["es2017", "dom", "dom.iterable"], 8 | "noEmit": true, 9 | "noUnusedParameters": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "experimentalDecorators": true, 17 | "outDir": "dist", 18 | "rootDir": "." 19 | }, 20 | "include": ["**/*.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/cache-manager.ts: -------------------------------------------------------------------------------- 1 | export class CacheManager { 2 | constructor(expiration?: number) { 3 | this._expiration = expiration; 4 | } 5 | 6 | private _expiration?: number; 7 | 8 | private _cache = new Map(); 9 | 10 | public get(key: string): T | undefined { 11 | return this._cache.get(key); 12 | } 13 | 14 | public set(key: string, value: T): void { 15 | this._cache.set(key, value); 16 | if (this._expiration) { 17 | window.setTimeout(() => this._cache.delete(key), this._expiration); 18 | } 19 | } 20 | 21 | public has(key: string): boolean { 22 | return this._cache.has(key); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/cards/lock-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LockEntity, 3 | LOCK_STATE_LOCKED, 4 | LOCK_STATE_LOCKING, 5 | LOCK_STATE_UNLOCKED, 6 | LOCK_STATE_UNLOCKING, 7 | } from "../../ha"; 8 | 9 | export function isUnlocked(entity: LockEntity) { 10 | return entity.state === LOCK_STATE_UNLOCKED; 11 | } 12 | 13 | export function isLocked(entity: LockEntity) { 14 | return entity.state === LOCK_STATE_LOCKED; 15 | } 16 | 17 | export function isActionPending(entity: LockEntity) { 18 | switch (entity.state) { 19 | case LOCK_STATE_LOCKING: 20 | case LOCK_STATE_UNLOCKING: 21 | return true; 22 | default: 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/cards/fan-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | 3 | export function getPercentage(stateObj: HassEntity) { 4 | return stateObj.attributes.percentage != null 5 | ? Math.round(stateObj.attributes.percentage) 6 | : undefined; 7 | } 8 | 9 | export function isOscillating(stateObj: HassEntity) { 10 | return stateObj.attributes.oscillating != null 11 | ? Boolean(stateObj.attributes.oscillating) 12 | : false; 13 | } 14 | 15 | export function computePercentageStep(stateObj: HassEntity) { 16 | if (stateObj.attributes.percentage_step) { 17 | return stateObj.attributes.percentage_step; 18 | } 19 | return 1; 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Install 13 | run: npm ci 14 | - name: Build 15 | run: npm run build 16 | - name: Release 17 | uses: softprops/action-gh-release@v1 18 | if: startsWith(github.ref, 'refs/tags/') 19 | with: 20 | draft: true 21 | generate_release_notes: true 22 | files: dist/*.js 23 | -------------------------------------------------------------------------------- /src/utils/custom-cards.ts: -------------------------------------------------------------------------------- 1 | import { repository } from "../../package.json"; 2 | 3 | interface RegisterCardParams { 4 | type: string; 5 | name: string; 6 | description: string; 7 | } 8 | export function registerCustomCard(params: RegisterCardParams) { 9 | const windowWithCards = window as unknown as Window & { 10 | customCards: unknown[]; 11 | }; 12 | windowWithCards.customCards = windowWithCards.customCards || []; 13 | 14 | const cardPage = params.type.replace("-card", "").replace("mushroom-", ""); 15 | windowWithCards.customCards.push({ 16 | ...params, 17 | preview: true, 18 | documentationURL: `${repository.url}/blob/main/docs/cards/${cardPage}.md`, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /rollup-plugins/rollup-ignore-plugin.js: -------------------------------------------------------------------------------- 1 | export default function (userOptions = {}) { 2 | // Files need to be absolute paths. 3 | // This only works if the file has no exports 4 | // and only is imported for its side effects 5 | const files = userOptions.files || []; 6 | 7 | if (files.length === 0) { 8 | return { 9 | name: "ignore", 10 | }; 11 | } 12 | 13 | return { 14 | name: "ignore", 15 | 16 | load(id) { 17 | return files.some((toIgnorePath) => id.startsWith(toIgnorePath)) 18 | ? { 19 | code: "", 20 | } 21 | : null; 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/lovelace/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Condition, 3 | HomeAssistant, 4 | LovelaceCardConfig, 5 | LovelaceConfig, 6 | } from "../../ha"; 7 | import { LovelaceChipConfig } from "./chip/types"; 8 | 9 | export interface LovelaceChipEditor extends LovelaceGenericElementEditor { 10 | setConfig(config: LovelaceChipConfig): void; 11 | } 12 | 13 | export interface LovelaceGenericElementEditor extends HTMLElement { 14 | hass?: HomeAssistant; 15 | lovelace?: LovelaceConfig; 16 | setConfig(config: any): void; 17 | focusYamlEditor?: () => void; 18 | } 19 | 20 | export interface ConditionalCardConfig extends LovelaceCardConfig { 21 | card: LovelaceCardConfig; 22 | conditions: Condition[]; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/custom-badges.ts: -------------------------------------------------------------------------------- 1 | import { repository } from "../../package.json"; 2 | 3 | interface RegisterBadgeParams { 4 | type: string; 5 | name: string; 6 | description: string; 7 | } 8 | export function registerCustomBadge(params: RegisterBadgeParams) { 9 | const windowWithBadges = window as unknown as Window & { 10 | customBadges: unknown[]; 11 | }; 12 | windowWithBadges.customBadges = windowWithBadges.customBadges || []; 13 | 14 | const badgePage = params.type.replace("-badge", "").replace("mushroom-", ""); 15 | windowWithBadges.customBadges.push({ 16 | ...params, 17 | preview: true, 18 | documentationURL: `${repository.url}/blob/main/docs/badges/${badgePage}.md`, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/icons/weather-icon.ts: -------------------------------------------------------------------------------- 1 | import { getWeatherStateSVG } from "../weather"; 2 | 3 | export const weatherSVGs = new Set([ 4 | "clear-night", 5 | "cloudy", 6 | "fog", 7 | "lightning", 8 | "lightning-rainy", 9 | "partlycloudy", 10 | "pouring", 11 | "rainy", 12 | "hail", 13 | "snowy", 14 | "snowy-rainy", 15 | "sunny", 16 | "windy", 17 | "windy-variant", 18 | ]); 19 | 20 | export const getWeatherSvgIcon = (icon?: string) => { 21 | if (!icon || !icon.startsWith("weather-")) { 22 | return undefined; 23 | } 24 | const name = icon.replace("weather-", ""); 25 | if (!weatherSVGs.has(name)) { 26 | return undefined; 27 | } 28 | return getWeatherStateSVG(name, true); 29 | }; 30 | -------------------------------------------------------------------------------- /.hass_dev/lovelace-mushroom-showcase.yaml: -------------------------------------------------------------------------------- 1 | title: Mushroom Showcase 2 | views: 3 | - !include views/light-view.yaml 4 | - !include views/cover-view.yaml 5 | - !include views/person-view.yaml 6 | - !include views/alarm-control-panel-view.yaml 7 | - !include views/fan-view.yaml 8 | - !include views/template-view.yaml 9 | - !include views/entity-view.yaml 10 | - !include views/chips-view.yaml 11 | - !include views/title-view.yaml 12 | - !include views/update-view.yaml 13 | - !include views/media-player-view.yaml 14 | - !include views/vacuum-view.yaml 15 | - !include views/lock-view.yaml 16 | - !include views/humidifier-view.yaml 17 | - !include views/select-view.yaml 18 | - !include views/number-view.yaml -------------------------------------------------------------------------------- /src/utils/icons/cover-icon.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | 3 | export const computeOpenIcon = (stateObj: HassEntity): string => { 4 | switch (stateObj.attributes.device_class) { 5 | case "awning": 6 | case "curtain": 7 | case "door": 8 | case "gate": 9 | return "mdi:arrow-expand-horizontal"; 10 | default: 11 | return "mdi:arrow-up"; 12 | } 13 | }; 14 | 15 | export const computeCloseIcon = (stateObj: HassEntity): string => { 16 | switch (stateObj.attributes.device_class) { 17 | case "awning": 18 | case "curtain": 19 | case "door": 20 | case "gate": 21 | return "mdi:arrow-collapse-horizontal"; 22 | default: 23 | return "mdi:arrow-down"; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/cards/cover-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { CoverEntity } from "../../ha"; 2 | 3 | export function getPosition(entity: CoverEntity) { 4 | return entity.attributes.current_position != null 5 | ? Math.round(entity.attributes.current_position) 6 | : undefined; 7 | } 8 | 9 | export function getTiltPosition(entity: CoverEntity) { 10 | return entity.attributes.current_tilt_position != null 11 | ? Math.round(entity.attributes.current_tilt_position) 12 | : undefined; 13 | } 14 | 15 | export function getStateColor(entity: CoverEntity) { 16 | const state = entity.state; 17 | if (state === "open" || state === "opening") { 18 | return "var(--rgb-state-cover-open)"; 19 | } 20 | if (state === "closed" || state === "closing") { 21 | return "var(--rgb-state-cover-closed)"; 22 | } 23 | return "var(--rgb-disabled)"; 24 | } 25 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/common/validate-condition.ts: -------------------------------------------------------------------------------- 1 | import { UNAVAILABLE } from "../../../data/entity"; 2 | import { HomeAssistant } from "../../../types"; 3 | 4 | export interface Condition { 5 | entity: string; 6 | state?: string; 7 | state_not?: string; 8 | } 9 | 10 | export function checkConditionsMet( 11 | conditions: Condition[], 12 | hass: HomeAssistant 13 | ): boolean { 14 | return conditions.every((c) => { 15 | const state = hass.states[c.entity] 16 | ? hass!.states[c.entity]?.state 17 | : UNAVAILABLE; 18 | 19 | return c.state ? state === c.state : state !== c.state_not; 20 | }); 21 | } 22 | 23 | export function validateConditionalConfig(conditions: Condition[]): boolean { 24 | return conditions.every( 25 | (c) => (c.entity && (c.state || c.state_not)) as unknown as boolean 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/ha/data/ws-templates.ts: -------------------------------------------------------------------------------- 1 | import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; 2 | 3 | export interface RenderTemplateResult { 4 | result: string; 5 | listeners: TemplateListeners; 6 | } 7 | 8 | interface TemplateListeners { 9 | all: boolean; 10 | domains: string[]; 11 | entities: string[]; 12 | time: boolean; 13 | } 14 | 15 | export const subscribeRenderTemplate = ( 16 | conn: Connection, 17 | onChange: (result: RenderTemplateResult) => void, 18 | params: { 19 | template: string; 20 | entity_ids?: string | string[]; 21 | variables?: Record; 22 | timeout?: number; 23 | strict?: boolean; 24 | } 25 | ): Promise => 26 | conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { 27 | type: "render_template", 28 | ...params, 29 | }); 30 | -------------------------------------------------------------------------------- /src/ha/data/ws-themes.ts: -------------------------------------------------------------------------------- 1 | export interface ThemeVars { 2 | // Incomplete 3 | "primary-color": string; 4 | "text-primary-color": string; 5 | "accent-color": string; 6 | [key: string]: string; 7 | } 8 | 9 | export type Theme = ThemeVars & { 10 | modes?: { 11 | light?: ThemeVars; 12 | dark?: ThemeVars; 13 | }; 14 | }; 15 | 16 | export interface Themes { 17 | default_theme: string; 18 | default_dark_theme: string | null; 19 | themes: Record; 20 | // Currently effective dark mode. Will never be undefined. If user selected "auto" 21 | // in theme picker, this property will still contain either true or false based on 22 | // what has been determined via system preferences and support from the selected theme. 23 | darkMode: boolean; 24 | // Currently globally active theme name 25 | theme: string; 26 | } 27 | -------------------------------------------------------------------------------- /.hass_dev/packages/number.yaml: -------------------------------------------------------------------------------- 1 | input_number: 2 | percentage: 3 | name: Percentage 4 | initial: 30 5 | min: 0 6 | max: 100 7 | step: 0.5 8 | 9 | template: 10 | - number: 11 | - name: Percentage 12 | unique_id: percentage_number 13 | step: 0.5 14 | min: 0 15 | max: 100 16 | state: "{{ states('input_number.percentage') }}" 17 | set_value: 18 | service: input_number.set_value 19 | target: 20 | entity_id: input_number.percentage 21 | data: 22 | value: "{{ value }}" 23 | 24 | homeassistant: 25 | customize: 26 | input_number.percentage: 27 | friendly_name: Percentage Input Number 28 | unit_of_measurement: "%" 29 | number.percentage: 30 | friendly_name: Percentage Number 31 | unit_of_measurement: "%" -------------------------------------------------------------------------------- /src/cards/title-card/title-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, object, optional, string } from "superstruct"; 2 | import { ActionConfig, actionConfigStruct, LovelaceCardConfig } from "../../ha"; 3 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 4 | 5 | export interface TitleCardConfig extends LovelaceCardConfig { 6 | title?: string; 7 | subtitle?: string; 8 | alignment?: string; 9 | title_tap_action?: ActionConfig; 10 | subtitle_tap_action?: ActionConfig; 11 | } 12 | 13 | export const titleCardConfigStruct = assign( 14 | lovelaceCardConfigStruct, 15 | object({ 16 | title: optional(string()), 17 | subtitle: optional(string()), 18 | alignment: optional(string()), 19 | title_tap_action: optional(actionConfigStruct), 20 | subtitle_tap_action: optional(actionConfigStruct), 21 | }) 22 | ); 23 | -------------------------------------------------------------------------------- /src/ha/data/humidifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | 6 | export type HumidifierAction = "off" | "humidifying" | "dehumidifying" | "idle"; 7 | 8 | export type HumidifierEntity = HassEntityBase & { 9 | attributes: HassEntityAttributeBase & { 10 | humidity?: number; 11 | current_humidity?: number; 12 | min_humidity?: number; 13 | max_humidity?: number; 14 | action: HumidifierAction; 15 | mode?: string; 16 | available_modes?: string[]; 17 | }; 18 | }; 19 | 20 | export const HUMIDIFIER_SUPPORT_MODES = 1; 21 | 22 | export const HUMIDIFIER_STATE_ON = "on"; 23 | export const HUMIDIFIER_STATE_OFF = "off"; 24 | 25 | export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier"; 26 | export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier"; 27 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/common/handle-actions.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent } from "../../../common/dom/fire_event"; 2 | import { ActionConfig } from "../../../data/lovelace"; 3 | import { HomeAssistant } from "../../../types"; 4 | 5 | export type ActionConfigParams = { 6 | entity?: string; 7 | camera_image?: string; 8 | hold_action?: ActionConfig; 9 | tap_action?: ActionConfig; 10 | double_tap_action?: ActionConfig; 11 | }; 12 | 13 | export const handleAction = async ( 14 | node: HTMLElement, 15 | _hass: HomeAssistant, 16 | config: ActionConfigParams, 17 | action: string 18 | ): Promise => { 19 | fireEvent(node, "hass-action", { config, action }); 20 | }; 21 | 22 | type ActionParams = { config: ActionConfigParams; action: string }; 23 | 24 | declare global { 25 | interface HASSDomEvents { 26 | "hass-action": ActionParams; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/loader.ts: -------------------------------------------------------------------------------- 1 | // Hack to load ha-components needed for editor 2 | export const loadHaComponents = () => { 3 | if ( 4 | !customElements.get("ha-form") || 5 | !customElements.get("hui-card-features-editor") 6 | ) { 7 | (customElements.get("hui-tile-card") as any)?.getConfigElement(); 8 | } 9 | if (!customElements.get("ha-entity-picker")) { 10 | (customElements.get("hui-entities-card") as any)?.getConfigElement(); 11 | } 12 | if (!customElements.get("ha-card-conditions-editor")) { 13 | (customElements.get("hui-conditional-card") as any)?.getConfigElement(); 14 | } 15 | }; 16 | 17 | export const loadCustomElement = async (name: string) => { 18 | let Component = customElements.get(name) as T; 19 | if (Component) { 20 | return Component; 21 | } 22 | await customElements.whenDefined(name); 23 | return customElements.get(name) as T; 24 | }; 25 | -------------------------------------------------------------------------------- /.hass_dev/packages/climate.yaml: -------------------------------------------------------------------------------- 1 | climate: 2 | - platform: generic_thermostat 3 | name: home_thermostat 4 | heater: input_boolean.home_thermostat_heater 5 | target_sensor: input_number.home_thermostat_temperature 6 | min_temp: 7 7 | max_temp: 35 8 | precision: 0.5 9 | target_temp_step: 0.5 10 | ac_mode: false 11 | 12 | input_boolean: 13 | home_thermostat_heater: 14 | name: home_thermostat_heater 15 | icon: mdi:car 16 | 17 | input_number: 18 | home_thermostat_temperature: 19 | name: home_thermostat_temperature 20 | initial: 20 21 | min: 0 22 | max: 35 23 | step: 0.5 24 | 25 | homeassistant: 26 | customize: 27 | input_boolean.home_thermostat_heater: 28 | friendly_name: Home Heater 29 | input_number.home_thermostat_temperature: 30 | friendly_name: Home Temperature 31 | climate.home_thermostat: 32 | friendly_name: Home Thermostat -------------------------------------------------------------------------------- /.hass_dev/views/title-view.yaml: -------------------------------------------------------------------------------- 1 | title: Title 2 | icon: mdi:format-title 3 | cards: 4 | - type: vertical-stack 5 | cards: 6 | - type: custom:mushroom-title-card 7 | title: Hello, {{user}} 8 | subtitle: How are you? 9 | - type: custom:mushroom-title-card 10 | title: Number of entities 11 | subtitle: | 12 | {{ states | count }} entities 13 | - type: custom:mushroom-title-card 14 | title: Title only 15 | - type: custom:mushroom-title-card 16 | subtitle: Subtitle only 17 | - type: custom:mushroom-title-card 18 | title: Actionable title 19 | title_tap_action: 20 | action: navigate 21 | navigation_path: /lovelace-mushroom-showcase 22 | - type: custom:mushroom-title-card 23 | subtitle: Actionable subtitle 24 | subtitle_tap_action: 25 | action: navigate 26 | navigation_path: /lovelace-mushroom-showcase -------------------------------------------------------------------------------- /src/cards/lock-card/lock-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export type LockCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig; 21 | 22 | export const lockCardConfigStruct = assign( 23 | lovelaceCardConfigStruct, 24 | assign( 25 | entitySharedConfigStruct, 26 | appearanceSharedConfigStruct, 27 | actionsSharedConfigStruct 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /src/cards/vacuum-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { 3 | STATE_CLEANING, 4 | STATE_DOCKED, 5 | STATE_IDLE, 6 | STATE_OFF, 7 | STATE_ON, 8 | STATE_RETURNING, 9 | } from "../../ha"; 10 | 11 | export function isCleaning(stateObj: HassEntity): boolean { 12 | switch (stateObj.state) { 13 | case STATE_CLEANING: 14 | case STATE_ON: 15 | return true; 16 | default: 17 | return false; 18 | } 19 | } 20 | 21 | export function isStopped(stateObj: HassEntity): boolean { 22 | switch (stateObj.state) { 23 | case STATE_DOCKED: 24 | case STATE_OFF: 25 | case STATE_IDLE: 26 | case STATE_RETURNING: 27 | return true; 28 | default: 29 | return false; 30 | } 31 | } 32 | 33 | export function isReturningHome(stateObj: HassEntity): boolean { 34 | switch (stateObj.state) { 35 | case STATE_RETURNING: 36 | return true; 37 | default: 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cards/person-card/person-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export type PersonCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig; 21 | 22 | export const personCardConfigStruct = assign( 23 | lovelaceCardConfigStruct, 24 | assign( 25 | entitySharedConfigStruct, 26 | appearanceSharedConfigStruct, 27 | actionsSharedConfigStruct 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /src/cards/alarm-control-panel-card/const.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../const"; 2 | 3 | export const ALARM_CONTROl_PANEL_CARD_NAME = `${PREFIX_NAME}-alarm-control-panel-card`; 4 | export const ALARM_CONTROl_PANEL_CARD_EDITOR_NAME = `${ALARM_CONTROl_PANEL_CARD_NAME}-editor`; 5 | export const ALARM_CONTROl_PANEL_ENTITY_DOMAINS = ["alarm_control_panel"]; 6 | 7 | export const ALARM_CONTROL_PANEL_CARD_STATE_COLOR = { 8 | disarmed: "var(--rgb-state-alarm-disarmed)", 9 | armed: "var(--rgb-state-alarm-armed)", 10 | triggered: "var(--rgb-state-alarm-triggered)", 11 | unavailable: "var(--rgb-warning)", 12 | }; 13 | 14 | export const ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR = "var(--rgb-grey)"; 15 | 16 | export const ALARM_CONTROL_PANEL_CARD_STATE_SERVICE = { 17 | disarmed: "alarm_disarm", 18 | armed_away: "alarm_arm_away", 19 | armed_home: "alarm_arm_home", 20 | armed_night: "alarm_arm_night", 21 | armed_vacation: "alarm_arm_vacation", 22 | armed_custom_bypass: "alarm_arm_custom_bypass", 23 | }; 24 | -------------------------------------------------------------------------------- /src/ha/common/color/compute-color.ts: -------------------------------------------------------------------------------- 1 | export const THEME_COLORS = new Set([ 2 | "primary", 3 | "accent", 4 | "disabled", 5 | "primary-text", 6 | "secondary-text", 7 | "disabled-text", 8 | "red", 9 | "pink", 10 | "purple", 11 | "deep-purple", 12 | "indigo", 13 | "blue", 14 | "light-blue", 15 | "cyan", 16 | "teal", 17 | "green", 18 | "light-green", 19 | "lime", 20 | "yellow", 21 | "amber", 22 | "orange", 23 | "deep-orange", 24 | "brown", 25 | "light-grey", 26 | "grey", 27 | "dark-grey", 28 | "blue-grey", 29 | "black", 30 | "white", 31 | ]); 32 | 33 | function isRgbString(color: string): boolean { 34 | return /^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$/.test(color); 35 | } 36 | 37 | export function computeCssColor(color: string): string { 38 | if (THEME_COLORS.has(color)) { 39 | return `var(--${color}-color)`; 40 | } 41 | if (isRgbString(color)) { 42 | const rgb = color.split(",").map((c) => c.trim()); 43 | return `rgb(${rgb.join(", ")})`; 44 | } 45 | return color; 46 | } 47 | -------------------------------------------------------------------------------- /src/ha/common/util/debounce.ts: -------------------------------------------------------------------------------- 1 | // From: https://davidwalsh.name/javascript-debounce-function 2 | 3 | // Returns a function, that, as long as it continues to be invoked, will not 4 | // be triggered. The function will be called after it stops being called for 5 | // N milliseconds. If `immediate` is passed, trigger the function on the 6 | // leading edge, instead of the trailing. 7 | 8 | export const debounce = ( 9 | func: (...args: T) => void, 10 | wait: number, 11 | immediate = false 12 | ) => { 13 | let timeout: number | undefined; 14 | const debouncedFunc = (...args: T): void => { 15 | const later = () => { 16 | timeout = undefined; 17 | if (!immediate) { 18 | func(...args); 19 | } 20 | }; 21 | const callNow = immediate && !timeout; 22 | clearTimeout(timeout); 23 | timeout = window.setTimeout(later, wait); 24 | if (callNow) { 25 | func(...args); 26 | } 27 | }; 28 | debouncedFunc.cancel = () => { 29 | clearTimeout(timeout); 30 | }; 31 | return debouncedFunc; 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/lovelace/chip-element-editor.ts: -------------------------------------------------------------------------------- 1 | import { customElement } from "lit/decorators.js"; 2 | import { computeChipComponentName } from "./chip/chip-element"; 3 | import { LovelaceChipConfig } from "./chip/types"; 4 | import { MushroomElementEditor } from "./element-editor"; 5 | import { LovelaceChipEditor } from "./types"; 6 | 7 | @customElement("mushroom-chip-element-editor") 8 | export class MushroomChipElementEditor extends MushroomElementEditor { 9 | protected get configElementType(): string | undefined { 10 | return this.value?.type; 11 | } 12 | 13 | protected async getConfigElement(): Promise { 14 | const elClass = (await getChipElementClass(this.configElementType!)) as any; 15 | 16 | // Check if a GUI editor exists 17 | if (elClass && elClass.getConfigElement) { 18 | return elClass.getConfigElement(); 19 | } 20 | 21 | return undefined; 22 | } 23 | } 24 | 25 | export const getChipElementClass = (type: string) => 26 | customElements.get(computeChipComponentName(type)); 27 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/common/entity/turn-on-off-entity.ts: -------------------------------------------------------------------------------- 1 | import { computeDomain } from "../../../../common/entity/compute_domain"; 2 | import { HomeAssistant, ServiceCallResponse } from "../../../../types"; 3 | 4 | export const turnOnOffEntity = ( 5 | hass: HomeAssistant, 6 | entityId: string, 7 | turnOn = true 8 | ): Promise => { 9 | const stateDomain = computeDomain(entityId); 10 | const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain; 11 | 12 | let service; 13 | switch (stateDomain) { 14 | case "lock": 15 | service = turnOn ? "unlock" : "lock"; 16 | break; 17 | case "cover": 18 | service = turnOn ? "open_cover" : "close_cover"; 19 | break; 20 | case "button": 21 | case "input_button": 22 | service = "press"; 23 | break; 24 | case "scene": 25 | service = "turn_on"; 26 | break; 27 | default: 28 | service = turnOn ? "turn_on" : "turn_off"; 29 | } 30 | 31 | return hass.callService(serviceDomain, service, { entity_id: entityId }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/form/custom/ha-selector-mushroom-color.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import "../../../shared/editor/color-picker"; 5 | 6 | export type MushColorSelector = { 7 | mush_color: {}; 8 | }; 9 | 10 | @customElement("ha-selector-mush_color") 11 | export class HaMushColorSelector extends LitElement { 12 | @property() public hass!: HomeAssistant; 13 | 14 | @property() public selector!: MushColorSelector; 15 | 16 | @property() public value?: string; 17 | 18 | @property() public label?: string; 19 | 20 | protected render() { 21 | return html` 22 | 28 | `; 29 | } 30 | 31 | private _valueChanged(ev: CustomEvent) { 32 | fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/cards/empty-card/empty-card-editor.ts: -------------------------------------------------------------------------------- 1 | import { html } from "lit"; 2 | import { customElement, state } from "lit/decorators.js"; 3 | import { LovelaceCardEditor } from "../../ha"; 4 | import setupCustomlocalize from "../../localize"; 5 | import { MushroomBaseElement } from "../../utils/base-element"; 6 | import { HaFormSchema } from "../../utils/form/ha-form"; 7 | import { EMPTY_CARD_EDITOR_NAME } from "./const"; 8 | import { EmptyCardConfig } from "./empty-card-config"; 9 | 10 | const SCHEMA: HaFormSchema[] = [ 11 | { 12 | name: "description", 13 | type: "constant", 14 | } 15 | ]; 16 | 17 | @customElement(EMPTY_CARD_EDITOR_NAME) 18 | export class EntityCardEditor extends MushroomBaseElement implements LovelaceCardEditor { 19 | @state() private _config?: EmptyCardConfig; 20 | 21 | public setConfig(): void { 22 | // No config necessary 23 | } 24 | 25 | protected render() { 26 | const customLocalize = setupCustomlocalize(this.hass); 27 | 28 | return html` 29 |

${customLocalize("editor.card.empty.no_config_options")}

30 | `; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/config/actions-config.ts: -------------------------------------------------------------------------------- 1 | import { object, optional } from "superstruct"; 2 | import { ActionConfig, actionConfigStruct } from "../../ha"; 3 | import { HaFormSchema } from "../../utils/form/ha-form"; 4 | import { UiAction } from "../../utils/form/ha-selector"; 5 | 6 | export const actionsSharedConfigStruct = object({ 7 | tap_action: optional(actionConfigStruct), 8 | hold_action: optional(actionConfigStruct), 9 | double_tap_action: optional(actionConfigStruct), 10 | }); 11 | 12 | export type ActionsSharedConfig = { 13 | tap_action?: ActionConfig; 14 | hold_action?: ActionConfig; 15 | double_tap_action?: ActionConfig; 16 | }; 17 | 18 | export const computeActionsFormSchema = ( 19 | actions?: UiAction[] 20 | ): HaFormSchema[] => { 21 | return [ 22 | { 23 | name: "tap_action", 24 | selector: { ui_action: { actions } }, 25 | }, 26 | { 27 | name: "hold_action", 28 | selector: { ui_action: { actions } }, 29 | }, 30 | { 31 | name: "double_tap_action", 32 | selector: { ui_action: { actions } }, 33 | }, 34 | ]; 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/form/custom/ha-selector-mushroom-layout.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import "../../../shared/editor/layout-picker"; 5 | 6 | export type MushLayoutSelector = { 7 | mush_layout: {}; 8 | }; 9 | 10 | @customElement("ha-selector-mush_layout") 11 | export class HaMushLayoutSelector extends LitElement { 12 | @property() public hass!: HomeAssistant; 13 | 14 | @property() public selector!: MushLayoutSelector; 15 | 16 | @property() public value?: string; 17 | 18 | @property() public label?: string; 19 | 20 | protected render() { 21 | return html` 22 | 28 | `; 29 | } 30 | 31 | private _valueChanged(ev: CustomEvent) { 32 | fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/cards/entity-card/entity-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional, string } from "superstruct"; 2 | import { 3 | ActionsSharedConfig, 4 | actionsSharedConfigStruct, 5 | } from "../../shared/config/actions-config"; 6 | import { 7 | appearanceSharedConfigStruct, 8 | AppearanceSharedConfig, 9 | } from "../../shared/config/appearance-config"; 10 | import { 11 | entitySharedConfigStruct, 12 | EntitySharedConfig, 13 | } from "../../shared/config/entity-config"; 14 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 15 | import { LovelaceCardConfig } from "../../ha"; 16 | 17 | export type EntityCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | icon_color?: string; 22 | }; 23 | 24 | export const entityCardConfigStruct = assign( 25 | lovelaceCardConfigStruct, 26 | assign( 27 | entitySharedConfigStruct, 28 | appearanceSharedConfigStruct, 29 | actionsSharedConfigStruct 30 | ), 31 | object({ 32 | icon_color: optional(string()), 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /src/cards/select-card/select-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional, string } from "superstruct"; 2 | import { 3 | ActionsSharedConfig, 4 | actionsSharedConfigStruct, 5 | } from "../../shared/config/actions-config"; 6 | import { 7 | appearanceSharedConfigStruct, 8 | AppearanceSharedConfig, 9 | } from "../../shared/config/appearance-config"; 10 | import { 11 | entitySharedConfigStruct, 12 | EntitySharedConfig, 13 | } from "../../shared/config/entity-config"; 14 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 15 | import { LovelaceCardConfig } from "../../ha"; 16 | 17 | export type SelectCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | icon_color?: string; 22 | }; 23 | 24 | export const selectCardConfigStruct = assign( 25 | lovelaceCardConfigStruct, 26 | assign( 27 | entitySharedConfigStruct, 28 | appearanceSharedConfigStruct, 29 | actionsSharedConfigStruct 30 | ), 31 | object({ 32 | icon_color: optional(string()), 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /src/utils/form/custom/ha-selector-mushroom-icon-type.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, HomeAssistant } from "../../../ha"; 2 | import { html, LitElement } from "lit"; 3 | import { customElement, property } from "lit/decorators.js"; 4 | import "../../../shared/editor/icon-type-picker"; 5 | 6 | export type MushIconTypeSelector = { 7 | mush_icon_type: {}; 8 | }; 9 | 10 | @customElement("ha-selector-mush_icon_type") 11 | export class HaMushIconTypeSelector extends LitElement { 12 | @property() public hass!: HomeAssistant; 13 | 14 | @property() public selector!: MushIconTypeSelector; 15 | 16 | @property() public value?: string; 17 | 18 | @property() public label?: string; 19 | 20 | protected render() { 21 | return html` 22 | 28 | `; 29 | } 30 | 31 | private _valueChanged(ev: CustomEvent) { 32 | fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/form/custom/ha-selector-mushroom-alignment.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import "../../../shared/editor/alignment-picker"; 5 | 6 | export type MushAlignementSelector = { 7 | mush_alignment: {}; 8 | }; 9 | 10 | @customElement("ha-selector-mush_alignment") 11 | export class HaMushAlignmentSelector extends LitElement { 12 | @property() public hass!: HomeAssistant; 13 | 14 | @property() public selector!: MushAlignementSelector; 15 | 16 | @property() public value?: string; 17 | 18 | @property() public label?: string; 19 | 20 | protected render() { 21 | return html` 22 | 28 | `; 29 | } 30 | 31 | private _valueChanged(ev: CustomEvent) { 32 | fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/badges/template/template-badge-config.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, object, optional, string, union } from "superstruct"; 2 | import { LovelaceBadgeConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { lovelaceBadgeConfigStruct } from "../../shared/config/lovelace-badge-config"; 8 | 9 | export type TemplateBadgeConfig = LovelaceBadgeConfig & 10 | ActionsSharedConfig & { 11 | entity?: string; 12 | area?: string; 13 | icon?: string; 14 | color?: string; 15 | label?: string; 16 | content?: string; 17 | picture?: string; 18 | entity_id?: string | string[]; 19 | }; 20 | 21 | export const templateBadgeConfigStruct = assign( 22 | lovelaceBadgeConfigStruct, 23 | actionsSharedConfigStruct, 24 | object({ 25 | entity: optional(string()), 26 | area: optional(string()), 27 | icon: optional(string()), 28 | color: optional(string()), 29 | label: optional(string()), 30 | content: optional(string()), 31 | picture: optional(string()), 32 | entity_id: optional(union([string(), array(string())])), 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /src/ha/common/string/compare.ts: -------------------------------------------------------------------------------- 1 | import memoizeOne from "memoize-one"; 2 | 3 | const collator = memoizeOne( 4 | (language: string | undefined) => new Intl.Collator(language) 5 | ); 6 | 7 | const caseInsensitiveCollator = memoizeOne( 8 | (language: string | undefined) => 9 | new Intl.Collator(language, { sensitivity: "accent" }) 10 | ); 11 | 12 | const fallbackStringCompare = (a: string, b: string) => { 13 | if (a < b) { 14 | return -1; 15 | } 16 | if (a > b) { 17 | return 1; 18 | } 19 | 20 | return 0; 21 | }; 22 | 23 | export const stringCompare = ( 24 | a: string, 25 | b: string, 26 | language: string | undefined = undefined 27 | ) => { 28 | // @ts-ignore 29 | if (Intl?.Collator) { 30 | return collator(language).compare(a, b); 31 | } 32 | 33 | return fallbackStringCompare(a, b); 34 | }; 35 | 36 | export const caseInsensitiveStringCompare = ( 37 | a: string, 38 | b: string, 39 | language: string | undefined = undefined 40 | ) => { 41 | // @ts-ignore 42 | if (Intl?.Collator) { 43 | return caseInsensitiveCollator(language).compare(a, b); 44 | } 45 | 46 | return fallbackStringCompare(a.toLowerCase(), b.toLowerCase()); 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils/form/custom/ha-selector-mushroom-info.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import "../../../shared/editor/info-picker"; 5 | import { Info } from "../../info"; 6 | 7 | export type MushInfoSelector = { 8 | mush_info: { 9 | infos?: Info[]; 10 | }; 11 | }; 12 | 13 | @customElement("ha-selector-mush_info") 14 | export class HaMushInfoSelector extends LitElement { 15 | @property() public hass!: HomeAssistant; 16 | 17 | @property() public selector!: MushInfoSelector; 18 | 19 | @property() public value?: string; 20 | 21 | @property() public label?: string; 22 | 23 | protected render() { 24 | return html` 25 | 32 | `; 33 | } 34 | 35 | private _valueChanged(ev: CustomEvent) { 36 | fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cards/alarm-control-panel-card/alarm-control-panel-card-config.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { AlarmMode } from "../../ha/data/alarm_control_panel"; 4 | import { 5 | ActionsSharedConfig, 6 | actionsSharedConfigStruct, 7 | } from "../../shared/config/actions-config"; 8 | import { 9 | AppearanceSharedConfig, 10 | appearanceSharedConfigStruct, 11 | } from "../../shared/config/appearance-config"; 12 | import { 13 | EntitySharedConfig, 14 | entitySharedConfigStruct, 15 | } from "../../shared/config/entity-config"; 16 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 17 | 18 | export type AlarmControlPanelCardConfig = LovelaceCardConfig & 19 | EntitySharedConfig & 20 | AppearanceSharedConfig & 21 | ActionsSharedConfig & { 22 | states?: AlarmMode[]; 23 | }; 24 | 25 | export const alarmControlPanelCardCardConfigStruct = assign( 26 | lovelaceCardConfigStruct, 27 | assign( 28 | entitySharedConfigStruct, 29 | appearanceSharedConfigStruct, 30 | actionsSharedConfigStruct 31 | ), 32 | object({ 33 | states: optional(array()), 34 | }) 35 | ); 36 | -------------------------------------------------------------------------------- /src/cards/update-card/update-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export type UpdateCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | show_buttons_control?: boolean; 22 | collapsible_controls?: boolean; 23 | }; 24 | 25 | export const updateCardConfigStruct = assign( 26 | lovelaceCardConfigStruct, 27 | assign( 28 | entitySharedConfigStruct, 29 | appearanceSharedConfigStruct, 30 | actionsSharedConfigStruct 31 | ), 32 | object({ 33 | show_buttons_control: optional(boolean()), 34 | collapsible_controls: optional(boolean()), 35 | }) 36 | ); 37 | -------------------------------------------------------------------------------- /src/ha/common/util/compute_rtl.ts: -------------------------------------------------------------------------------- 1 | import { LitElement } from "lit"; 2 | import { HomeAssistant } from "../../types"; 3 | 4 | export function computeRTL(hass: HomeAssistant) { 5 | const lang = hass.language || "en"; 6 | if (hass.translationMetadata.translations[lang]) { 7 | return hass.translationMetadata.translations[lang].isRTL || false; 8 | } 9 | return false; 10 | } 11 | 12 | export function computeRTLDirection(hass: HomeAssistant) { 13 | return emitRTLDirection(computeRTL(hass)); 14 | } 15 | 16 | export function emitRTLDirection(rtl: boolean) { 17 | return rtl ? "rtl" : "ltr"; 18 | } 19 | 20 | export function computeDirectionStyles(isRTL: boolean, element: LitElement) { 21 | const direction: string = emitRTLDirection(isRTL); 22 | setDirectionStyles(direction, element); 23 | } 24 | 25 | export function setDirectionStyles(direction: string, element: LitElement) { 26 | element.style.direction = direction; 27 | element.style.setProperty("--direction", direction); 28 | element.style.setProperty( 29 | "--float-start", 30 | direction === "ltr" ? "left" : "right" 31 | ); 32 | element.style.setProperty( 33 | "--float-end", 34 | direction === "ltr" ? "right" : "left" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/ha/data/vacuum.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | 6 | export const STATE_ON = "on"; 7 | export const STATE_OFF = "off"; 8 | export const STATE_CLEANING = "cleaning"; 9 | export const STATE_DOCKED = "docked"; 10 | export const STATE_IDLE = "idle"; 11 | export const STATE_PAUSED = "paused"; 12 | export const STATE_RETURNING = "returning"; 13 | export const STATE_ERROR = "error"; 14 | 15 | export const VACUUM_SUPPORT_TURN_ON = 1; 16 | export const VACUUM_SUPPORT_TURN_OFF = 2; 17 | export const VACUUM_SUPPORT_PAUSE = 4; 18 | export const VACUUM_SUPPORT_STOP = 8; 19 | export const VACUUM_SUPPORT_RETURN_HOME = 16; 20 | export const VACUUM_SUPPORT_STATUS = 128; 21 | export const VACUUM_SUPPORT_LOCATE = 512; 22 | export const VACUUM_SUPPORT_CLEAN_SPOT = 1024; 23 | export const VACUUM_SUPPORT_MAP = 2048; 24 | export const VACUUM_SUPPORT_STATE = 4096; 25 | export const VACUUM_SUPPORT_START = 8192; 26 | 27 | interface VacuumEntityAttributes extends HassEntityAttributeBase { 28 | battery_level: number; 29 | fan_speed: any; 30 | [key: string]: any; 31 | } 32 | 33 | export interface VacuumEntity extends HassEntityBase { 34 | attributes: VacuumEntityAttributes; 35 | } 36 | -------------------------------------------------------------------------------- /src/cards/humidifier-card/humidifier-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export type HumidifierCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | show_target_humidity_control?: boolean; 22 | collapsible_controls?: boolean; 23 | }; 24 | 25 | export const humidifierCardConfigStruct = assign( 26 | lovelaceCardConfigStruct, 27 | assign( 28 | entitySharedConfigStruct, 29 | appearanceSharedConfigStruct, 30 | actionsSharedConfigStruct 31 | ), 32 | object({ 33 | show_target_humidity_control: optional(boolean()), 34 | collapsible_controls: optional(boolean()), 35 | }) 36 | ); 37 | -------------------------------------------------------------------------------- /src/utils/card-styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from "lit"; 2 | 3 | export const cardStyle = css` 4 | ha-card { 5 | box-sizing: border-box; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: var(--layout-align); 9 | height: auto; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | ha-card.fill-container { 14 | height: 100%; 15 | } 16 | :host([layout="grid"]) ha-card { 17 | height: 100%; 18 | } 19 | .actions { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | justify-content: flex-start; 24 | overflow-x: auto; 25 | overflow-y: hidden; 26 | scrollbar-width: none; /* Firefox */ 27 | -ms-overflow-style: none; /* IE 10+ */ 28 | padding: var(--control-spacing); 29 | padding-top: 0; 30 | box-sizing: border-box; 31 | gap: var(--control-spacing); 32 | } 33 | .actions::-webkit-scrollbar { 34 | background: transparent; /* Chrome/Safari/Webkit */ 35 | height: 0px; 36 | } 37 | .unavailable { 38 | --main-color: rgb(var(--rgb-warning)); 39 | } 40 | .not-found { 41 | --main-color: rgb(var(--rgb-danger)); 42 | } 43 | mushroom-state-item[disabled] { 44 | cursor: initial; 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/utils/lovelace/chip/chip-element.ts: -------------------------------------------------------------------------------- 1 | import { PREFIX_NAME } from "../../../const"; 2 | import { LovelaceChip, LovelaceChipConfig } from "./types"; 3 | 4 | export const createChipElement = ( 5 | config: LovelaceChipConfig 6 | ): LovelaceChip | undefined => { 7 | try { 8 | const tag = computeChipComponentName(config.type); 9 | if (customElements.get(tag)) { 10 | // @ts-ignore 11 | const element = document.createElement(tag, config) as LovelaceChip; 12 | element.setConfig(config); 13 | return element; 14 | } 15 | // @ts-ignore 16 | const element = document.createElement(tag) as LovelaceChip; 17 | customElements.whenDefined(tag).then(() => { 18 | try { 19 | customElements.upgrade(element); 20 | element.setConfig(config); 21 | } catch (err: any) { 22 | // Do nothing 23 | } 24 | }); 25 | return element; 26 | } catch (err) { 27 | console.error(err); 28 | return undefined; 29 | } 30 | }; 31 | 32 | export function computeChipComponentName(type: string): string { 33 | return `${PREFIX_NAME}-${type}-chip`; 34 | } 35 | 36 | export function computeChipEditorComponentName(type: string): string { 37 | return `${PREFIX_NAME}-${type}-chip-editor`; 38 | } 39 | -------------------------------------------------------------------------------- /src/cards/alarm-control-panel-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { UNAVAILABLE } from "../../ha"; 3 | import { 4 | ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR, 5 | ALARM_CONTROL_PANEL_CARD_STATE_COLOR, 6 | ALARM_CONTROL_PANEL_CARD_STATE_SERVICE, 7 | } from "./const"; 8 | 9 | export function getStateColor(state: string): string { 10 | return ( 11 | ALARM_CONTROL_PANEL_CARD_STATE_COLOR[state.split("_")[0]] ?? 12 | ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR 13 | ); 14 | } 15 | 16 | export function getStateService(state: string): string | undefined { 17 | return ALARM_CONTROL_PANEL_CARD_STATE_SERVICE[state]; 18 | } 19 | 20 | export function shouldPulse(state: string): boolean { 21 | return ["arming", "triggered", "pending", UNAVAILABLE].indexOf(state) >= 0; 22 | } 23 | 24 | export function isActionsAvailable(stateObj: HassEntity) { 25 | return UNAVAILABLE !== stateObj.state; 26 | } 27 | 28 | export function isDisarmed(stateObj: HassEntity) { 29 | return stateObj.state === "disarmed"; 30 | } 31 | 32 | export function hasCode(stateObj: HassEntity): boolean { 33 | return ( 34 | stateObj.attributes.code_format && 35 | stateObj.attributes.code_format !== "no_code" 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/cards/person-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { UNKNOWN } from "../../ha"; 3 | 4 | export function getStateIcon(stateObj: HassEntity, zones: HassEntity[]) { 5 | const state = stateObj.state; 6 | if (state === UNKNOWN) { 7 | return "mdi:help"; 8 | } else if (state === "not_home") { 9 | return "mdi:home-export-outline"; 10 | } else if (state === "home") { 11 | return "mdi:home"; 12 | } 13 | 14 | const zone = zones.find((z) => state === z.attributes.friendly_name); 15 | if (zone && zone.attributes.icon) { 16 | return zone.attributes.icon; 17 | } 18 | 19 | return "mdi:home"; 20 | } 21 | 22 | export function getStateColor(stateObj: HassEntity, zones: HassEntity[]) { 23 | const state = stateObj.state; 24 | if (state === UNKNOWN) { 25 | return "var(--rgb-state-person-unknown)"; 26 | } else if (state === "not_home") { 27 | return "var(--rgb-state-person-not-home)"; 28 | } else if (state === "home") { 29 | return "var(--rgb-state-person-home)"; 30 | } 31 | const isInZone = zones.some((z) => state === z.attributes.friendly_name); 32 | if (isInZone) { 33 | return "var(--rgb-state-person-zone)"; 34 | } 35 | return "var(--rgb-state-person-home)"; 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/badge-icon.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { property, customElement } from "lit/decorators.js"; 3 | 4 | @customElement("mushroom-badge-icon") 5 | export class BadgeIcon extends LitElement { 6 | @property() public icon: string = ""; 7 | 8 | protected render(): TemplateResult { 9 | return html` 10 |
11 | 12 |
13 | `; 14 | } 15 | 16 | static get styles(): CSSResultGroup { 17 | return css` 18 | :host { 19 | --main-color: rgb(var(--rgb-grey)); 20 | --icon-color: rgb(var(--rgb-white)); 21 | } 22 | .badge { 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | line-height: 0; 27 | width: var(--badge-size); 28 | height: var(--badge-size); 29 | font-size: var(--badge-size); 30 | border-radius: var(--badge-border-radius); 31 | background-color: var(--main-color); 32 | transition: background-color 280ms ease-in-out; 33 | } 34 | .badge ha-icon { 35 | --mdc-icon-size: var(--badge-icon-size); 36 | color: var(--icon-color); 37 | } 38 | `; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cards/cover-card/cover-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { 3 | actionsSharedConfigStruct, 4 | ActionsSharedConfig, 5 | } from "../../shared/config/actions-config"; 6 | import { 7 | appearanceSharedConfigStruct, 8 | AppearanceSharedConfig, 9 | } from "../../shared/config/appearance-config"; 10 | import { 11 | entitySharedConfigStruct, 12 | EntitySharedConfig, 13 | } from "../../shared/config/entity-config"; 14 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 15 | import { LovelaceCardConfig } from "../../ha"; 16 | 17 | export type CoverCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | show_buttons_control?: false; 22 | show_position_control?: false; 23 | show_tilt_position_control?: false; 24 | }; 25 | 26 | export const coverCardConfigStruct = assign( 27 | lovelaceCardConfigStruct, 28 | assign( 29 | entitySharedConfigStruct, 30 | appearanceSharedConfigStruct, 31 | actionsSharedConfigStruct 32 | ), 33 | object({ 34 | show_buttons_control: optional(boolean()), 35 | show_position_control: optional(boolean()), 36 | show_tilt_position_control: optional(boolean()), 37 | }) 38 | ); 39 | -------------------------------------------------------------------------------- /.hass_dev/packages/alarm_control_panel.yaml: -------------------------------------------------------------------------------- 1 | alarm_control_panel: 2 | - platform: manual 3 | name: Alarm panel 1 4 | arming_time: 5 5 | - platform: manual 6 | name: Alarm panel 2 7 | arming_time: 5 8 | - platform: manual 9 | name: Alarm panel code 10 | code: 1234 11 | arming_time: 5 12 | - platform: manual 13 | name: Alarm panel text code 14 | code: azerty 15 | arming_time: 5 16 | - platform: template 17 | panels: 18 | templated_alarm_panel: 19 | value_template: "{{ states('alarm_control_panel.alarm_panel_1') }}" 20 | code_format: no_code 21 | arm_away: 22 | service: alarm_control_panel.alarm_arm_away 23 | target: 24 | entity_id: 25 | - alarm_control_panel.alarm_panel_1 26 | - alarm_control_panel.alarm_panel_2 27 | arm_home: 28 | service: alarm_control_panel.alarm_arm_home 29 | target: 30 | entity_id: 31 | - alarm_control_panel.alarm_panel_1 32 | - alarm_control_panel.alarm_panel_2 33 | disarm: 34 | service: alarm_control_panel.alarm_disarm 35 | target: 36 | entity_id: 37 | - alarm_control_panel.alarm_panel_1 38 | - alarm_control_panel.alarm_panel_2 -------------------------------------------------------------------------------- /src/ha/data/lock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | 6 | interface LockEntityAttributes extends HassEntityAttributeBase { 7 | changed_by?: string; 8 | code_format?: string; 9 | code?: string; 10 | is_locked?: boolean; 11 | is_locking?: boolean; 12 | is_unlocking?: boolean; 13 | } 14 | 15 | export type LockCommand = 16 | | typeof LOCK_SERVICE_LOCK 17 | | typeof LOCK_SERVICE_OPEN 18 | | typeof LOCK_SERVICE_UNLOCK; 19 | 20 | export type LOCK_STATES = 21 | | typeof LOCK_STATE_JAMMED 22 | | typeof LOCK_STATE_LOCKED 23 | | typeof LOCK_STATE_LOCKING 24 | | typeof LOCK_STATE_UNLOCKED 25 | | typeof LOCK_STATE_UNLOCKING; 26 | 27 | export interface LockEntity extends HassEntityBase { 28 | attributes: LockEntityAttributes; 29 | state: LOCK_STATES | "unavailable" | "unknown"; 30 | } 31 | 32 | export const LOCK_STATE_JAMMED = "jammed"; 33 | export const LOCK_STATE_LOCKED = "locked"; 34 | export const LOCK_STATE_LOCKING = "locking"; 35 | export const LOCK_STATE_UNLOCKED = "unlocked"; 36 | export const LOCK_STATE_UNLOCKING = "unlocking"; 37 | 38 | export const LOCK_SUPPORT_OPEN = 1; 39 | 40 | export const LOCK_SERVICE_LOCK = "lock"; 41 | export const LOCK_SERVICE_OPEN = "open"; 42 | export const LOCK_SERVICE_UNLOCK = "unlock"; 43 | -------------------------------------------------------------------------------- /src/cards/number-card/number-card-config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assign, 3 | enums, 4 | literal, 5 | object, 6 | optional, 7 | string, 8 | union, 9 | } from "superstruct"; 10 | import { LovelaceCardConfig } from "../../ha"; 11 | import { 12 | ActionsSharedConfig, 13 | actionsSharedConfigStruct, 14 | } from "../../shared/config/actions-config"; 15 | import { 16 | AppearanceSharedConfig, 17 | appearanceSharedConfigStruct, 18 | } from "../../shared/config/appearance-config"; 19 | import { 20 | EntitySharedConfig, 21 | entitySharedConfigStruct, 22 | } from "../../shared/config/entity-config"; 23 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 24 | 25 | export const DISPLAY_MODES = ["slider", "buttons"] as const; 26 | 27 | type DisplayMode = (typeof DISPLAY_MODES)[number]; 28 | 29 | export type NumberCardConfig = LovelaceCardConfig & 30 | EntitySharedConfig & 31 | AppearanceSharedConfig & 32 | ActionsSharedConfig & { 33 | icon_color?: string; 34 | display_mode?: DisplayMode; 35 | }; 36 | 37 | export const NumberCardConfigStruct = assign( 38 | lovelaceCardConfigStruct, 39 | assign( 40 | entitySharedConfigStruct, 41 | appearanceSharedConfigStruct, 42 | actionsSharedConfigStruct 43 | ), 44 | object({ 45 | icon_color: optional(string()), 46 | display_mode: optional(enums(DISPLAY_MODES)), 47 | }) 48 | ); 49 | -------------------------------------------------------------------------------- /src/ha/common/color/hex.ts: -------------------------------------------------------------------------------- 1 | import { formatHex, parse } from "culori"; 2 | 3 | /** 4 | * Expands a 3-digit hex color to a 6-digit hex color. 5 | * @param hex - The hex color to expand. 6 | * @returns The expanded hex color. 7 | * @throws If the hex color is invalid. 8 | */ 9 | export const expandHex = (hex: string): string => { 10 | const color = parse(hex); 11 | if (!color) { 12 | throw new Error(`Invalid hex color: ${hex}`); 13 | } 14 | const formattedColor = formatHex(color); 15 | if (!formattedColor) { 16 | throw new Error(`Could not format hex color: ${hex}`); 17 | } 18 | return formattedColor.replace("#", ""); 19 | }; 20 | 21 | /** 22 | * Blends two hex colors. c1 is placed over c2, blend is c1's opacity. 23 | * @param c1 - The first hex color. 24 | * @param c2 - The second hex color. 25 | * @param blend - The blend percentage (0-100). 26 | * @returns The blended hex color. 27 | */ 28 | export const hexBlend = (c1: string, c2: string, blend = 50): string => { 29 | c1 = expandHex(c1); 30 | c2 = expandHex(c2); 31 | let color = ""; 32 | for (let i = 0; i <= 5; i += 2) { 33 | const h1 = parseInt(c1.substring(i, i + 2), 16); 34 | const h2 = parseInt(c2.substring(i, i + 2), 16); 35 | const hex = Math.floor(h2 + (h1 - h2) * (blend / 100)) 36 | .toString(16) 37 | .padStart(2, "0"); 38 | color += hex; 39 | } 40 | return `#${color}`; 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/shape-avatar.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { property, customElement } from "lit/decorators.js"; 3 | import { classMap } from "lit/directives/class-map.js"; 4 | 5 | @customElement("mushroom-shape-avatar") 6 | export class ShapePicture extends LitElement { 7 | @property() public picture_url: string = ""; 8 | 9 | protected render(): TemplateResult { 10 | return html` 11 |
12 | 13 |
14 | `; 15 | } 16 | 17 | static get styles(): CSSResultGroup { 18 | return css` 19 | :host { 20 | --main-color: var(--primary-text-color); 21 | --icon-color-disabled: rgb(var(--rgb-disabled)); 22 | --shape-color: rgba(var(--rgb-primary-text-color), 0.05); 23 | --shape-color-disabled: rgba(var(--rgb-disabled), 0.2); 24 | flex: none; 25 | } 26 | .container { 27 | position: relative; 28 | width: var(--icon-size); 29 | height: var(--icon-size); 30 | flex: none; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | } 35 | .picture { 36 | width: 100%; 37 | height: 100%; 38 | border-radius: var(--icon-border-radius); 39 | } 40 | `; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/shared/config/appearance-config.ts: -------------------------------------------------------------------------------- 1 | import { boolean, enums, Infer, object, optional } from "superstruct"; 2 | import { HaFormSchema } from "../../utils/form/ha-form"; 3 | import { IconType, ICON_TYPES, Info, INFOS } from "../../utils/info"; 4 | import { Layout, layoutStruct } from "../../utils/layout"; 5 | 6 | export const appearanceSharedConfigStruct = object({ 7 | layout: optional(layoutStruct), 8 | fill_container: optional(boolean()), 9 | primary_info: optional(enums(INFOS)), 10 | secondary_info: optional(enums(INFOS)), 11 | icon_type: optional(enums(ICON_TYPES)), 12 | }); 13 | 14 | export type AppearanceSharedConfig = Infer; 15 | 16 | export type Appearance = { 17 | layout: Layout; 18 | fill_container: boolean; 19 | primary_info: Info; 20 | secondary_info: Info; 21 | icon_type: IconType; 22 | }; 23 | 24 | export const APPEARANCE_FORM_SCHEMA: HaFormSchema[] = [ 25 | { 26 | type: "grid", 27 | name: "", 28 | schema: [ 29 | { name: "layout", selector: { mush_layout: {} } }, 30 | { name: "fill_container", selector: { boolean: {} } }, 31 | ], 32 | }, 33 | { 34 | type: "grid", 35 | name: "", 36 | schema: [ 37 | { name: "primary_info", selector: { mush_info: {} } }, 38 | { name: "secondary_info", selector: { mush_info: {} } }, 39 | { name: "icon_type", selector: { mush_icon_type: {} } }, 40 | ], 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/cards/vacuum-card/vacuum-card-config.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, boolean, object, optional, string } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export const VACUUM_COMMANDS = [ 18 | "on_off", 19 | "start_pause", 20 | "stop", 21 | "locate", 22 | "clean_spot", 23 | "return_home", 24 | ] as const; 25 | 26 | export type VacuumCommand = (typeof VACUUM_COMMANDS)[number]; 27 | 28 | export type VacuumCardConfig = LovelaceCardConfig & 29 | EntitySharedConfig & 30 | AppearanceSharedConfig & 31 | ActionsSharedConfig & { 32 | icon_animation?: boolean; 33 | commands?: VacuumCommand[]; 34 | }; 35 | 36 | export const vacuumCardConfigStruct = assign( 37 | lovelaceCardConfigStruct, 38 | assign( 39 | entitySharedConfigStruct, 40 | appearanceSharedConfigStruct, 41 | actionsSharedConfigStruct 42 | ), 43 | object({ 44 | icon_animation: optional(boolean()), 45 | commands: optional(array(string())), 46 | }) 47 | ); 48 | -------------------------------------------------------------------------------- /src/mushroom.ts: -------------------------------------------------------------------------------- 1 | import { version } from "../package.json"; 2 | import "./utils/form/custom/ha-selector-mushroom-alignment"; 3 | import "./utils/form/custom/ha-selector-mushroom-color"; 4 | import "./utils/form/custom/ha-selector-mushroom-icon-type"; 5 | import "./utils/form/custom/ha-selector-mushroom-info"; 6 | import "./utils/form/custom/ha-selector-mushroom-layout"; 7 | 8 | import "./cards/alarm-control-panel-card/alarm-control-panel-card"; 9 | import "./cards/chips-card/chips-card"; 10 | import "./cards/climate-card/climate-card"; 11 | import "./cards/cover-card/cover-card"; 12 | import "./cards/empty-card/empty-card"; 13 | import "./cards/entity-card/entity-card"; 14 | import "./cards/fan-card/fan-card"; 15 | import "./cards/humidifier-card/humidifier-card"; 16 | import "./cards/legacy-template-card/legacy-template-card"; 17 | import "./cards/light-card/light-card"; 18 | import "./cards/lock-card/lock-card"; 19 | import "./cards/media-player-card/media-player-card"; 20 | import "./cards/number-card/number-card"; 21 | import "./cards/person-card/person-card"; 22 | import "./cards/select-card/select-card"; 23 | import "./cards/template-card/template-card"; 24 | import "./cards/title-card/title-card"; 25 | import "./cards/update-card/update-card"; 26 | import "./cards/vacuum-card/vacuum-card"; 27 | 28 | import "./badges/template/template-badge"; 29 | 30 | console.info( 31 | `%c🍄 Mushroom 🍄 - ${version}`, 32 | "color: #ef5350; font-weight: 700;" 33 | ); 34 | -------------------------------------------------------------------------------- /src/cards/fan-card/fan-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional } from "superstruct"; 2 | import { 3 | actionsSharedConfigStruct, 4 | ActionsSharedConfig, 5 | } from "../../shared/config/actions-config"; 6 | import { 7 | appearanceSharedConfigStruct, 8 | AppearanceSharedConfig, 9 | } from "../../shared/config/appearance-config"; 10 | import { 11 | entitySharedConfigStruct, 12 | EntitySharedConfig, 13 | } from "../../shared/config/entity-config"; 14 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 15 | import { LovelaceCardConfig } from "../../ha"; 16 | 17 | export type FanCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | icon_animation?: boolean; 22 | show_percentage_control?: boolean; 23 | show_oscillate_control?: boolean; 24 | show_direction_control?: boolean; 25 | collapsible_controls?: boolean; 26 | }; 27 | 28 | export const fanCardConfigStruct = assign( 29 | lovelaceCardConfigStruct, 30 | assign( 31 | entitySharedConfigStruct, 32 | appearanceSharedConfigStruct, 33 | actionsSharedConfigStruct 34 | ), 35 | object({ 36 | icon_animation: optional(boolean()), 37 | show_percentage_control: optional(boolean()), 38 | show_oscillate_control: optional(boolean()), 39 | show_direction_control: optional(boolean()), 40 | collapsible_controls: optional(boolean()), 41 | }) 42 | ); 43 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/common/entity/turn-on-off-entities.ts: -------------------------------------------------------------------------------- 1 | import { STATES_OFF } from "../../../../common/const"; 2 | import { computeDomain } from "../../../../common/entity/compute_domain"; 3 | import { HomeAssistant } from "../../../../types"; 4 | 5 | export const turnOnOffEntities = ( 6 | hass: HomeAssistant, 7 | entityIds: string[], 8 | turnOn = true 9 | ): void => { 10 | const domainsToCall = {}; 11 | entityIds.forEach((entityId) => { 12 | const stateObj = hass.states[entityId]; 13 | if (stateObj && STATES_OFF.includes(stateObj.state) === turnOn) { 14 | const stateDomain = computeDomain(entityId); 15 | const serviceDomain = ["cover", "lock"].includes(stateDomain) 16 | ? stateDomain 17 | : "homeassistant"; 18 | 19 | if (!(serviceDomain in domainsToCall)) { 20 | domainsToCall[serviceDomain] = []; 21 | } 22 | domainsToCall[serviceDomain].push(entityId); 23 | } 24 | }); 25 | 26 | Object.keys(domainsToCall).forEach((domain) => { 27 | let service; 28 | switch (domain) { 29 | case "lock": 30 | service = turnOn ? "unlock" : "lock"; 31 | break; 32 | case "cover": 33 | service = turnOn ? "open_cover" : "close_cover"; 34 | break; 35 | default: 36 | service = turnOn ? "turn_on" : "turn_off"; 37 | } 38 | 39 | const entities = domainsToCall[domain]; 40 | hass.callService(domain, service, { entity_id: entities }); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/cards/climate-card/climate-card-config.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, boolean, object, optional, string } from "superstruct"; 2 | import { HvacMode, LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export const HVAC_MODES: HvacMode[] = [ 18 | "auto", 19 | "heat_cool", 20 | "heat", 21 | "cool", 22 | "dry", 23 | "fan_only", 24 | "off", 25 | ]; 26 | 27 | export type ClimateCardConfig = LovelaceCardConfig & 28 | EntitySharedConfig & 29 | AppearanceSharedConfig & 30 | ActionsSharedConfig & { 31 | show_temperature_control?: false; 32 | hvac_modes?: HvacMode[]; 33 | collapsible_controls?: boolean; 34 | }; 35 | 36 | export const climateCardConfigStruct = assign( 37 | lovelaceCardConfigStruct, 38 | assign( 39 | entitySharedConfigStruct, 40 | appearanceSharedConfigStruct, 41 | actionsSharedConfigStruct 42 | ), 43 | object({ 44 | show_temperature_control: optional(boolean()), 45 | hvac_modes: optional(array(string())), 46 | collapsible_controls: optional(boolean()), 47 | }) 48 | ); 49 | -------------------------------------------------------------------------------- /src/utils/appearance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Appearance, 3 | AppearanceSharedConfig, 4 | } from "../shared/config/appearance-config"; 5 | import { IconType, Info } from "./info"; 6 | import { Layout } from "./layout"; 7 | 8 | type AdditionalConfig = { [key: string]: any }; 9 | 10 | export function computeAppearance( 11 | config: AppearanceSharedConfig & AdditionalConfig 12 | ): Appearance { 13 | return { 14 | layout: config.layout ?? getDefaultLayout(config), 15 | fill_container: config.fill_container ?? false, 16 | primary_info: config.primary_info ?? getDefaultPrimaryInfo(config), 17 | secondary_info: config.secondary_info ?? getDefaultSecondaryInfo(config), 18 | icon_type: config.icon_type ?? getDefaultIconType(config), 19 | }; 20 | } 21 | 22 | function getDefaultLayout(config: AdditionalConfig): Layout { 23 | if (config.vertical) { 24 | return "vertical"; 25 | } 26 | return "default"; 27 | } 28 | 29 | function getDefaultIconType(config: AdditionalConfig): IconType { 30 | if (config.hide_icon) { 31 | return "none"; 32 | } 33 | if (config.use_entity_picture || config.use_media_artwork) { 34 | return "entity-picture"; 35 | } 36 | return "icon"; 37 | } 38 | 39 | function getDefaultPrimaryInfo(config: AdditionalConfig): Info { 40 | if (config.hide_name) { 41 | return "none"; 42 | } 43 | return "name"; 44 | } 45 | 46 | function getDefaultSecondaryInfo(config: AdditionalConfig): Info { 47 | if (config.hide_state) { 48 | return "none"; 49 | } 50 | return "state"; 51 | } 52 | -------------------------------------------------------------------------------- /src/cards/light-card/light-card-config.ts: -------------------------------------------------------------------------------- 1 | import { assign, boolean, object, optional, string } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export type LightCardConfig = LovelaceCardConfig & 18 | EntitySharedConfig & 19 | AppearanceSharedConfig & 20 | ActionsSharedConfig & { 21 | icon_color?: string; 22 | show_brightness_control?: boolean; 23 | show_color_temp_control?: boolean; 24 | show_color_control?: boolean; 25 | collapsible_controls?: boolean; 26 | use_light_color?: boolean; 27 | }; 28 | 29 | export const lightCardConfigStruct = assign( 30 | lovelaceCardConfigStruct, 31 | assign( 32 | entitySharedConfigStruct, 33 | appearanceSharedConfigStruct, 34 | actionsSharedConfigStruct 35 | ), 36 | object({ 37 | icon_color: optional(string()), 38 | show_brightness_control: optional(boolean()), 39 | show_color_temp_control: optional(boolean()), 40 | show_color_control: optional(boolean()), 41 | collapsible_controls: optional(boolean()), 42 | use_light_color: optional(boolean()), 43 | }) 44 | ); 45 | -------------------------------------------------------------------------------- /src/ha/data/translation.ts: -------------------------------------------------------------------------------- 1 | export enum NumberFormat { 2 | language = "language", 3 | system = "system", 4 | comma_decimal = "comma_decimal", 5 | decimal_comma = "decimal_comma", 6 | space_comma = "space_comma", 7 | none = "none", 8 | } 9 | 10 | export enum TimeFormat { 11 | language = "language", 12 | system = "system", 13 | am_pm = "12", 14 | twenty_four = "24", 15 | } 16 | 17 | export enum TimeZone { 18 | local = "local", 19 | server = "server", 20 | } 21 | 22 | export enum DateFormat { 23 | language = "language", 24 | system = "system", 25 | DMY = "DMY", 26 | MDY = "MDY", 27 | YMD = "YMD", 28 | } 29 | 30 | export enum FirstWeekday { 31 | language = "language", 32 | monday = "monday", 33 | tuesday = "tuesday", 34 | wednesday = "wednesday", 35 | thursday = "thursday", 36 | friday = "friday", 37 | saturday = "saturday", 38 | sunday = "sunday", 39 | } 40 | 41 | export interface FrontendLocaleData { 42 | language: string; 43 | number_format: NumberFormat; 44 | time_format: TimeFormat; 45 | date_format: DateFormat; 46 | first_weekday: FirstWeekday; 47 | time_zone: TimeZone; 48 | } 49 | 50 | declare global { 51 | interface FrontendUserData { 52 | language: FrontendLocaleData; 53 | } 54 | } 55 | 56 | export type TranslationCategory = 57 | | "title" 58 | | "state" 59 | | "entity" 60 | | "entity_component" 61 | | "config" 62 | | "config_panel" 63 | | "options" 64 | | "device_automation" 65 | | "mfa_setup" 66 | | "system_health" 67 | | "device_class" 68 | | "application_credentials" 69 | | "issues" 70 | | "selector"; 71 | -------------------------------------------------------------------------------- /src/shared/form/mushroom-select.ts: -------------------------------------------------------------------------------- 1 | import { SelectBase } from "@material/mwc-select/mwc-select-base"; 2 | import { styles } from "@material/mwc-select/mwc-select.css"; 3 | import { css, CSSResult, html, nothing } from "lit"; 4 | import { customElement, property } from "lit/decorators.js"; 5 | import { debounce, nextRender } from "../../ha"; 6 | 7 | @customElement("mushroom-select") 8 | export class MushroomSelect extends SelectBase { 9 | // @ts-ignore 10 | @property({ type: Boolean }) public icon?: boolean; 11 | 12 | // @ts-ignore 13 | protected override renderLeadingIcon() { 14 | if (!this.icon) { 15 | return nothing; 16 | } 17 | 18 | return html``; 21 | } 22 | 23 | connectedCallback() { 24 | super.connectedCallback(); 25 | window.addEventListener("translations-updated", this._translationsUpdated); 26 | } 27 | 28 | disconnectedCallback() { 29 | super.disconnectedCallback(); 30 | window.removeEventListener( 31 | "translations-updated", 32 | this._translationsUpdated 33 | ); 34 | } 35 | 36 | private _translationsUpdated = debounce(async () => { 37 | await nextRender(); 38 | this.layoutOptions(); 39 | }, 500); 40 | 41 | static override styles: CSSResult[] = [ 42 | // @ts-ignore 43 | styles, 44 | css` 45 | .mdc-select__anchor { 46 | height: var(--select-height, 56px) !important; 47 | } 48 | `, 49 | ]; 50 | } 51 | 52 | declare global { 53 | interface HTMLElementTagNameMap { 54 | "mushroom-select": MushroomSelect; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/cards/legacy-template-card/legacy-template-card-config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | array, 3 | assign, 4 | boolean, 5 | object, 6 | optional, 7 | string, 8 | union, 9 | } from "superstruct"; 10 | import { LovelaceCardConfig } from "../../ha"; 11 | import { 12 | ActionsSharedConfig, 13 | actionsSharedConfigStruct, 14 | } from "../../shared/config/actions-config"; 15 | import { 16 | AppearanceSharedConfig, 17 | appearanceSharedConfigStruct, 18 | } from "../../shared/config/appearance-config"; 19 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 20 | 21 | export type LegacyTemplateCardConfig = LovelaceCardConfig & 22 | AppearanceSharedConfig & 23 | ActionsSharedConfig & { 24 | entity?: string; 25 | icon?: string; 26 | icon_color?: string; 27 | primary?: string; 28 | secondary?: string; 29 | badge_icon?: string; 30 | badge_color?: string; 31 | picture?: string; 32 | multiline_secondary?: boolean; 33 | entity_id?: string | string[]; 34 | }; 35 | 36 | export const legacyTemplateCardConfigStruct = assign( 37 | lovelaceCardConfigStruct, 38 | assign(appearanceSharedConfigStruct, actionsSharedConfigStruct), 39 | object({ 40 | entity: optional(string()), 41 | icon: optional(string()), 42 | icon_color: optional(string()), 43 | primary: optional(string()), 44 | secondary: optional(string()), 45 | badge_icon: optional(string()), 46 | badge_color: optional(string()), 47 | picture: optional(string()), 48 | multiline_secondary: optional(boolean()), 49 | entity_id: optional(union([string(), array(string())])), 50 | }) 51 | ); 52 | -------------------------------------------------------------------------------- /docs/cards/title.md: -------------------------------------------------------------------------------- 1 | # Title card 2 | 3 | ![Title light](../images/title-light.png) 4 | ![Title dark](../images/title-dark.png) 5 | 6 | ## Description 7 | 8 | A Title block to separate sections of cards 9 | 10 | ## Configuration variables 11 | 12 | All the options are available in the lovelace editor but you can use `yaml` if you want. 13 | 14 | | Name | Type | Default | Description | 15 | | :-------------------- | :-------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------- | 16 | | `title` | string | Optional | Title to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | 17 | | `subtitle` | string | Optional | Subtitle to render. May contain [templates](https://www.home-assistant.io/docs/configuration/templating/). | 18 | | `entity_id` | `string` `list` | Optional | Only reacts to the state changes of these entities. This can be used if the automatic analysis fails to find all relevant entities. | 19 | | `title_tap_action` | action | `none` | Home assistant action to perform on title tap | 20 | | `subtitle_tap_action` | action | `none` | Home assistant action to perform on subtitle tap | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Requirements 9 | options: 10 | - label: I have updated Mushroom to the latest available version 11 | required: true 12 | - label: I did a search to see if there is a similar issue or if a pull request is open. 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Is your feature request related to a problem? 17 | description: > 18 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: > 25 | A clear and concise description of what you want to happen. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you've considered 31 | description: > 32 | A clear and concise description of any alternative solutions or features you've considered. 33 | - type: textarea 34 | attributes: 35 | label: Additional context 36 | description: > 37 | Add any other context or screenshots about the feature request. 38 | - type: markdown 39 | attributes: 40 | value: > 41 | Thanks for contributing 🎉 42 | -------------------------------------------------------------------------------- /src/utils/base-element.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, LitElement, PropertyValues } from "lit"; 2 | import { property } from "lit/decorators.js"; 3 | import { HomeAssistant } from "../ha"; 4 | import "../shared/badge-icon"; 5 | import "../shared/card"; 6 | import "../shared/shape-avatar"; 7 | import "../shared/shape-icon"; 8 | import "../shared/state-info"; 9 | import "../shared/state-item"; 10 | import { animations } from "../utils/entity-styles"; 11 | import { defaultColorCss, defaultDarkColorCss } from "./colors"; 12 | import { themeColorCss, themeVariables } from "./theme"; 13 | 14 | export function computeDarkMode(hass?: HomeAssistant): boolean { 15 | if (!hass) return false; 16 | return (hass.themes as any).darkMode as boolean; 17 | } 18 | export class MushroomBaseElement extends LitElement { 19 | @property({ attribute: false }) public hass!: HomeAssistant; 20 | 21 | protected updated(changedProps: PropertyValues): void { 22 | super.updated(changedProps); 23 | if (changedProps.has("hass") && this.hass) { 24 | const currentDarkMode = computeDarkMode(changedProps.get("hass")); 25 | const newDarkMode = computeDarkMode(this.hass); 26 | if (currentDarkMode !== newDarkMode) { 27 | this.toggleAttribute("dark-mode", newDarkMode); 28 | } 29 | } 30 | } 31 | 32 | static get styles(): CSSResultGroup { 33 | return [ 34 | animations, 35 | css` 36 | :host { 37 | ${defaultColorCss} 38 | } 39 | :host([dark-mode]) { 40 | ${defaultDarkColorCss} 41 | } 42 | :host { 43 | ${themeColorCss} 44 | ${themeVariables} 45 | } 46 | `, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ha/data/entity.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { computeDomain } from "../common/entity/compute_domain"; 3 | 4 | export const UNAVAILABLE = "unavailable"; 5 | export const UNKNOWN = "unknown"; 6 | 7 | export const ON = "on"; 8 | export const OFF = "off"; 9 | 10 | const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF]; 11 | 12 | export function isActive(stateObj: HassEntity) { 13 | const domain = computeDomain(stateObj.entity_id); 14 | const state = stateObj.state; 15 | 16 | if (["button", "input_button", "scene"].includes(domain)) { 17 | return state !== UNAVAILABLE; 18 | } 19 | 20 | if (OFF_STATES.includes(state)) { 21 | return false; 22 | } 23 | 24 | // Custom cases 25 | switch (domain) { 26 | case "cover": 27 | case "valve": 28 | return !["closed", "closing"].includes(state); 29 | case "device_tracker": 30 | case "person": 31 | return state !== "not_home"; 32 | case "media_player": 33 | return state !== "standby"; 34 | case "vacuum": 35 | return !["idle", "docked", "paused"].includes(state); 36 | case "plant": 37 | return state === "problem"; 38 | default: 39 | return true; 40 | } 41 | } 42 | 43 | export function isAvailable(stateObj: HassEntity) { 44 | return stateObj.state !== UNAVAILABLE; 45 | } 46 | 47 | export function isOff(stateObj: HassEntity) { 48 | return stateObj.state === OFF; 49 | } 50 | 51 | export function isUnknown(stateObj: HassEntity) { 52 | return stateObj.state === UNKNOWN; 53 | } 54 | 55 | export function getEntityPicture(stateObj: HassEntity) { 56 | return ( 57 | (stateObj.attributes.entity_picture_local as string | undefined) || 58 | stateObj.attributes.entity_picture 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/cards/fan-card/controls/fan-direction-control.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 3 | import { customElement, property } from "lit/decorators.js"; 4 | import { classMap } from "lit/directives/class-map.js"; 5 | import { HomeAssistant, isActive } from "../../../ha"; 6 | import "../../../shared/slider"; 7 | import { isOscillating } from "../utils"; 8 | 9 | @customElement("mushroom-fan-direction-control") 10 | export class FanPercentageControl extends LitElement { 11 | @property({ attribute: false }) public hass!: HomeAssistant; 12 | 13 | @property({ attribute: false }) public entity!: HassEntity; 14 | 15 | private _onTap(e: MouseEvent): void { 16 | e.stopPropagation(); 17 | const currentDirection = this.entity.attributes.direction; 18 | const newDirection = currentDirection === "forward" ? "reverse" : "forward"; 19 | 20 | this.hass.callService("fan", "set_direction", { 21 | entity_id: this.entity.entity_id, 22 | direction: newDirection, 23 | }); 24 | } 25 | 26 | protected render(): TemplateResult { 27 | const currentDirection = this.entity.attributes.direction; 28 | const active = isActive(this.entity); 29 | 30 | return html` 31 | <mushroom-button 32 | @click=${this._onTap} 33 | .disabled=${!active} 34 | > 35 | <ha-icon 36 | .icon=${currentDirection === "reverse" 37 | ? "mdi:rotate-left" 38 | : "mdi:rotate-right"} 39 | ></ha-icon> 40 | </mushroom-button> 41 | `; 42 | } 43 | 44 | static get styles(): CSSResultGroup { 45 | return css` 46 | :host { 47 | display: flex; 48 | } 49 | `; 50 | } 51 | } -------------------------------------------------------------------------------- /src/cards/cover-card/controls/cover-position-control.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { CoverEntity, HomeAssistant, isAvailable } from "../../../ha"; 4 | import "../../../shared/slider"; 5 | import { getPosition } from "../utils"; 6 | 7 | @customElement("mushroom-cover-position-control") 8 | export class CoverPositionControl extends LitElement { 9 | @property({ attribute: false }) public hass!: HomeAssistant; 10 | 11 | @property({ attribute: false }) public entity!: CoverEntity; 12 | 13 | private onChange(e: CustomEvent<{ value: number }>): void { 14 | const value = e.detail.value; 15 | 16 | this.hass.callService("cover", "set_cover_position", { 17 | entity_id: this.entity.entity_id, 18 | position: value, 19 | }); 20 | } 21 | 22 | onCurrentChange(e: CustomEvent<{ value?: number }>): void { 23 | const value = e.detail.value; 24 | this.dispatchEvent( 25 | new CustomEvent("current-change", { 26 | detail: { 27 | value, 28 | }, 29 | }) 30 | ); 31 | } 32 | 33 | protected render(): TemplateResult { 34 | const position = getPosition(this.entity); 35 | 36 | return html` 37 | <mushroom-slider 38 | .value=${position} 39 | .disabled=${!isAvailable(this.entity)} 40 | .showActive=${true} 41 | @change=${this.onChange} 42 | @current-change=${this.onCurrentChange} 43 | /> 44 | `; 45 | } 46 | 47 | static get styles(): CSSResultGroup { 48 | return css` 49 | mushroom-slider { 50 | --main-color: var(--slider-color); 51 | --bg-color: var(--slider-bg-color); 52 | } 53 | `; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/back-chip-editor.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, nothing } from "lit"; 2 | import { customElement, property, state } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import { HaFormSchema } from "../../../utils/form/ha-form"; 5 | import { computeChipEditorComponentName } from "../../../utils/lovelace/chip/chip-element"; 6 | import { EntityChipConfig } from "../../../utils/lovelace/chip/types"; 7 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 8 | import { DEFAULT_BACK_ICON } from "./back-chip"; 9 | 10 | const SCHEMA: HaFormSchema[] = [ 11 | { name: "icon", selector: { icon: { placeholder: DEFAULT_BACK_ICON } } }, 12 | ]; 13 | 14 | @customElement(computeChipEditorComponentName("back")) 15 | export class BackChipEditor extends LitElement implements LovelaceChipEditor { 16 | @property({ attribute: false }) public hass?: HomeAssistant; 17 | 18 | @state() private _config?: EntityChipConfig; 19 | 20 | public setConfig(config: EntityChipConfig): void { 21 | this._config = config; 22 | } 23 | 24 | private _computeLabel = (schema: HaFormSchema) => { 25 | return this.hass!.localize( 26 | `ui.panel.lovelace.editor.card.generic.${schema.name}` 27 | ); 28 | }; 29 | 30 | protected render() { 31 | if (!this.hass || !this._config) { 32 | return nothing; 33 | } 34 | 35 | return html` 36 | <ha-form 37 | .hass=${this.hass} 38 | .data=${this._config} 39 | .schema=${SCHEMA} 40 | .computeLabel=${this._computeLabel} 41 | @value-changed=${this._valueChanged} 42 | ></ha-form> 43 | `; 44 | } 45 | 46 | private _valueChanged(ev: CustomEvent): void { 47 | fireEvent(this, "config-changed", { config: ev.detail.value }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/menu-chip-editor.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, nothing } from "lit"; 2 | import { customElement, property, state } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import { HaFormSchema } from "../../../utils/form/ha-form"; 5 | import { computeChipEditorComponentName } from "../../../utils/lovelace/chip/chip-element"; 6 | import { EntityChipConfig } from "../../../utils/lovelace/chip/types"; 7 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 8 | import { DEFAULT_MENU_ICON } from "./menu-chip"; 9 | 10 | const SCHEMA: HaFormSchema[] = [ 11 | { name: "icon", selector: { icon: { placeholder: DEFAULT_MENU_ICON } } }, 12 | ]; 13 | 14 | @customElement(computeChipEditorComponentName("menu")) 15 | export class MenuChipEditor extends LitElement implements LovelaceChipEditor { 16 | @property({ attribute: false }) public hass?: HomeAssistant; 17 | 18 | @state() private _config?: EntityChipConfig; 19 | 20 | public setConfig(config: EntityChipConfig): void { 21 | this._config = config; 22 | } 23 | 24 | private _computeLabel = (schema: HaFormSchema) => { 25 | return this.hass!.localize( 26 | `ui.panel.lovelace.editor.card.generic.${schema.name}` 27 | ); 28 | }; 29 | 30 | protected render() { 31 | if (!this.hass || !this._config) { 32 | return nothing; 33 | } 34 | 35 | return html` 36 | <ha-form 37 | .hass=${this.hass} 38 | .data=${this._config} 39 | .schema=${SCHEMA} 40 | .computeLabel=${this._computeLabel} 41 | @value-changed=${this._valueChanged} 42 | ></ha-form> 43 | `; 44 | } 45 | 46 | private _valueChanged(ev: CustomEvent): void { 47 | fireEvent(this, "config-changed", { config: ev.detail.value }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ha/common/structs/handle-errors.ts: -------------------------------------------------------------------------------- 1 | import { StructError } from "superstruct"; 2 | import type { HomeAssistant } from "../../types"; 3 | 4 | export const handleStructError = ( 5 | hass: HomeAssistant, 6 | err: Error 7 | ): { warnings: string[]; errors?: string[] } => { 8 | if (!(err instanceof StructError)) { 9 | return { warnings: [err.message], errors: undefined }; 10 | } 11 | const errors: string[] = []; 12 | const warnings: string[] = []; 13 | for (const failure of err.failures()) { 14 | if (failure.value === undefined) { 15 | errors.push( 16 | hass.localize( 17 | "ui.errors.config.key_missing", 18 | "key", 19 | failure.path.join(".") 20 | ) 21 | ); 22 | } else if (failure.type === "never") { 23 | warnings.push( 24 | hass.localize( 25 | "ui.errors.config.key_not_expected", 26 | "key", 27 | failure.path.join(".") 28 | ) 29 | ); 30 | } else if (failure.type === "union") { 31 | continue; 32 | } else if (failure.type === "enums") { 33 | warnings.push( 34 | hass.localize( 35 | "ui.errors.config.key_wrong_type", 36 | "key", 37 | failure.path.join("."), 38 | "type_correct", 39 | failure.message.replace("Expected ", "").split(", ")[0], 40 | "type_wrong", 41 | JSON.stringify(failure.value) 42 | ) 43 | ); 44 | } else { 45 | warnings.push( 46 | hass.localize( 47 | "ui.errors.config.key_wrong_type", 48 | "key", 49 | failure.path.join("."), 50 | "type_correct", 51 | failure.refinement || failure.type, 52 | "type_wrong", 53 | JSON.stringify(failure.value) 54 | ) 55 | ); 56 | } 57 | } 58 | return { warnings, errors }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/cards/fan-card/controls/fan-oscillate-control.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 3 | import { customElement, property } from "lit/decorators.js"; 4 | import { classMap } from "lit/directives/class-map.js"; 5 | import { HomeAssistant, isActive } from "../../../ha"; 6 | import "../../../shared/slider"; 7 | import { isOscillating } from "../utils"; 8 | 9 | @customElement("mushroom-fan-oscillate-control") 10 | export class FanPercentageControl extends LitElement { 11 | @property({ attribute: false }) public hass!: HomeAssistant; 12 | 13 | @property({ attribute: false }) public entity!: HassEntity; 14 | 15 | private _onTap(e: MouseEvent): void { 16 | e.stopPropagation(); 17 | const oscillating = isOscillating(this.entity); 18 | 19 | this.hass.callService("fan", "oscillate", { 20 | entity_id: this.entity.entity_id, 21 | oscillating: !oscillating, 22 | }); 23 | } 24 | 25 | protected render(): TemplateResult { 26 | const oscillating = isOscillating(this.entity); 27 | const active = isActive(this.entity); 28 | 29 | return html` 30 | <mushroom-button 31 | class=${classMap({ active: oscillating })} 32 | @click=${this._onTap} 33 | .disabled=${!active} 34 | > 35 | <ha-icon 36 | .icon=${oscillating 37 | ? "mdi:arrow-oscillating" 38 | : "mdi:arrow-oscillating-off"} 39 | ></ha-icon> 40 | </mushroom-button> 41 | `; 42 | } 43 | 44 | static get styles(): CSSResultGroup { 45 | return css` 46 | :host { 47 | display: flex; 48 | } 49 | mushroom-button.active { 50 | --icon-color: rgb(var(--rgb-state-fan)); 51 | --bg-color: rgba(var(--rgb-state-fan), 0.2); 52 | } 53 | `; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cards/media-player-card/controls/media-player-media-control.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { computeRTL, HomeAssistant, MediaPlayerEntity } from "../../../ha"; 4 | import { MediaPlayerMediaControl } from "../media-player-card-config"; 5 | import { computeMediaControls, handleMediaControlClick } from "../utils"; 6 | 7 | export const isMediaControlVisible = ( 8 | entity: MediaPlayerEntity, 9 | controls?: MediaPlayerMediaControl[] 10 | ) => computeMediaControls(entity, controls ?? []).length > 0; 11 | 12 | @customElement("mushroom-media-player-media-control") 13 | export class MediaPlayerMediaControls extends LitElement { 14 | @property({ attribute: false }) public hass!: HomeAssistant; 15 | 16 | @property({ attribute: false }) public entity!: MediaPlayerEntity; 17 | 18 | @property({ attribute: false }) public controls!: MediaPlayerMediaControl[]; 19 | 20 | @property({ type: Boolean }) public fill: boolean = false; 21 | 22 | private _handleClick(e: MouseEvent): void { 23 | e.stopPropagation(); 24 | const action = (e.target! as any).action as string; 25 | handleMediaControlClick(this.hass, this.entity, action!); 26 | } 27 | 28 | protected render(): TemplateResult { 29 | const rtl = computeRTL(this.hass); 30 | 31 | const controls = computeMediaControls(this.entity, this.controls); 32 | 33 | return html` 34 | <mushroom-button-group .fill=${this.fill} ?rtl=${rtl}> 35 | ${controls.map( 36 | (control) => html` 37 | <mushroom-button 38 | .action=${control.action} 39 | @click=${this._handleClick} 40 | > 41 | <ha-icon .icon=${control.icon}></ha-icon> 42 | </mushroom-button> 43 | ` 44 | )} 45 | </mushroom-button-group> 46 | `; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cards/light-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { rgb, lch } from "culori"; 2 | import { 3 | LightColorMode, 4 | LightEntity, 5 | lightSupportsColor, 6 | lightSupportsBrightness, 7 | } from "../../ha"; 8 | 9 | export function getBrightness(entity: LightEntity): number | undefined { 10 | return entity.attributes.brightness != null 11 | ? Math.max(Math.round((entity.attributes.brightness * 100) / 255), 1) 12 | : undefined; 13 | } 14 | 15 | export function getColorTemp(entity: LightEntity): number | undefined { 16 | return entity.attributes.color_temp != null 17 | ? Math.round(entity.attributes.color_temp) 18 | : undefined; 19 | } 20 | 21 | export function getRGBColor(entity: LightEntity): number[] | undefined { 22 | return entity.attributes.rgb_color != null 23 | ? entity.attributes.rgb_color 24 | : undefined; 25 | } 26 | 27 | export function isColorLight(rgb: number[]): boolean { 28 | const color = { 29 | mode: "rgb" as const, 30 | r: rgb[0] / 255, 31 | g: rgb[1] / 255, 32 | b: rgb[2] / 255, 33 | }; 34 | const lchColor = lch(color); 35 | return (lchColor?.l || 0) > 96; 36 | } 37 | export function isColorSuperLight(rgb: number[]): boolean { 38 | const color = { 39 | mode: "rgb" as const, 40 | r: rgb[0] / 255, 41 | g: rgb[1] / 255, 42 | b: rgb[2] / 255, 43 | }; 44 | const lchColor = lch(color); 45 | return (lchColor?.l || 0) > 97; 46 | } 47 | 48 | export function supportsColorTempControl(entity: LightEntity): boolean { 49 | return ( 50 | entity.attributes.supported_color_modes?.some((m) => 51 | [LightColorMode.COLOR_TEMP].includes(m) 52 | ) ?? false 53 | ); 54 | } 55 | 56 | export function supportsColorControl(entity: LightEntity): boolean { 57 | return lightSupportsColor(entity); 58 | } 59 | 60 | export function supportsBrightnessControl(entity: LightEntity): boolean { 61 | return lightSupportsBrightness(entity); 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mushroom-cards", 3 | "version": "5.0.8", 4 | "description": "Home Assistant Mushroom Cards", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "rollup -c --watch --bundleConfigAsCjs", 8 | "build": "rollup -c --bundleConfigAsCjs", 9 | "format": "prettier --write .", 10 | "start:hass-stable": "docker run --rm -p8123:8123 -v ./.hass_dev:/config homeassistant/home-assistant:stable", 11 | "start:hass": "docker run --rm -p8123:8123 -v ./.hass_dev:/config homeassistant/home-assistant:beta", 12 | "start:hass-dev": "docker run --rm -p8123:8123 -v ./.hass_dev:/config homeassistant/home-assistant:dev" 13 | }, 14 | "author": "Paul Bottein", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/piitaya/lovelace-mushroom" 18 | }, 19 | "license": "ISC", 20 | "dependencies": { 21 | "@material/mwc-select": "^0.27.0", 22 | "@material/mwc-textfield": "^0.27.0", 23 | "culori": "^4.0.1", 24 | "hammerjs": "^2.0.8", 25 | "home-assistant-js-websocket": "^9.5.0", 26 | "intl-messageformat": "^10.7.16", 27 | "lit": "^3.3.1", 28 | "memoize-one": "^6.0.0", 29 | "object-hash": "^3.0.0", 30 | "sortablejs": "^1.15.6", 31 | "superstruct": "^2.0.2" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.28.4", 35 | "@babel/preset-env": "^7.28.3", 36 | "@material/mwc-ripple": "^0.27.0", 37 | "@rollup/plugin-babel": "^6.0.4", 38 | "@rollup/plugin-commonjs": "^28.0.6", 39 | "@rollup/plugin-json": "^6.1.0", 40 | "@rollup/plugin-node-resolve": "^16.0.1", 41 | "@rollup/plugin-terser": "^0.4.4", 42 | "@rollup/plugin-typescript": "^12.1.4", 43 | "@types/culori": "^4.0.1", 44 | "@types/hammerjs": "^2.0.46", 45 | "eslint": "^9.36.0", 46 | "prettier": "^3.6.2", 47 | "rollup": "^4.50.1", 48 | "rollup-plugin-serve": "^1.1.1", 49 | "typescript": "^5.9.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/cards/fan-card/controls/fan-percentage-control.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 3 | import { customElement, property } from "lit/decorators.js"; 4 | import { HomeAssistant, isActive, isAvailable } from "../../../ha"; 5 | import "../../../shared/slider"; 6 | import { computePercentageStep, getPercentage } from "../utils"; 7 | 8 | @customElement("mushroom-fan-percentage-control") 9 | export class FanPercentageControl extends LitElement { 10 | @property({ attribute: false }) public hass!: HomeAssistant; 11 | 12 | @property({ attribute: false }) public entity!: HassEntity; 13 | 14 | onChange(e: CustomEvent<{ value: number }>): void { 15 | const value = e.detail.value; 16 | this.hass.callService("fan", "set_percentage", { 17 | entity_id: this.entity.entity_id, 18 | percentage: value, 19 | }); 20 | } 21 | 22 | onCurrentChange(e: CustomEvent<{ value?: number }>): void { 23 | const value = e.detail.value; 24 | this.dispatchEvent( 25 | new CustomEvent("current-change", { 26 | detail: { 27 | value, 28 | }, 29 | }) 30 | ); 31 | } 32 | 33 | protected render(): TemplateResult { 34 | const percentage = getPercentage(this.entity); 35 | 36 | return html` 37 | <mushroom-slider 38 | .value=${percentage} 39 | .disabled=${!isAvailable(this.entity)} 40 | .inactive=${!isActive(this.entity)} 41 | .showActive=${true} 42 | @change=${this.onChange} 43 | @current-change=${this.onCurrentChange} 44 | step=${computePercentageStep(this.entity)} 45 | /> 46 | `; 47 | } 48 | 49 | static get styles(): CSSResultGroup { 50 | return css` 51 | mushroom-slider { 52 | --main-color: rgb(var(--rgb-state-fan)); 53 | --bg-color: rgba(var(--rgb-state-fan), 0.2); 54 | } 55 | `; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ha/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./common/const"; 2 | export * from "./common/dom/fire_event"; 3 | export * from "./common/dom/get_main_window"; 4 | export * from "./common/entity/compute_domain"; 5 | export * from "./common/entity/compute_state_domain"; 6 | export * from "./common/entity/supports-feature"; 7 | export * from "./common/number/clamp"; 8 | export * from "./common/number/format_number"; 9 | export * from "./common/number/round"; 10 | export * from "./common/structs/handle-errors"; 11 | export * from "./common/translations/localize"; 12 | export * from "./common/util/compute_rtl"; 13 | export * from "./common/util/debounce"; 14 | export * from "./common/util/deep-equal"; 15 | export * from "./common/util/render-status"; 16 | export * from "./data/climate"; 17 | export * from "./data/cover"; 18 | export * from "./data/entity"; 19 | export * from "./data/fan"; 20 | export * from "./data/humidifier"; 21 | export * from "./data/light"; 22 | export * from "./data/lock"; 23 | export * from "./data/lovelace"; 24 | export * from "./data/main_window"; 25 | export * from "./data/media-player"; 26 | export * from "./data/translation"; 27 | export * from "./data/update"; 28 | export * from "./data/vacuum"; 29 | export * from "./data/ws-templates"; 30 | export * from "./data/ws-themes"; 31 | export * from "./panels/lovelace/card-features/types"; 32 | export * from "./panels/lovelace/common/directives/action-handler-directive"; 33 | export * from "./panels/lovelace/common/entity/turn-on-off-entities"; 34 | export * from "./panels/lovelace/common/entity/turn-on-off-entity"; 35 | export * from "./panels/lovelace/common/handle-actions"; 36 | export * from "./panels/lovelace/common/has-action"; 37 | export * from "./panels/lovelace/common/validate-condition"; 38 | export * from "./panels/lovelace/editor/structs/action-struct"; 39 | export * from "./panels/lovelace/types"; 40 | export * from "./resources/ha-sortable-styles"; 41 | export * from "./types"; 42 | export * from "./util"; 43 | -------------------------------------------------------------------------------- /src/ha/data/climate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | 6 | export type HvacMode = 7 | | "off" 8 | | "heat" 9 | | "cool" 10 | | "heat_cool" 11 | | "auto" 12 | | "dry" 13 | | "fan_only"; 14 | 15 | export const CLIMATE_PRESET_NONE = "none"; 16 | 17 | export type HvacAction = "off" | "heating" | "cooling" | "drying" | "idle"; 18 | 19 | export type ClimateEntity = HassEntityBase & { 20 | attributes: HassEntityAttributeBase & { 21 | hvac_mode: HvacMode; 22 | hvac_modes: HvacMode[]; 23 | hvac_action?: HvacAction; 24 | current_temperature: number; 25 | min_temp: number; 26 | max_temp: number; 27 | temperature: number; 28 | target_temp_step?: number; 29 | target_temp_high?: number; 30 | target_temp_low?: number; 31 | humidity?: number; 32 | current_humidity?: number; 33 | target_humidity_low?: number; 34 | target_humidity_high?: number; 35 | min_humidity?: number; 36 | max_humidity?: number; 37 | fan_mode?: string; 38 | fan_modes?: string[]; 39 | preset_mode?: string; 40 | preset_modes?: string[]; 41 | swing_mode?: string; 42 | swing_modes?: string[]; 43 | aux_heat?: "on" | "off"; 44 | }; 45 | }; 46 | 47 | export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1; 48 | export const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2; 49 | export const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4; 50 | export const CLIMATE_SUPPORT_FAN_MODE = 8; 51 | export const CLIMATE_SUPPORT_PRESET_MODE = 16; 52 | export const CLIMATE_SUPPORT_SWING_MODE = 32; 53 | export const CLIMATE_SUPPORT_AUX_HEAT = 64; 54 | 55 | const hvacModeOrdering: { [key in HvacMode]: number } = { 56 | auto: 1, 57 | heat_cool: 2, 58 | heat: 3, 59 | cool: 4, 60 | dry: 5, 61 | fan_only: 6, 62 | off: 7, 63 | }; 64 | 65 | export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) => 66 | hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; 67 | -------------------------------------------------------------------------------- /src/shared/button-group.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { classMap } from "lit/directives/class-map.js"; 4 | 5 | @customElement("mushroom-button-group") 6 | export class MushroomButtonGroup extends LitElement { 7 | @property() public fill: boolean = false; 8 | 9 | @property() public rtl: boolean = false; 10 | 11 | protected render(): TemplateResult { 12 | return html` 13 | <div 14 | class=${classMap({ 15 | container: true, 16 | fill: this.fill, 17 | })} 18 | > 19 | <slot></slot> 20 | </div> 21 | `; 22 | } 23 | 24 | static get styles(): CSSResultGroup { 25 | return css` 26 | :host { 27 | display: flex; 28 | flex-direction: row; 29 | width: 100%; 30 | } 31 | .container { 32 | width: 100%; 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: flex-end; 36 | } 37 | .container ::slotted(*:not(:last-child)) { 38 | margin-right: var(--spacing); 39 | } 40 | :host([rtl]) .container ::slotted(*:not(:last-child)) { 41 | margin-right: initial; 42 | margin-left: var(--spacing); 43 | } 44 | .container > ::slotted(mushroom-button) { 45 | width: 0; 46 | flex-grow: 0; 47 | flex-shrink: 1; 48 | flex-basis: calc(var(--control-height) * var(--control-button-ratio)); 49 | } 50 | .container > ::slotted(mushroom-input-number) { 51 | width: 0; 52 | flex-grow: 0; 53 | flex-shrink: 1; 54 | flex-basis: calc( 55 | var(--control-height) * var(--control-button-ratio) * 3 56 | ); 57 | } 58 | .container.fill > ::slotted(mushroom-button), 59 | .container.fill > ::slotted(mushroom-input-number) { 60 | flex-grow: 1; 61 | } 62 | `; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/cards/climate-card/utils.ts: -------------------------------------------------------------------------------- 1 | import { HvacAction, HvacMode } from "../../ha"; 2 | 3 | export const CLIMATE_HVAC_MODE_COLORS: Record<HvacMode, string> = { 4 | auto: "var(--rgb-state-climate-auto)", 5 | cool: "var(--rgb-state-climate-cool)", 6 | dry: "var(--rgb-state-climate-dry)", 7 | fan_only: "var(--rgb-state-climate-fan-only)", 8 | heat: "var(--rgb-state-climate-heat)", 9 | heat_cool: "var(--rgb-state-climate-heat-cool)", 10 | off: "var(--rgb-state-climate-off)", 11 | }; 12 | 13 | export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = { 14 | cooling: "var(--rgb-state-climate-cool)", 15 | drying: "var(--rgb-state-climate-dry)", 16 | heating: "var(--rgb-state-climate-heat)", 17 | idle: "var(--rgb-state-climate-idle)", 18 | off: "var(--rgb-state-climate-off)", 19 | }; 20 | 21 | export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = { 22 | auto: "mdi:thermostat-auto", 23 | cool: "mdi:snowflake", 24 | dry: "mdi:water-percent", 25 | fan_only: "mdi:fan", 26 | heat: "mdi:fire", 27 | heat_cool: "mdi:sun-snowflake-variant", 28 | off: "mdi:power", 29 | }; 30 | 31 | export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = { 32 | cooling: "mdi:snowflake", 33 | drying: "mdi:water-percent", 34 | heating: "mdi:fire", 35 | idle: "mdi:clock-outline", 36 | off: "mdi:power", 37 | }; 38 | 39 | export function getHvacModeColor(hvacMode: HvacMode): string { 40 | return CLIMATE_HVAC_MODE_COLORS[hvacMode] ?? CLIMATE_HVAC_MODE_COLORS.off; 41 | } 42 | 43 | export function getHvacActionColor(hvacAction: HvacAction): string { 44 | return ( 45 | CLIMATE_HVAC_ACTION_COLORS[hvacAction] ?? CLIMATE_HVAC_ACTION_COLORS.off 46 | ); 47 | } 48 | 49 | export function getHvacModeIcon(hvacMode: HvacMode): string { 50 | return CLIMATE_HVAC_MODE_ICONS[hvacMode] ?? "mdi:thermostat"; 51 | } 52 | 53 | export function getHvacActionIcon(hvacAction: HvacAction): string | undefined { 54 | return CLIMATE_HVAC_ACTION_ICONS[hvacAction] ?? ""; 55 | } 56 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | <!--- Describe your changes in detail --> 4 | 5 | ## Related Issue 6 | 7 | <!--- This project only accepts pull requests related to open issues --> 8 | <!--- If suggesting a new feature or change, please discuss it in an issue first --> 9 | <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> 10 | <!--- Please link to the issue here --> 11 | 12 | This PR fixes or closes issue: fixes # 13 | 14 | ## Motivation and Context 15 | 16 | <!--- Why is this change required? What problem does it solve? --> 17 | 18 | ## How Has This Been Tested 19 | 20 | <!--- Please describe in detail how you tested your changes. --> 21 | <!--- Include details of your testing environment, and the tests you ran to --> 22 | <!--- see how your change affects other areas of the code, etc. --> 23 | 24 | ## Types of changes 25 | 26 | <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> 27 | 28 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 29 | - [ ] 🚀 New feature (non-breaking change which adds functionality) 30 | - [ ] 🌎 Translation (addition or update a translation) 31 | - [ ] ⚙️ Tech (code style improvement, performance improvement or dependencies bump) 32 | - [ ] 📚 Documentation (fix or addition in the documentation) 33 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change) 34 | 35 | ## Checklist 36 | 37 | <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> 38 | <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> 39 | 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have tested the change locally. 44 | - [ ] I followed [the steps](https://github.com/piitaya/lovelace-mushroom#maintainer-steps-to-add-a-new-language) if I add a new language . 45 | -------------------------------------------------------------------------------- /src/cards/humidifier-card/controls/humidifier-humidity-control.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { 4 | HomeAssistant, 5 | HumidifierEntity, 6 | isActive, 7 | isAvailable, 8 | } from "../../../ha"; 9 | import "../../../shared/slider"; 10 | 11 | @customElement("mushroom-humidifier-humidity-control") 12 | export class HumidifierHumidityControl extends LitElement { 13 | @property({ attribute: false }) public hass!: HomeAssistant; 14 | 15 | @property({ attribute: false }) public entity!: HumidifierEntity; 16 | 17 | @property({ attribute: false }) public color!: string | undefined; 18 | 19 | onChange(e: CustomEvent<{ value: number }>): void { 20 | const value = e.detail.value; 21 | this.hass.callService("humidifier", "set_humidity", { 22 | entity_id: this.entity.entity_id, 23 | humidity: value, 24 | }); 25 | } 26 | 27 | onCurrentChange(e: CustomEvent<{ value?: number }>): void { 28 | const value = e.detail.value; 29 | this.dispatchEvent( 30 | new CustomEvent("current-change", { 31 | detail: { 32 | value, 33 | }, 34 | }) 35 | ); 36 | } 37 | 38 | protected render(): TemplateResult { 39 | const max = this.entity.attributes.max_humidity || 100; 40 | const min = this.entity.attributes.min_humidity || 0; 41 | 42 | return html`<mushroom-slider 43 | .value=${this.entity.attributes.humidity} 44 | .disabled=${!isAvailable(this.entity)} 45 | .inactive=${!isActive(this.entity)} 46 | .showActive=${true} 47 | .min=${min} 48 | .max=${max} 49 | @change=${this.onChange} 50 | @current-change=${this.onCurrentChange} 51 | />`; 52 | } 53 | 54 | static get styles(): CSSResultGroup { 55 | return css` 56 | mushroom-slider { 57 | --main-color: rgb(var(--rgb-state-humidifier)); 58 | --bg-color: rgba(var(--rgb-state-humidifier), 0.2); 59 | } 60 | `; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cards/media-player-card/media-player-card-config.ts: -------------------------------------------------------------------------------- 1 | import { array, assign, boolean, enums, object, optional } from "superstruct"; 2 | import { LovelaceCardConfig } from "../../ha"; 3 | import { 4 | ActionsSharedConfig, 5 | actionsSharedConfigStruct, 6 | } from "../../shared/config/actions-config"; 7 | import { 8 | AppearanceSharedConfig, 9 | appearanceSharedConfigStruct, 10 | } from "../../shared/config/appearance-config"; 11 | import { 12 | EntitySharedConfig, 13 | entitySharedConfigStruct, 14 | } from "../../shared/config/entity-config"; 15 | import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; 16 | 17 | export const MEDIA_LAYER_MEDIA_CONTROLS = [ 18 | "on_off", 19 | "shuffle", 20 | "previous", 21 | "play_pause_stop", 22 | "next", 23 | "repeat", 24 | ] as const; 25 | 26 | export type MediaPlayerMediaControl = 27 | (typeof MEDIA_LAYER_MEDIA_CONTROLS)[number]; 28 | 29 | export const MEDIA_PLAYER_VOLUME_CONTROLS = [ 30 | "volume_mute", 31 | "volume_set", 32 | "volume_buttons", 33 | ] as const; 34 | 35 | export type MediaPlayerVolumeControl = 36 | (typeof MEDIA_PLAYER_VOLUME_CONTROLS)[number]; 37 | 38 | export type MediaPlayerCardConfig = LovelaceCardConfig & 39 | EntitySharedConfig & 40 | AppearanceSharedConfig & 41 | ActionsSharedConfig & { 42 | use_media_info?: boolean; 43 | show_volume_level?: boolean; 44 | volume_controls?: MediaPlayerVolumeControl[]; 45 | media_controls?: MediaPlayerMediaControl[]; 46 | collapsible_controls?: boolean; 47 | }; 48 | 49 | export const mediaPlayerCardConfigStruct = assign( 50 | lovelaceCardConfigStruct, 51 | assign( 52 | entitySharedConfigStruct, 53 | appearanceSharedConfigStruct, 54 | actionsSharedConfigStruct 55 | ), 56 | object({ 57 | use_media_info: optional(boolean()), 58 | show_volume_level: optional(boolean()), 59 | volume_controls: optional(array(enums(MEDIA_PLAYER_VOLUME_CONTROLS))), 60 | media_controls: optional(array(enums(MEDIA_LAYER_MEDIA_CONTROLS))), 61 | collapsible_controls: optional(boolean()), 62 | }) 63 | ); 64 | -------------------------------------------------------------------------------- /src/utils/info.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { html } from "lit"; 3 | import { getEntityPicture, HomeAssistant, isAvailable, isUnknown } from "../ha"; 4 | 5 | const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; 6 | 7 | export const INFOS = [ 8 | "name", 9 | "state", 10 | "last-changed", 11 | "last-updated", 12 | "none", 13 | ] as const; 14 | export type Info = (typeof INFOS)[number]; 15 | 16 | export const ICON_TYPES = ["icon", "entity-picture", "none"] as const; 17 | export type IconType = (typeof ICON_TYPES)[number]; 18 | 19 | export function computeInfoDisplay( 20 | info: Info, 21 | name: string, 22 | state: string, 23 | stateObj: HassEntity, 24 | hass: HomeAssistant 25 | ) { 26 | switch (info) { 27 | case "name": 28 | return name; 29 | case "state": 30 | const domain = stateObj.entity_id.split(".")[0]; 31 | if ( 32 | (stateObj.attributes.device_class === "timestamp" || 33 | TIMESTAMP_STATE_DOMAINS.includes(domain)) && 34 | isAvailable(stateObj) && 35 | !isUnknown(stateObj) 36 | ) { 37 | return html` 38 | <ha-relative-time 39 | .hass=${hass} 40 | .datetime=${stateObj.state} 41 | capitalize 42 | ></ha-relative-time> 43 | `; 44 | } else { 45 | return state; 46 | } 47 | case "last-changed": 48 | return html` 49 | <ha-relative-time 50 | .hass=${hass} 51 | .datetime=${stateObj.last_changed} 52 | capitalize 53 | ></ha-relative-time> 54 | `; 55 | case "last-updated": 56 | return html` 57 | <ha-relative-time 58 | .hass=${hass} 59 | .datetime=${stateObj.last_updated} 60 | capitalize 61 | ></ha-relative-time> 62 | `; 63 | case "none": 64 | return undefined; 65 | } 66 | } 67 | 68 | export function computeEntityPicture(stateObj: HassEntity, iconType: IconType) { 69 | return iconType === "entity-picture" ? getEntityPicture(stateObj) : undefined; 70 | } 71 | -------------------------------------------------------------------------------- /src/cards/light-card/controls/light-brightness-control.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { HomeAssistant, isActive, isAvailable, LightEntity } from "../../../ha"; 4 | import "../../../shared/slider"; 5 | import { getBrightness } from "../utils"; 6 | 7 | @customElement("mushroom-light-brightness-control") 8 | export class LightBrighnessControl extends LitElement { 9 | @property({ attribute: false }) public hass!: HomeAssistant; 10 | 11 | @property({ attribute: false }) public entity!: LightEntity; 12 | 13 | onChange(e: CustomEvent<{ value: number }>): void { 14 | const value = e.detail.value; 15 | this.hass.callService("light", "turn_on", { 16 | entity_id: this.entity.entity_id, 17 | brightness_pct: value, 18 | }); 19 | } 20 | 21 | onCurrentChange(e: CustomEvent<{ value?: number }>): void { 22 | const value = e.detail.value; 23 | this.dispatchEvent( 24 | new CustomEvent("current-change", { 25 | detail: { 26 | value, 27 | }, 28 | }) 29 | ); 30 | } 31 | 32 | protected render(): TemplateResult { 33 | const brightness = getBrightness(this.entity); 34 | 35 | return html` 36 | <mushroom-slider 37 | .value=${brightness} 38 | .disabled=${!isAvailable(this.entity)} 39 | .inactive=${!isActive(this.entity)} 40 | .showActive=${true} 41 | min=${1} 42 | @change=${this.onChange} 43 | @current-change=${this.onCurrentChange} 44 | /> 45 | `; 46 | } 47 | 48 | static get styles(): CSSResultGroup { 49 | return css` 50 | :host { 51 | --slider-color: rgb(var(--rgb-state-light)); 52 | --slider-outline-color: transparent; 53 | --slider-bg-color: rgba(var(--rgb-state-light), 0.2); 54 | } 55 | mushroom-slider { 56 | --main-color: var(--slider-color); 57 | --bg-color: var(--slider-bg-color); 58 | --main-outline-color: var(--slider-outline-color); 59 | } 60 | `; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/conditional-chip.ts: -------------------------------------------------------------------------------- 1 | import { loadCustomElement } from "../../../utils/loader"; 2 | import { 3 | computeChipComponentName, 4 | computeChipEditorComponentName, 5 | createChipElement, 6 | } from "../../../utils/lovelace/chip/chip-element"; 7 | import { 8 | ConditionalChipConfig, 9 | LovelaceChip, 10 | } from "../../../utils/lovelace/chip/types"; 11 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 12 | 13 | const componentName = computeChipComponentName("conditional"); 14 | 15 | export const setupConditionChipComponent = async () => { 16 | // Don't resetup the component if already set up. 17 | if (customElements.get(componentName)) { 18 | return; 19 | } 20 | 21 | // Load conditional base 22 | if (!customElements.get("hui-conditional-base")) { 23 | const helpers = await (window as any).loadCardHelpers(); 24 | helpers.createCardElement({ 25 | type: "conditional", 26 | card: { type: "button" }, 27 | conditions: [], 28 | }); 29 | } 30 | const HuiConditionalBase = await loadCustomElement("hui-conditional-base"); 31 | 32 | // @ts-ignore 33 | class ConditionalChip extends HuiConditionalBase implements LovelaceChip { 34 | public static async getConfigElement(): Promise<LovelaceChipEditor> { 35 | await import("./conditional-chip-editor"); 36 | return document.createElement( 37 | computeChipEditorComponentName("conditional") 38 | ) as LovelaceChipEditor; 39 | } 40 | 41 | public static async getStubConfig(): Promise<ConditionalChipConfig> { 42 | return { 43 | type: `conditional`, 44 | conditions: [], 45 | }; 46 | } 47 | 48 | public setConfig(config: ConditionalChipConfig): void { 49 | this.validateConfig(config); 50 | 51 | if (!config.chip) { 52 | throw new Error("No chip configured"); 53 | } 54 | 55 | this._element = createChipElement(config.chip) as LovelaceChip; 56 | } 57 | } 58 | 59 | if (!customElements.get(componentName)) { 60 | // @ts-ignore 61 | customElements.define(componentName, ConditionalChip); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/shared/button.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; 2 | import { property, customElement } from "lit/decorators.js"; 3 | 4 | @customElement("mushroom-button") 5 | export class Button extends LitElement { 6 | @property() public title: string = ""; 7 | @property({ type: Boolean }) public disabled: boolean = false; 8 | 9 | protected render(): TemplateResult { 10 | return html` 11 | <button 12 | type="button" 13 | class="button" 14 | .title=${this.title} 15 | .disabled=${this.disabled} 16 | > 17 | <slot> </slot> 18 | </button> 19 | `; 20 | } 21 | 22 | static get styles(): CSSResultGroup { 23 | return css` 24 | :host { 25 | --icon-color: var(--primary-text-color); 26 | --icon-color-disabled: rgb(var(--rgb-disabled)); 27 | --bg-color: rgba(var(--rgb-primary-text-color), 0.05); 28 | --bg-color-disabled: rgba(var(--rgb-disabled), 0.2); 29 | height: var(--control-height); 30 | width: calc(var(--control-height) * var(--control-button-ratio)); 31 | flex: none; 32 | } 33 | .button { 34 | cursor: pointer; 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | width: 100%; 39 | height: 100%; 40 | border-radius: var(--control-border-radius); 41 | border: none; 42 | background-color: var(--bg-color); 43 | transition: background-color 280ms ease-in-out; 44 | font-size: var(--control-height); 45 | margin: 0; 46 | padding: 0; 47 | box-sizing: border-box; 48 | line-height: 0; 49 | } 50 | .button:disabled { 51 | cursor: not-allowed; 52 | background-color: var(--bg-color-disabled); 53 | } 54 | .button ::slotted(*) { 55 | --mdc-icon-size: var(--control-icon-size); 56 | color: var(--icon-color); 57 | pointer-events: none; 58 | } 59 | .button:disabled ::slotted(*) { 60 | color: var(--icon-color-disabled); 61 | } 62 | `; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/quickbar-chip-editor.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, nothing } from "lit"; 2 | import { customElement, property, state } from "lit/decorators.js"; 3 | import { fireEvent, HomeAssistant } from "../../../ha"; 4 | import { HaFormSchema } from "../../../utils/form/ha-form"; 5 | import { computeChipEditorComponentName } from "../../../utils/lovelace/chip/chip-element"; 6 | import { EntityChipConfig, QuickBarMode } from "../../../utils/lovelace/chip/types"; 7 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 8 | import { DEFAULT_QUICKBAR_ICON, DEFAULT_QUICKBAR_MODE } from "./quickbar-chip"; 9 | 10 | const SCHEMA: HaFormSchema[] = [ 11 | { name: "icon", selector: { icon: { placeholder: DEFAULT_QUICKBAR_ICON } } }, 12 | { 13 | name: "mode", 14 | selector: { 15 | select: { 16 | options: [{ value: QuickBarMode.Entity, label: "Entity" }, { value: QuickBarMode.Device, label: "Device" }, { value: QuickBarMode.Command, label: "Command" }] 17 | } 18 | } 19 | }, 20 | ]; 21 | 22 | @customElement(computeChipEditorComponentName("quickbar")) 23 | export class QuickBarChipEditor extends LitElement implements LovelaceChipEditor { 24 | @property({ attribute: false }) public hass?: HomeAssistant; 25 | 26 | @state() private _config?: EntityChipConfig; 27 | 28 | public setConfig(config: EntityChipConfig): void { 29 | this._config = config; 30 | } 31 | 32 | private _computeLabel = (schema: HaFormSchema) => { 33 | return this.hass!.localize( 34 | `ui.panel.lovelace.editor.card.generic.${schema.name}` 35 | ); 36 | }; 37 | 38 | protected render() { 39 | if (!this.hass || !this._config) { 40 | return nothing; 41 | } 42 | 43 | return html` 44 | <ha-form 45 | .hass=${this.hass} 46 | .data=${this._config} 47 | .schema=${SCHEMA} 48 | .computeLabel=${this._computeLabel} 49 | @value-changed=${this._valueChanged} 50 | ></ha-form> 51 | `; 52 | } 53 | 54 | private _valueChanged(ev: CustomEvent): void { 55 | fireEvent(this, "config-changed", { config: ev.detail.value }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/back-chip.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; 2 | import { customElement, property, state } from "lit/decorators.js"; 3 | import { actionHandler, computeRTL, HomeAssistant } from "../../../ha"; 4 | import { 5 | computeChipComponentName, 6 | computeChipEditorComponentName, 7 | } from "../../../utils/lovelace/chip/chip-element"; 8 | import { 9 | BackChipConfig, 10 | LovelaceChip, 11 | } from "../../../utils/lovelace/chip/types"; 12 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 13 | 14 | export const DEFAULT_BACK_ICON = "mdi:arrow-left"; 15 | 16 | @customElement(computeChipComponentName("back")) 17 | export class BackChip extends LitElement implements LovelaceChip { 18 | public static async getConfigElement(): Promise<LovelaceChipEditor> { 19 | await import("./back-chip-editor"); 20 | return document.createElement( 21 | computeChipEditorComponentName("back") 22 | ) as LovelaceChipEditor; 23 | } 24 | 25 | public static async getStubConfig( 26 | _hass: HomeAssistant 27 | ): Promise<BackChipConfig> { 28 | return { 29 | type: `back`, 30 | }; 31 | } 32 | 33 | @property({ attribute: false }) public hass?: HomeAssistant; 34 | 35 | @state() private _config?: BackChipConfig; 36 | 37 | public setConfig(config: BackChipConfig): void { 38 | this._config = config; 39 | } 40 | 41 | private _handleAction() { 42 | window.history.back(); 43 | } 44 | 45 | protected render() { 46 | if (!this.hass || !this._config) { 47 | return nothing; 48 | } 49 | 50 | const icon = this._config.icon || DEFAULT_BACK_ICON; 51 | 52 | const rtl = computeRTL(this.hass); 53 | 54 | return html` 55 | <mushroom-chip 56 | ?rtl=${rtl} 57 | @action=${this._handleAction} 58 | .actionHandler=${actionHandler()} 59 | > 60 | <ha-state-icon .hass=${this.hass} .icon=${icon}></ha-state-icon> 61 | </mushroom-chip> 62 | `; 63 | } 64 | 65 | static get styles(): CSSResultGroup { 66 | return css` 67 | mushroom-chip { 68 | cursor: pointer; 69 | } 70 | `; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/cards/select-card/controls/select-option-control.ts: -------------------------------------------------------------------------------- 1 | import { HassEntity } from "home-assistant-js-websocket"; 2 | import { css, CSSResultGroup, html, LitElement } from "lit"; 3 | import { customElement, property } from "lit/decorators.js"; 4 | import { HomeAssistant } from "../../../ha"; 5 | import "../../../shared/form/mushroom-select"; 6 | import { getCurrentOption, getOptions } from "../utils"; 7 | 8 | @customElement("mushroom-select-option-control") 9 | export class SelectOptionControl extends LitElement { 10 | @property() public hass!: HomeAssistant; 11 | 12 | @property({ attribute: false }) public entity!: HassEntity; 13 | 14 | _selectChanged(ev) { 15 | const value = ev.target.value; 16 | 17 | const currentValue = getCurrentOption(this.entity); 18 | 19 | if (value && value !== currentValue) { 20 | this._setValue(value); 21 | } 22 | } 23 | 24 | _setValue(option) { 25 | const entityId = this.entity.entity_id; 26 | const domain = entityId.split(".")[0]; 27 | 28 | this.hass.callService(domain, "select_option", { 29 | entity_id: this.entity.entity_id, 30 | option: option, 31 | }); 32 | } 33 | 34 | render() { 35 | const value = getCurrentOption(this.entity); 36 | 37 | const options = getOptions(this.entity); 38 | 39 | return html` 40 | <mushroom-select 41 | @selected=${this._selectChanged} 42 | @closed=${(e) => e.stopPropagation()} 43 | .value=${value ?? ""} 44 | naturalMenuWidth 45 | fixedMenuPosition 46 | > 47 | ${options.map((option) => { 48 | return html` 49 | <mwc-list-item .value=${option}> 50 | ${this.hass.formatEntityState(this.entity, option)} 51 | </mwc-list-item> 52 | `; 53 | })} 54 | </mushroom-select> 55 | `; 56 | } 57 | 58 | static get styles(): CSSResultGroup { 59 | return css` 60 | :host { 61 | display: flex; 62 | height: 100%; 63 | align-items: center; 64 | } 65 | mushroom-select { 66 | --select-height: var(--control-height); 67 | width: 100%; 68 | } 69 | `; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ha/panels/lovelace/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LovelaceBadgeConfig, 3 | LovelaceCardConfig, 4 | LovelaceConfig, 5 | } from "../../data/lovelace"; 6 | import { FrontendLocaleData } from "../../data/translation"; 7 | import { Constructor, HomeAssistant } from "../../types"; 8 | 9 | declare global { 10 | // eslint-disable-next-line 11 | interface HASSDomEvents { 12 | "ll-rebuild": Record<string, unknown>; 13 | "ll-badge-rebuild": Record<string, unknown>; 14 | } 15 | } 16 | 17 | export interface Lovelace { 18 | config: LovelaceConfig; 19 | // If not set, a strategy was used to generate everything 20 | rawConfig: LovelaceConfig | undefined; 21 | editMode: boolean; 22 | urlPath: string | null; 23 | mode: "generated" | "yaml" | "storage"; 24 | locale: FrontendLocaleData; 25 | enableFullEditMode: () => void; 26 | setEditMode: (editMode: boolean) => void; 27 | saveConfig: (newConfig: LovelaceConfig) => Promise<void>; 28 | deleteConfig: () => Promise<void>; 29 | } 30 | 31 | export interface LovelaceBadge extends HTMLElement { 32 | hass?: HomeAssistant; 33 | setConfig(config: LovelaceBadgeConfig): void; 34 | } 35 | 36 | export interface LovelaceCard extends HTMLElement { 37 | hass?: HomeAssistant; 38 | isPanel?: boolean; 39 | editMode?: boolean; 40 | getCardSize(): number | Promise<number>; 41 | setConfig(config: LovelaceCardConfig): void; 42 | } 43 | 44 | export interface LovelaceCardConstructor extends Constructor<LovelaceCard> { 45 | getStubConfig?: ( 46 | hass: HomeAssistant, 47 | entities: string[], 48 | entitiesFallback: string[] 49 | ) => LovelaceCardConfig; 50 | getConfigElement?: () => LovelaceCardEditor; 51 | } 52 | 53 | export interface LovelaceCardEditor extends LovelaceGenericElementEditor { 54 | setConfig(config: LovelaceCardConfig): void; 55 | } 56 | 57 | export interface LovelaceBadgeEditor extends LovelaceGenericElementEditor { 58 | setConfig(config: LovelaceBadgeConfig): void; 59 | } 60 | 61 | export interface LovelaceGenericElementEditor extends HTMLElement { 62 | hass?: HomeAssistant; 63 | lovelace?: LovelaceConfig; 64 | setConfig(config: any): void; 65 | focusYamlEditor?: () => void; 66 | } 67 | -------------------------------------------------------------------------------- /src/cards/empty-card/empty-card.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, nothing } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { 4 | LovelaceCard, 5 | LovelaceCardEditor, 6 | type LovelaceGridOptions, 7 | } from "../../ha"; 8 | import { MushroomBaseElement } from "../../utils/base-element"; 9 | import { registerCustomCard } from "../../utils/custom-cards"; 10 | import { EMPTY_CARD_EDITOR_NAME, EMPTY_CARD_NAME } from "./const"; 11 | 12 | registerCustomCard({ 13 | type: EMPTY_CARD_NAME, 14 | name: "Mushroom Empty Card", 15 | description: 16 | "The empty card allows you to add a placeholder between your cards.", 17 | }); 18 | 19 | @customElement(EMPTY_CARD_NAME) 20 | export class EmptyCard extends MushroomBaseElement implements LovelaceCard { 21 | @property({ type: Boolean }) public preview = false; 22 | 23 | public static async getConfigElement(): Promise<LovelaceCardEditor> { 24 | await import("./empty-card-editor"); 25 | return document.createElement(EMPTY_CARD_EDITOR_NAME) as LovelaceCardEditor; 26 | } 27 | 28 | public getCardSize(): number { 29 | return 1; 30 | } 31 | 32 | public getGridOptions(): LovelaceGridOptions { 33 | return { 34 | rows: 1, 35 | columns: 6, 36 | }; 37 | } 38 | 39 | public setConfig(): void { 40 | // No config necessary 41 | } 42 | 43 | protected render() { 44 | if (!this.preview) { 45 | return nothing; 46 | } 47 | 48 | return html` 49 | <ha-card> 50 | <ha-icon icon="mdi:dots-horizontal"></ha-icon> 51 | </ha-card> 52 | `; 53 | } 54 | 55 | static get styles(): CSSResultGroup { 56 | return [ 57 | super.styles, 58 | css` 59 | :host { 60 | display: block; 61 | height: 100%; 62 | } 63 | 64 | ha-card { 65 | background: none; 66 | height: 100%; 67 | min-height: 56px; 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | --mdc-icon-size: 40px; 72 | --icon-primary-color: var(--divider-color, rgba(0, 0, 0, 0.12)); 73 | } 74 | `, 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.hass_dev/views/lock-view.yaml: -------------------------------------------------------------------------------- 1 | title: Lock 2 | icon: mdi:lock 3 | cards: 4 | - type: grid 5 | title: Basic 6 | cards: 7 | - type: custom:mushroom-lock-card 8 | entity: lock.front_door 9 | - type: custom:mushroom-lock-card 10 | entity: lock.front_door 11 | name: Custom name and icon 12 | icon: mdi:robot-outline 13 | columns: 2 14 | square: false 15 | - type: grid 16 | title: Infos 17 | cards: 18 | - type: custom:mushroom-lock-card 19 | entity: lock.front_door 20 | primary_info: state 21 | secondary_info: name 22 | - type: custom:mushroom-lock-card 23 | entity: lock.front_door 24 | primary_info: name 25 | secondary_info: last-changed 26 | - type: custom:mushroom-lock-card 27 | entity: lock.front_door 28 | primary_info: name 29 | secondary_info: last-updated 30 | - type: custom:mushroom-lock-card 31 | entity: lock.front_door 32 | primary_info: name 33 | secondary_info: none 34 | - type: custom:mushroom-lock-card 35 | entity: lock.front_door 36 | icon_type: none 37 | columns: 2 38 | square: false 39 | - type: grid 40 | title: Controls 41 | cards: 42 | - type: custom:mushroom-lock-card 43 | entity: lock.front_door 44 | name: Buttons control 45 | - type: custom:mushroom-lock-card 46 | entity: lock.front_door 47 | name: Position control 48 | columns: 2 49 | square: false 50 | - type: custom:mushroom-lock-card 51 | entity: lock.front_door 52 | name: Multiple controls 53 | - type: vertical-stack 54 | title: Layout 55 | cards: 56 | - type: grid 57 | columns: 2 58 | square: false 59 | cards: 60 | - type: custom:mushroom-lock-card 61 | entity: lock.front_door 62 | - type: grid 63 | columns: 2 64 | square: false 65 | cards: 66 | - type: custom:mushroom-lock-card 67 | entity: lock.front_door 68 | layout: "vertical" 69 | - type: custom:mushroom-lock-card 70 | entity: lock.front_door 71 | layout: "horizontal" -------------------------------------------------------------------------------- /src/shared/state-info.ts: -------------------------------------------------------------------------------- 1 | import { 2 | css, 3 | CSSResultGroup, 4 | html, 5 | LitElement, 6 | nothing, 7 | TemplateResult, 8 | } from "lit"; 9 | import { customElement, property } from "lit/decorators.js"; 10 | 11 | @customElement("mushroom-state-info") 12 | export class StateItem extends LitElement { 13 | @property({ attribute: false }) public primary?: string | TemplateResult<1>; 14 | 15 | @property({ attribute: false }) public secondary?: string | TemplateResult<1>; 16 | 17 | @property({ type: Boolean }) public multiline_secondary?: boolean = false; 18 | 19 | protected render(): TemplateResult { 20 | return html` 21 | <div class="container"> 22 | <span class="primary">${this.primary ?? ""}</span> 23 | ${this.secondary 24 | ? html`<span 25 | class="secondary${this.multiline_secondary 26 | ? ` multiline_secondary` 27 | : ``}" 28 | >${this.secondary}</span 29 | >` 30 | : nothing} 31 | </div> 32 | `; 33 | } 34 | 35 | static get styles(): CSSResultGroup { 36 | return css` 37 | .container { 38 | min-width: 0; 39 | flex: 1; 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | .primary { 44 | font-weight: var(--card-primary-font-weight); 45 | font-size: var(--card-primary-font-size); 46 | line-height: var(--card-primary-line-height); 47 | color: var(--card-primary-color); 48 | letter-spacing: var(--card-primary-letter-spacing); 49 | text-overflow: ellipsis; 50 | overflow: hidden; 51 | white-space: nowrap; 52 | } 53 | .secondary { 54 | font-weight: var(--card-secondary-font-weight); 55 | font-size: var(--card-secondary-font-size); 56 | line-height: var(--card-secondary-line-height); 57 | color: var(--card-secondary-color); 58 | letter-spacing: var(--card-secondary-letter-spacing); 59 | text-overflow: ellipsis; 60 | overflow: hidden; 61 | white-space: nowrap; 62 | } 63 | .multiline_secondary { 64 | white-space: pre-wrap; 65 | } 66 | `; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.hass_dev/views/select-view.yaml: -------------------------------------------------------------------------------- 1 | title: Select 2 | icon: mdi:form-dropdown 3 | cards: 4 | - type: grid 5 | title: Basic 6 | cards: 7 | - type: custom:mushroom-select-card 8 | entity: input_select.who_cooks 9 | - type: custom:mushroom-select-card 10 | entity: input_select.who_cooks 11 | name: Custom name and icon 12 | icon: mdi:robot-outline 13 | columns: 2 14 | square: false 15 | - type: grid 16 | title: Infos 17 | cards: 18 | - type: custom:mushroom-select-card 19 | entity: input_select.who_cooks 20 | primary_info: state 21 | secondary_info: name 22 | - type: custom:mushroom-select-card 23 | entity: input_select.who_cooks 24 | primary_info: name 25 | secondary_info: last-changed 26 | - type: custom:mushroom-select-card 27 | entity: input_select.who_cooks 28 | primary_info: name 29 | secondary_info: last-updated 30 | - type: custom:mushroom-select-card 31 | entity: input_select.who_cooks 32 | primary_info: name 33 | secondary_info: none 34 | - type: custom:mushroom-select-card 35 | entity: input_select.who_cooks 36 | icon_type: none 37 | columns: 2 38 | square: false 39 | - type: grid 40 | title: Icon colors 41 | cards: 42 | - type: custom:mushroom-select-card 43 | entity: input_select.who_cooks 44 | icon_color: red 45 | - type: custom:mushroom-select-card 46 | entity: input_select.who_cooks 47 | icon_color: green 48 | columns: 2 49 | square: false 50 | - type: vertical-stack 51 | title: Layout 52 | cards: 53 | - type: grid 54 | columns: 2 55 | square: false 56 | cards: 57 | - type: custom:mushroom-select-card 58 | entity: input_select.who_cooks 59 | - type: grid 60 | columns: 2 61 | square: false 62 | cards: 63 | - type: custom:mushroom-select-card 64 | entity: input_select.who_cooks 65 | layout: "vertical" 66 | - type: custom:mushroom-select-card 67 | entity: input_select.who_cooks 68 | layout: "horizontal" -------------------------------------------------------------------------------- /.hass_dev/views/number-view.yaml: -------------------------------------------------------------------------------- 1 | title: Number 2 | icon: mdi:numeric 3 | cards: 4 | - type: grid 5 | title: Basic 6 | cards: 7 | - type: custom:mushroom-number-card 8 | entity: input_number.percentage 9 | - type: custom:mushroom-number-card 10 | entity: input_number.percentage 11 | name: Custom name and icon 12 | icon: mdi:robot-outline 13 | columns: 2 14 | square: false 15 | - type: grid 16 | title: Infos 17 | cards: 18 | - type: custom:mushroom-number-card 19 | entity: input_number.percentage 20 | primary_info: state 21 | secondary_info: name 22 | - type: custom:mushroom-number-card 23 | entity: input_number.percentage 24 | primary_info: name 25 | secondary_info: last-changed 26 | - type: custom:mushroom-number-card 27 | entity: input_number.percentage 28 | primary_info: name 29 | secondary_info: last-updated 30 | - type: custom:mushroom-number-card 31 | entity: input_number.percentage 32 | primary_info: name 33 | secondary_info: none 34 | - type: custom:mushroom-number-card 35 | entity: input_number.percentage 36 | icon_type: none 37 | columns: 2 38 | square: false 39 | - type: grid 40 | title: Icon colors 41 | cards: 42 | - type: custom:mushroom-number-card 43 | entity: input_number.percentage 44 | icon_color: red 45 | - type: custom:mushroom-number-card 46 | entity: input_number.percentage 47 | icon_color: green 48 | columns: 2 49 | square: false 50 | - type: vertical-stack 51 | title: Layout 52 | cards: 53 | - type: grid 54 | columns: 2 55 | square: false 56 | cards: 57 | - type: custom:mushroom-number-card 58 | entity: input_number.percentage 59 | - type: grid 60 | columns: 2 61 | square: false 62 | cards: 63 | - type: custom:mushroom-number-card 64 | entity: input_number.percentage 65 | layout: "vertical" 66 | - type: custom:mushroom-number-card 67 | entity: input_number.percentage 68 | layout: "horizontal" -------------------------------------------------------------------------------- /src/shared/editor/icon-type-picker.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { HomeAssistant } from "../../ha"; 4 | import setupCustomlocalize from "../../localize"; 5 | import { ICON_TYPES } from "../../utils/info"; 6 | import "../form/mushroom-select"; 7 | 8 | @customElement("mushroom-icon-type-picker") 9 | export class IconTypePicker extends LitElement { 10 | @property() public label = ""; 11 | 12 | @property() public value?: string; 13 | 14 | @property() public configValue = ""; 15 | 16 | @property() public hass!: HomeAssistant; 17 | 18 | _selectChanged(ev) { 19 | const value = ev.target.value; 20 | if (value) { 21 | this.dispatchEvent( 22 | new CustomEvent("value-changed", { 23 | detail: { 24 | value: value !== "default" ? value : "", 25 | }, 26 | }) 27 | ); 28 | } 29 | } 30 | 31 | render() { 32 | const customLocalize = setupCustomlocalize(this.hass); 33 | 34 | return html` 35 | <mushroom-select 36 | .label=${this.label} 37 | .configValue=${this.configValue} 38 | @selected=${this._selectChanged} 39 | @closed=${(e) => e.stopPropagation()} 40 | .value=${this.value || "default"} 41 | fixedMenuPosition 42 | naturalMenuWidth 43 | > 44 | <mwc-list-item value="default"> 45 | ${customLocalize("editor.form.icon_type_picker.values.default")} 46 | </mwc-list-item> 47 | ${ICON_TYPES.map((iconType) => { 48 | return html` 49 | <mwc-list-item .value=${iconType}> 50 | ${customLocalize( 51 | `editor.form.icon_type_picker.values.${iconType}` 52 | ) || capitalizeFirstLetter(iconType)} 53 | </mwc-list-item> 54 | `; 55 | })} 56 | </mushroom-select> 57 | `; 58 | } 59 | 60 | static get styles(): CSSResultGroup { 61 | return css` 62 | mushroom-select { 63 | width: 100%; 64 | } 65 | `; 66 | } 67 | } 68 | 69 | function capitalizeFirstLetter(string: string) { 70 | return string.charAt(0).toUpperCase() + string.slice(1); 71 | } 72 | -------------------------------------------------------------------------------- /src/shared/editor/info-picker.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { HomeAssistant } from "../../ha"; 4 | import setupCustomlocalize from "../../localize"; 5 | import { Info, INFOS } from "../../utils/info"; 6 | import "./../form/mushroom-select"; 7 | 8 | @customElement("mushroom-info-picker") 9 | export class InfoPicker extends LitElement { 10 | @property() public label = ""; 11 | 12 | @property() public value?: string; 13 | 14 | @property() public configValue = ""; 15 | 16 | @property() public infos?: Info[]; 17 | 18 | @property() public hass!: HomeAssistant; 19 | 20 | _selectChanged(ev) { 21 | const value = ev.target.value; 22 | if (value) { 23 | this.dispatchEvent( 24 | new CustomEvent("value-changed", { 25 | detail: { 26 | value: value !== "default" ? value : "", 27 | }, 28 | }) 29 | ); 30 | } 31 | } 32 | 33 | render() { 34 | const customLocalize = setupCustomlocalize(this.hass); 35 | 36 | return html` 37 | <mushroom-select 38 | .label=${this.label} 39 | .configValue=${this.configValue} 40 | @selected=${this._selectChanged} 41 | @closed=${(e) => e.stopPropagation()} 42 | .value=${this.value || "default"} 43 | fixedMenuPosition 44 | naturalMenuWidth 45 | > 46 | <mwc-list-item value="default"> 47 | ${customLocalize("editor.form.info_picker.values.default")} 48 | </mwc-list-item> 49 | ${(this.infos ?? INFOS).map((info) => { 50 | return html` 51 | <mwc-list-item .value=${info}> 52 | ${customLocalize(`editor.form.info_picker.values.${info}`) || 53 | capitalizeFirstLetter(info)} 54 | </mwc-list-item> 55 | `; 56 | })} 57 | </mushroom-select> 58 | `; 59 | } 60 | 61 | static get styles(): CSSResultGroup { 62 | return css` 63 | mushroom-select { 64 | width: 100%; 65 | } 66 | `; 67 | } 68 | } 69 | 70 | function capitalizeFirstLetter(string: string) { 71 | return string.charAt(0).toUpperCase() + string.slice(1); 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | getBabelInputPlugin, 3 | getBabelOutputPlugin, 4 | } from "@rollup/plugin-babel"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | import json from "@rollup/plugin-json"; 7 | import nodeResolve from "@rollup/plugin-node-resolve"; 8 | import terser from "@rollup/plugin-terser"; 9 | import typescript from "@rollup/plugin-typescript"; 10 | import serve from "rollup-plugin-serve"; 11 | import ignore from "./rollup-plugins/rollup-ignore-plugin.js"; 12 | 13 | const IGNORED_FILES = [ 14 | "@material/mwc-notched-outline/mwc-notched-outline.js", 15 | "@material/mwc-ripple/mwc-ripple.js", 16 | "@material/mwc-list/mwc-list.js", 17 | "@material/mwc-list/mwc-list-item.js", 18 | "@material/mwc-menu/mwc-menu.js", 19 | "@material/mwc-menu/mwc-menu-surface.js", 20 | "@material/mwc-icon/mwc-icon.js", 21 | ]; 22 | 23 | const dev = process.env.ROLLUP_WATCH; 24 | 25 | const serveOptions = { 26 | contentBase: ["./dist"], 27 | host: "0.0.0.0", 28 | port: 4000, 29 | allowCrossOrigin: true, 30 | headers: { 31 | "Access-Control-Allow-Origin": "*", 32 | }, 33 | }; 34 | 35 | const plugins = [ 36 | ignore({ 37 | files: IGNORED_FILES.map((file) => require.resolve(file)), 38 | }), 39 | typescript({ 40 | declaration: false, 41 | }), 42 | nodeResolve(), 43 | json(), 44 | commonjs(), 45 | getBabelInputPlugin({ 46 | babelHelpers: "bundled", 47 | }), 48 | getBabelOutputPlugin({ 49 | presets: [ 50 | [ 51 | "@babel/preset-env", 52 | { 53 | modules: false, 54 | }, 55 | ], 56 | ], 57 | compact: true, 58 | }), 59 | ...(dev ? [serve(serveOptions)] : [terser()]), 60 | ]; 61 | 62 | export default [ 63 | { 64 | input: "src/mushroom.ts", 65 | output: { 66 | dir: "dist", 67 | format: "es", 68 | inlineDynamicImports: true, 69 | }, 70 | plugins, 71 | moduleContext: (id) => { 72 | const thisAsWindowForModules = [ 73 | "node_modules/@formatjs/intl-utils/lib/src/diff.js", 74 | "node_modules/@formatjs/intl-utils/lib/src/resolve-locale.js", 75 | ]; 76 | if (thisAsWindowForModules.some((id_) => id.trimRight().endsWith(id_))) { 77 | return "window"; 78 | } 79 | }, 80 | }, 81 | ]; 82 | -------------------------------------------------------------------------------- /src/cards/chips-card/chips/menu-chip.ts: -------------------------------------------------------------------------------- 1 | import { 2 | css, 3 | CSSResultGroup, 4 | html, 5 | LitElement, 6 | nothing, 7 | TemplateResult, 8 | } from "lit"; 9 | import { customElement, property, state } from "lit/decorators.js"; 10 | import { 11 | actionHandler, 12 | computeRTL, 13 | fireEvent, 14 | HomeAssistant, 15 | } from "../../../ha"; 16 | import { 17 | computeChipComponentName, 18 | computeChipEditorComponentName, 19 | } from "../../../utils/lovelace/chip/chip-element"; 20 | import { 21 | LovelaceChip, 22 | MenuChipConfig, 23 | } from "../../../utils/lovelace/chip/types"; 24 | import { LovelaceChipEditor } from "../../../utils/lovelace/types"; 25 | 26 | export const DEFAULT_MENU_ICON = "mdi:menu"; 27 | 28 | @customElement(computeChipComponentName("menu")) 29 | export class MenuChip extends LitElement implements LovelaceChip { 30 | public static async getConfigElement(): Promise<LovelaceChipEditor> { 31 | await import("./menu-chip-editor"); 32 | return document.createElement( 33 | computeChipEditorComponentName("menu") 34 | ) as LovelaceChipEditor; 35 | } 36 | 37 | public static async getStubConfig( 38 | _hass: HomeAssistant 39 | ): Promise<MenuChipConfig> { 40 | return { 41 | type: `menu`, 42 | }; 43 | } 44 | 45 | @property({ attribute: false }) public hass?: HomeAssistant; 46 | 47 | @state() private _config?: MenuChipConfig; 48 | 49 | public setConfig(config: MenuChipConfig): void { 50 | this._config = config; 51 | } 52 | 53 | private _handleAction() { 54 | fireEvent(this, "hass-toggle-menu" as any); 55 | } 56 | 57 | protected render() { 58 | if (!this.hass || !this._config) { 59 | return nothing; 60 | } 61 | 62 | const icon = this._config.icon || DEFAULT_MENU_ICON; 63 | 64 | const rtl = computeRTL(this.hass); 65 | 66 | return html` 67 | <mushroom-chip 68 | ?rtl=${rtl} 69 | @action=${this._handleAction} 70 | .actionHandler=${actionHandler()} 71 | > 72 | <ha-state-icon .hass=${this.hass} .icon=${icon}></ha-state-icon> 73 | </mushroom-chip> 74 | `; 75 | } 76 | 77 | static get styles(): CSSResultGroup { 78 | return css` 79 | mushroom-chip { 80 | cursor: pointer; 81 | } 82 | `; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/shared/editor/layout-picker.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { HomeAssistant } from "../../ha"; 4 | import setupCustomlocalize from "../../localize"; 5 | import "./../form/mushroom-select"; 6 | 7 | const LAYOUTS = ["default", "horizontal", "vertical"] as const; 8 | type Layout = (typeof LAYOUTS)[number]; 9 | 10 | const ICONS: Record<Layout, string> = { 11 | default: "mdi:card-text-outline", 12 | vertical: "mdi:focus-field-vertical", 13 | horizontal: "mdi:focus-field-horizontal", 14 | }; 15 | 16 | @customElement("mushroom-layout-picker") 17 | export class LayoutPicker extends LitElement { 18 | @property() public label = ""; 19 | 20 | @property() public value?: string; 21 | 22 | @property() public configValue = ""; 23 | 24 | @property() public hass!: HomeAssistant; 25 | 26 | _selectChanged(ev) { 27 | const value = ev.target.value; 28 | if (value) { 29 | this.dispatchEvent( 30 | new CustomEvent("value-changed", { 31 | detail: { 32 | value: value !== "default" ? value : "", 33 | }, 34 | }) 35 | ); 36 | } 37 | } 38 | 39 | render() { 40 | const customLocalize = setupCustomlocalize(this.hass); 41 | 42 | const value = this.value || "default"; 43 | 44 | return html` 45 | <mushroom-select 46 | icon 47 | .label=${this.label} 48 | .configValue=${this.configValue} 49 | @selected=${this._selectChanged} 50 | @closed=${(e) => e.stopPropagation()} 51 | .value=${value} 52 | fixedMenuPosition 53 | naturalMenuWidth 54 | > 55 | <ha-icon slot="icon" .icon=${ICONS[value as Layout]}></ha-icon> 56 | ${LAYOUTS.map( 57 | (layout) => html` 58 | <mwc-list-item .value=${layout} graphic="icon"> 59 | ${customLocalize(`editor.form.layout_picker.values.${layout}`)} 60 | <ha-icon slot="graphic" .icon=${ICONS[layout]}></ha-icon> 61 | </mwc-list-item> 62 | ` 63 | )} 64 | </mushroom-select> 65 | `; 66 | } 67 | 68 | static get styles(): CSSResultGroup { 69 | return css` 70 | mushroom-select { 71 | width: 100%; 72 | } 73 | `; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ha/data/cover.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HassEntityAttributeBase, 3 | HassEntityBase, 4 | } from "home-assistant-js-websocket"; 5 | import { supportsFeature } from "../common/entity/supports-feature"; 6 | 7 | export const COVER_SUPPORT_OPEN = 1; 8 | export const COVER_SUPPORT_CLOSE = 2; 9 | export const COVER_SUPPORT_SET_POSITION = 4; 10 | export const COVER_SUPPORT_STOP = 8; 11 | export const COVER_SUPPORT_OPEN_TILT = 16; 12 | export const COVER_SUPPORT_CLOSE_TILT = 32; 13 | export const COVER_SUPPORT_STOP_TILT = 64; 14 | export const COVER_SUPPORT_SET_TILT_POSITION = 128; 15 | 16 | export function isFullyOpen(stateObj: CoverEntity) { 17 | if (stateObj.attributes.current_position !== undefined) { 18 | return stateObj.attributes.current_position === 100; 19 | } 20 | return stateObj.state === "open"; 21 | } 22 | 23 | export function isFullyClosed(stateObj: CoverEntity) { 24 | if (stateObj.attributes.current_position !== undefined) { 25 | return stateObj.attributes.current_position === 0; 26 | } 27 | return stateObj.state === "closed"; 28 | } 29 | 30 | export function isFullyOpenTilt(stateObj: CoverEntity) { 31 | return stateObj.attributes.current_tilt_position === 100; 32 | } 33 | 34 | export function isFullyClosedTilt(stateObj: CoverEntity) { 35 | return stateObj.attributes.current_tilt_position === 0; 36 | } 37 | 38 | export function isOpening(stateObj: CoverEntity) { 39 | return stateObj.state === "opening"; 40 | } 41 | 42 | export function isClosing(stateObj: CoverEntity) { 43 | return stateObj.state === "closing"; 44 | } 45 | 46 | export function isTiltOnly(stateObj: CoverEntity) { 47 | const supportsCover = 48 | supportsFeature(stateObj, COVER_SUPPORT_OPEN) || 49 | supportsFeature(stateObj, COVER_SUPPORT_CLOSE) || 50 | supportsFeature(stateObj, COVER_SUPPORT_STOP); 51 | const supportsTilt = 52 | supportsFeature(stateObj, COVER_SUPPORT_OPEN_TILT) || 53 | supportsFeature(stateObj, COVER_SUPPORT_CLOSE_TILT) || 54 | supportsFeature(stateObj, COVER_SUPPORT_STOP_TILT); 55 | return supportsTilt && !supportsCover; 56 | } 57 | 58 | interface CoverEntityAttributes extends HassEntityAttributeBase { 59 | current_position: number; 60 | current_tilt_position: number; 61 | } 62 | 63 | export interface CoverEntity extends HassEntityBase { 64 | attributes: CoverEntityAttributes; 65 | } 66 | -------------------------------------------------------------------------------- /src/shared/state-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | css, 3 | CSSResultGroup, 4 | html, 5 | LitElement, 6 | nothing, 7 | TemplateResult, 8 | } from "lit"; 9 | import { customElement, property } from "lit/decorators.js"; 10 | import { classMap } from "lit/directives/class-map.js"; 11 | import { Appearance } from "./config/appearance-config"; 12 | import "./shape-icon"; 13 | 14 | @customElement("mushroom-state-item") 15 | export class StateItem extends LitElement { 16 | @property() public appearance?: Appearance; 17 | 18 | protected render(): TemplateResult { 19 | return html` 20 | <div 21 | class=${classMap({ 22 | container: true, 23 | vertical: this.appearance?.layout === "vertical", 24 | })} 25 | > 26 | ${this.appearance?.icon_type !== "none" 27 | ? html` 28 | <div class="icon"> 29 | <slot name="icon"></slot> 30 | <slot name="badge"></slot> 31 | </div> 32 | ` 33 | : nothing} 34 | ${this.appearance?.primary_info !== "none" || 35 | this.appearance?.secondary_info !== "none" 36 | ? html` 37 | <div class="info"> 38 | <slot name="info"></slot> 39 | </div> 40 | ` 41 | : nothing} 42 | </div> 43 | `; 44 | } 45 | 46 | static get styles(): CSSResultGroup { 47 | return css` 48 | :host { 49 | display: block; 50 | height: 100%; 51 | } 52 | .container { 53 | height: 100%; 54 | display: flex; 55 | flex-direction: row; 56 | align-items: center; 57 | justify-content: center; 58 | box-sizing: border-box; 59 | padding: var(--spacing); 60 | gap: var(--spacing); 61 | } 62 | .icon { 63 | position: relative; 64 | } 65 | .icon ::slotted(*[slot="badge"]) { 66 | position: absolute; 67 | top: -3px; 68 | right: -3px; 69 | } 70 | :host([rtl]) .icon ::slotted(*[slot="badge"]) { 71 | right: initial; 72 | left: -3px; 73 | } 74 | .info { 75 | min-width: 0; 76 | width: 100%; 77 | display: flex; 78 | flex-direction: column; 79 | } 80 | .container.vertical { 81 | flex-direction: column; 82 | } 83 | .container.vertical .info { 84 | text-align: center; 85 | } 86 | `; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/shared/editor/alignment-picker.ts: -------------------------------------------------------------------------------- 1 | import { css, CSSResultGroup, html, LitElement } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { HomeAssistant } from "../../ha"; 4 | import setupCustomlocalize from "../../localize"; 5 | import "./../form/mushroom-select"; 6 | 7 | const ALIGNMENT = ["default", "start", "center", "end", "justify"] as const; 8 | type Alignment = (typeof ALIGNMENT)[number]; 9 | 10 | const ICONS: Record<Alignment, string> = { 11 | default: "mdi:format-align-left", 12 | start: "mdi:format-align-left", 13 | center: "mdi:format-align-center", 14 | end: "mdi:format-align-right", 15 | justify: "mdi:format-align-justify", 16 | }; 17 | 18 | @customElement("mushroom-alignment-picker") 19 | export class AlignmentPicker extends LitElement { 20 | @property() public label = ""; 21 | 22 | @property() public value?: string; 23 | 24 | @property() public configValue = ""; 25 | 26 | @property() public hass!: HomeAssistant; 27 | 28 | _selectChanged(ev) { 29 | const value = ev.target.value; 30 | if (value) { 31 | this.dispatchEvent( 32 | new CustomEvent("value-changed", { 33 | detail: { 34 | value: value !== "default" ? value : "", 35 | }, 36 | }) 37 | ); 38 | } 39 | } 40 | 41 | render() { 42 | const customLocalize = setupCustomlocalize(this.hass); 43 | 44 | const value = this.value || "default"; 45 | 46 | return html` 47 | <mushroom-select 48 | icon 49 | .label=${this.label} 50 | .configValue=${this.configValue} 51 | @selected=${this._selectChanged} 52 | @closed=${(e) => e.stopPropagation()} 53 | .value=${this.value || "default"} 54 | fixedMenuPosition 55 | naturalMenuWidth 56 | > 57 | <ha-icon slot="icon" .icon=${ICONS[value as Alignment]}></ha-icon> 58 | ${ALIGNMENT.map((alignment) => { 59 | return html` 60 | <mwc-list-item .value=${alignment} graphic="icon"> 61 | ${customLocalize( 62 | `editor.form.alignment_picker.values.${alignment}` 63 | )} 64 | <ha-icon slot="graphic" .icon=${ICONS[alignment]}></ha-icon> 65 | </mwc-list-item> 66 | `; 67 | })} 68 | </mushroom-select> 69 | `; 70 | } 71 | 72 | static get styles(): CSSResultGroup { 73 | return css` 74 | mushroom-select { 75 | width: 100%; 76 | } 77 | `; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ha/resources/ha-sortable-styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from "lit"; 2 | 3 | export const sortableStyles = css` 4 | #sortable a:nth-of-type(2n) paper-icon-item { 5 | animation-name: keyframes1; 6 | animation-iteration-count: infinite; 7 | transform-origin: 50% 10%; 8 | animation-delay: -0.75s; 9 | animation-duration: 0.25s; 10 | } 11 | 12 | #sortable a:nth-of-type(2n-1) paper-icon-item { 13 | animation-name: keyframes2; 14 | animation-iteration-count: infinite; 15 | animation-direction: alternate; 16 | transform-origin: 30% 5%; 17 | animation-delay: -0.5s; 18 | animation-duration: 0.33s; 19 | } 20 | 21 | #sortable a { 22 | height: 48px; 23 | display: flex; 24 | } 25 | 26 | #sortable { 27 | outline: none; 28 | display: block !important; 29 | } 30 | 31 | .hidden-panel { 32 | display: flex !important; 33 | } 34 | 35 | .sortable-fallback { 36 | display: none; 37 | } 38 | 39 | .sortable-ghost { 40 | opacity: 0.4; 41 | } 42 | 43 | .sortable-fallback { 44 | opacity: 0; 45 | } 46 | 47 | @keyframes keyframes1 { 48 | 0% { 49 | transform: rotate(-1deg); 50 | animation-timing-function: ease-in; 51 | } 52 | 53 | 50% { 54 | transform: rotate(1.5deg); 55 | animation-timing-function: ease-out; 56 | } 57 | } 58 | 59 | @keyframes keyframes2 { 60 | 0% { 61 | transform: rotate(1deg); 62 | animation-timing-function: ease-in; 63 | } 64 | 65 | 50% { 66 | transform: rotate(-1.5deg); 67 | animation-timing-function: ease-out; 68 | } 69 | } 70 | 71 | .show-panel, 72 | .hide-panel { 73 | display: none; 74 | position: absolute; 75 | top: 0; 76 | right: 4px; 77 | --mdc-icon-button-size: 40px; 78 | } 79 | 80 | :host([rtl]) .show-panel { 81 | right: initial; 82 | left: 4px; 83 | } 84 | 85 | .hide-panel { 86 | top: 4px; 87 | right: 8px; 88 | } 89 | 90 | :host([rtl]) .hide-panel { 91 | right: initial; 92 | left: 8px; 93 | } 94 | 95 | :host([expanded]) .hide-panel { 96 | display: block; 97 | } 98 | 99 | :host([expanded]) .show-panel { 100 | display: inline-flex; 101 | } 102 | 103 | paper-icon-item.hidden-panel, 104 | paper-icon-item.hidden-panel span, 105 | paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { 106 | color: var(--secondary-text-color); 107 | cursor: pointer; 108 | } 109 | `; 110 | -------------------------------------------------------------------------------- /.hass_dev/views/humidifier-view.yaml: -------------------------------------------------------------------------------- 1 | title: Humidifier 2 | icon: mdi:air-humidifier 3 | cards: 4 | - type: grid 5 | title: Basic 6 | cards: 7 | - type: custom:mushroom-humidifier-card 8 | entity: humidifier.humidifier 9 | - type: custom:mushroom-humidifier-card 10 | entity: humidifier.humidifier 11 | name: Custom name and icon 12 | icon: mdi:robot-outline 13 | columns: 2 14 | square: false 15 | - type: grid 16 | title: Infos 17 | cards: 18 | - type: custom:mushroom-humidifier-card 19 | entity: humidifier.humidifier 20 | primary_info: state 21 | secondary_info: name 22 | - type: custom:mushroom-humidifier-card 23 | entity: humidifier.humidifier 24 | primary_info: name 25 | secondary_info: last-changed 26 | - type: custom:mushroom-humidifier-card 27 | entity: humidifier.humidifier 28 | primary_info: name 29 | secondary_info: last-updated 30 | - type: custom:mushroom-humidifier-card 31 | entity: humidifier.humidifier 32 | primary_info: name 33 | secondary_info: none 34 | - type: custom:mushroom-humidifier-card 35 | entity: humidifier.humidifier 36 | icon_type: none 37 | columns: 2 38 | square: false 39 | - type: grid 40 | title: Controls 41 | cards: 42 | - type: custom:mushroom-humidifier-card 43 | entity: humidifier.humidifier 44 | name: Humidity control 45 | show_target_humidity_control: true 46 | - type: custom:mushroom-humidifier-card 47 | entity: humidifier.humidifier 48 | name: Collapsible controls 49 | collapsible_controls: true 50 | show_target_humidity_control: true 51 | columns: 2 52 | square: false 53 | - type: vertical-stack 54 | title: Layout 55 | cards: 56 | - type: grid 57 | columns: 2 58 | square: false 59 | cards: 60 | - type: custom:mushroom-humidifier-card 61 | entity: humidifier.humidifier 62 | show_target_humidity_control: true 63 | - type: grid 64 | columns: 2 65 | square: false 66 | cards: 67 | - type: custom:mushroom-humidifier-card 68 | entity: humidifier.humidifier 69 | layout: "vertical" 70 | show_target_humidity_control: true 71 | - type: custom:mushroom-humidifier-card 72 | entity: humidifier.humidifier 73 | layout: "horizontal" 74 | show_target_humidity_control: true -------------------------------------------------------------------------------- /src/cards/climate-card/controls/climate-hvac-modes-control.ts: -------------------------------------------------------------------------------- 1 | import { html, LitElement, TemplateResult } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import { styleMap } from "lit/directives/style-map.js"; 4 | import { 5 | ClimateEntity, 6 | compareClimateHvacModes, 7 | computeRTL, 8 | HomeAssistant, 9 | HvacMode, 10 | isAvailable, 11 | } from "../../../ha"; 12 | import "../../../shared/button"; 13 | import "../../../shared/button-group"; 14 | import { getHvacModeColor, getHvacModeIcon } from "../utils"; 15 | 16 | export const isHvacModesVisible = (entity: ClimateEntity, modes?: HvacMode[]) => 17 | (entity.attributes.hvac_modes || []).some((mode) => 18 | (modes ?? []).includes(mode) 19 | ); 20 | 21 | @customElement("mushroom-climate-hvac-modes-control") 22 | export class ClimateHvacModesControl extends LitElement { 23 | @property({ attribute: false }) public hass!: HomeAssistant; 24 | 25 | @property({ attribute: false }) public entity!: ClimateEntity; 26 | 27 | @property({ attribute: false }) public modes!: HvacMode[]; 28 | 29 | @property() public fill: boolean = false; 30 | 31 | private callService(e: CustomEvent) { 32 | e.stopPropagation(); 33 | const mode = (e.target! as any).mode as HvacMode; 34 | this.hass.callService("climate", "set_hvac_mode", { 35 | entity_id: this.entity!.entity_id, 36 | hvac_mode: mode, 37 | }); 38 | } 39 | 40 | protected render(): TemplateResult { 41 | const rtl = computeRTL(this.hass); 42 | 43 | const modes = this.entity.attributes.hvac_modes 44 | .filter((mode) => (this.modes ?? []).includes(mode)) 45 | .sort(compareClimateHvacModes); 46 | 47 | return html` 48 | <mushroom-button-group .fill=${this.fill} ?rtl=${rtl}> 49 | ${modes.map((mode) => this.renderModeButton(mode))} 50 | </mushroom-button-group> 51 | `; 52 | } 53 | 54 | private renderModeButton(mode: HvacMode) { 55 | const iconStyle = {}; 56 | const color = mode === "off" ? "var(--rgb-grey)" : getHvacModeColor(mode); 57 | if (mode === this.entity.state) { 58 | iconStyle["--icon-color"] = `rgb(${color})`; 59 | iconStyle["--bg-color"] = `rgba(${color}, 0.2)`; 60 | } 61 | 62 | return html` 63 | <mushroom-button 64 | style=${styleMap(iconStyle)} 65 | .mode=${mode} 66 | .disabled=${!isAvailable(this.entity)} 67 | @click=${this.callService} 68 | > 69 | <ha-icon .icon=${getHvacModeIcon(mode)}></ha-icon> 70 | </mushroom-button> 71 | `; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import { rgb } from "culori"; 2 | import { css } from "lit"; 3 | 4 | export const COLORS = [ 5 | "primary", 6 | "accent", 7 | "red", 8 | "pink", 9 | "purple", 10 | "deep-purple", 11 | "indigo", 12 | "blue", 13 | "light-blue", 14 | "cyan", 15 | "teal", 16 | "green", 17 | "light-green", 18 | "lime", 19 | "yellow", 20 | "amber", 21 | "orange", 22 | "deep-orange", 23 | "brown", 24 | "light-grey", 25 | "grey", 26 | "dark-grey", 27 | "blue-grey", 28 | "black", 29 | "white", 30 | "disabled", 31 | ]; 32 | 33 | export function computeRgbColor(color: string): string { 34 | if (color === "primary" || color === "accent") { 35 | return `var(--rgb-${color}-color)`; 36 | } 37 | if (COLORS.includes(color)) { 38 | return `var(--rgb-${color})`; 39 | } else if (color.startsWith("#")) { 40 | try { 41 | const rgbColor = rgb(color); 42 | if (rgbColor) { 43 | const { r, g, b } = rgbColor; 44 | return `${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}`; 45 | } 46 | return ""; 47 | } catch (err) { 48 | return ""; 49 | } 50 | } 51 | return color; 52 | } 53 | 54 | export function computeColorName(color: string): string { 55 | return color 56 | .split("-") 57 | .map((s) => capitalizeFirstLetter(s)) 58 | .join(" "); 59 | } 60 | 61 | function capitalizeFirstLetter(string: string): string { 62 | return string.charAt(0).toUpperCase() + string.slice(1); 63 | } 64 | 65 | export const defaultColorCss = css` 66 | --default-red: 244, 67, 54; 67 | --default-pink: 233, 30, 99; 68 | --default-purple: 146, 107, 199; 69 | --default-deep-purple: 110, 65, 171; 70 | --default-indigo: 63, 81, 181; 71 | --default-blue: 33, 150, 243; 72 | --default-light-blue: 3, 169, 244; 73 | --default-cyan: 0, 188, 212; 74 | --default-teal: 0, 150, 136; 75 | --default-green: 76, 175, 80; 76 | --default-light-green: 139, 195, 74; 77 | --default-lime: 205, 220, 57; 78 | --default-yellow: 255, 235, 59; 79 | --default-amber: 255, 193, 7; 80 | --default-orange: 255, 152, 0; 81 | --default-deep-orange: 255, 111, 34; 82 | --default-brown: 121, 85, 72; 83 | --default-light-grey: 189, 189, 189; 84 | --default-grey: 158, 158, 158; 85 | --default-dark-grey: 96, 96, 96; 86 | --default-blue-grey: 96, 125, 139; 87 | --default-black: 0, 0, 0; 88 | --default-white: 255, 255, 255; 89 | --default-disabled: 189, 189, 189; 90 | `; 91 | 92 | export const defaultDarkColorCss = css` 93 | --default-disabled: 111, 111, 111; 94 | `; 95 | -------------------------------------------------------------------------------- /src/cards/lock-card/lock-card-editor.ts: -------------------------------------------------------------------------------- 1 | import { html, nothing } from "lit"; 2 | import { customElement, state } from "lit/decorators.js"; 3 | import { assert } from "superstruct"; 4 | import { LovelaceCardEditor, fireEvent } from "../../ha"; 5 | import setupCustomlocalize from "../../localize"; 6 | import { computeActionsFormSchema } from "../../shared/config/actions-config"; 7 | import { APPEARANCE_FORM_SCHEMA } from "../../shared/config/appearance-config"; 8 | import { MushroomBaseElement } from "../../utils/base-element"; 9 | import { GENERIC_LABELS } from "../../utils/form/generic-fields"; 10 | import { HaFormSchema } from "../../utils/form/ha-form"; 11 | import { loadHaComponents } from "../../utils/loader"; 12 | import { LOCK_CARD_EDITOR_NAME, LOCK_ENTITY_DOMAINS } from "./const"; 13 | import { LockCardConfig, lockCardConfigStruct } from "./lock-card-config"; 14 | 15 | const SCHEMA: HaFormSchema[] = [ 16 | { name: "entity", selector: { entity: { domain: LOCK_ENTITY_DOMAINS } } }, 17 | { name: "name", selector: { text: {} } }, 18 | { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, 19 | ...APPEARANCE_FORM_SCHEMA, 20 | ...computeActionsFormSchema(), 21 | ]; 22 | 23 | @customElement(LOCK_CARD_EDITOR_NAME) 24 | export class LockCardEditor 25 | extends MushroomBaseElement 26 | implements LovelaceCardEditor 27 | { 28 | @state() private _config?: LockCardConfig; 29 | 30 | connectedCallback() { 31 | super.connectedCallback(); 32 | void loadHaComponents(); 33 | } 34 | 35 | public setConfig(config: LockCardConfig): void { 36 | assert(config, lockCardConfigStruct); 37 | this._config = config; 38 | } 39 | 40 | private _computeLabel = (schema: HaFormSchema) => { 41 | const customLocalize = setupCustomlocalize(this.hass!); 42 | 43 | if (GENERIC_LABELS.includes(schema.name)) { 44 | return customLocalize(`editor.card.generic.${schema.name}`); 45 | } 46 | return this.hass!.localize( 47 | `ui.panel.lovelace.editor.card.generic.${schema.name}` 48 | ); 49 | }; 50 | 51 | protected render() { 52 | if (!this.hass || !this._config) { 53 | return nothing; 54 | } 55 | 56 | return html` 57 | <ha-form 58 | .hass=${this.hass} 59 | .data=${this._config} 60 | .schema=${SCHEMA} 61 | .computeLabel=${this._computeLabel} 62 | @value-changed=${this._valueChanged} 63 | ></ha-form> 64 | `; 65 | } 66 | 67 | private _valueChanged(ev: CustomEvent): void { 68 | fireEvent(this, "config-changed", { config: ev.detail.value }); 69 | } 70 | } 71 | --------------------------------------------------------------------------------