diff --git a/.gitignore b/.gitignore index 8b568f884c..7a0283af66 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ src/cast/dev_const.ts # Secrets .lokalise_token yarn-error.log + +#asdf +.tool-versions diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 1d331a82d7..459b7e4aa3 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -50,15 +50,8 @@ stages: - template: templates/azp-job-wheels.yaml@azure parameters: builderVersion: '$(versionWheels)' - builderApk: 'build-base' - wheelsLocal: true + wheelsRequirement: 'requirement.txt' preBuild: - - task: NodeTool@0 - displayName: "Use Node $(versionNode)" - inputs: - versionSpec: "$(versionNode)" - script: | - set -e - - yarn install - script/build_frontend + sleep 240 + echo "home-assistant-frontend==$(Build.SourceBranchName)" > requirement.txt diff --git a/demo/public/assets/arsaboo/floorplans/ecobee_blank.png b/demo/public/assets/arsaboo/floorplans/ecobee_blank.png index a1c74af660..b2f6cec0bb 100644 Binary files a/demo/public/assets/arsaboo/floorplans/ecobee_blank.png and b/demo/public/assets/arsaboo/floorplans/ecobee_blank.png differ diff --git a/demo/public/assets/arsaboo/icons/Harmony.png b/demo/public/assets/arsaboo/icons/Harmony.png index 276aa1079d..f26c4b8e38 100644 Binary files a/demo/public/assets/arsaboo/icons/Harmony.png and b/demo/public/assets/arsaboo/icons/Harmony.png differ diff --git a/demo/public/assets/arsaboo/icons/abode_disabled.png b/demo/public/assets/arsaboo/icons/abode_disabled.png index 328240e1aa..fbe4f3d7f3 100644 Binary files a/demo/public/assets/arsaboo/icons/abode_disabled.png and b/demo/public/assets/arsaboo/icons/abode_disabled.png differ diff --git a/demo/public/assets/arsaboo/icons/abode_enabled.png b/demo/public/assets/arsaboo/icons/abode_enabled.png index 83d45ec724..1104fdb9bd 100644 Binary files a/demo/public/assets/arsaboo/icons/abode_enabled.png and b/demo/public/assets/arsaboo/icons/abode_enabled.png differ diff --git a/demo/public/assets/arsaboo/icons/automation_disabled.png b/demo/public/assets/arsaboo/icons/automation_disabled.png index f2b9f24ac1..fe2df80beb 100644 Binary files a/demo/public/assets/arsaboo/icons/automation_disabled.png and b/demo/public/assets/arsaboo/icons/automation_disabled.png differ diff --git a/demo/public/assets/arsaboo/icons/automation_enabled.png b/demo/public/assets/arsaboo/icons/automation_enabled.png index 3e75bc33bc..fc8d949fb8 100644 Binary files a/demo/public/assets/arsaboo/icons/automation_enabled.png and b/demo/public/assets/arsaboo/icons/automation_enabled.png differ diff --git a/demo/public/assets/arsaboo/icons/camera_patio_streaming.png b/demo/public/assets/arsaboo/icons/camera_patio_streaming.png index 6a17276bde..e5c8f4ecac 100644 Binary files a/demo/public/assets/arsaboo/icons/camera_patio_streaming.png and b/demo/public/assets/arsaboo/icons/camera_patio_streaming.png differ diff --git a/demo/public/assets/arsaboo/icons/ecobee_blank.png b/demo/public/assets/arsaboo/icons/ecobee_blank.png index ed95102496..26c49c1c2a 100644 Binary files a/demo/public/assets/arsaboo/icons/ecobee_blank.png and b/demo/public/assets/arsaboo/icons/ecobee_blank.png differ diff --git a/demo/public/assets/arsaboo/icons/light_off.png b/demo/public/assets/arsaboo/icons/light_off.png index 60747f54ac..164c2c0dc3 100644 Binary files a/demo/public/assets/arsaboo/icons/light_off.png and b/demo/public/assets/arsaboo/icons/light_off.png differ diff --git a/demo/public/assets/arsaboo/icons/light_on.png b/demo/public/assets/arsaboo/icons/light_on.png index fb1b0f27e1..ee267600fc 100644 Binary files a/demo/public/assets/arsaboo/icons/light_on.png and b/demo/public/assets/arsaboo/icons/light_on.png differ diff --git a/demo/public/assets/arsaboo/icons/security_armed_red.png b/demo/public/assets/arsaboo/icons/security_armed_red.png index 31e727ebd5..61cb8fd533 100644 Binary files a/demo/public/assets/arsaboo/icons/security_armed_red.png and b/demo/public/assets/arsaboo/icons/security_armed_red.png differ diff --git a/demo/public/assets/arsaboo/icons/security_disarmed.png b/demo/public/assets/arsaboo/icons/security_disarmed.png index c038767000..359dd08565 100644 Binary files a/demo/public/assets/arsaboo/icons/security_disarmed.png and b/demo/public/assets/arsaboo/icons/security_disarmed.png differ diff --git a/demo/public/assets/arsaboo/icons/tv_disabled.png b/demo/public/assets/arsaboo/icons/tv_disabled.png index 08457116bf..cc5dccd4ea 100644 Binary files a/demo/public/assets/arsaboo/icons/tv_disabled.png and b/demo/public/assets/arsaboo/icons/tv_disabled.png differ diff --git a/demo/public/assets/arsaboo/icons/tv_enabled.png b/demo/public/assets/arsaboo/icons/tv_enabled.png index cfea8c160d..133f2bc229 100644 Binary files a/demo/public/assets/arsaboo/icons/tv_enabled.png and b/demo/public/assets/arsaboo/icons/tv_enabled.png differ diff --git a/demo/public/assets/arsaboo/icons/tv_on2.png b/demo/public/assets/arsaboo/icons/tv_on2.png index ead5dbf545..6323ef30ea 100644 Binary files a/demo/public/assets/arsaboo/icons/tv_on2.png and b/demo/public/assets/arsaboo/icons/tv_on2.png differ diff --git a/demo/public/assets/arsaboo/images/camera.backyard.jpg b/demo/public/assets/arsaboo/images/camera.backyard.jpg index d2c9a88111..c90cd70a5d 100644 Binary files a/demo/public/assets/arsaboo/images/camera.backyard.jpg and b/demo/public/assets/arsaboo/images/camera.backyard.jpg differ diff --git a/demo/public/assets/arsaboo/images/camera.driveway.jpg b/demo/public/assets/arsaboo/images/camera.driveway.jpg index 072a0b152e..e7b59e6b8a 100644 Binary files a/demo/public/assets/arsaboo/images/camera.driveway.jpg and b/demo/public/assets/arsaboo/images/camera.driveway.jpg differ diff --git a/demo/public/assets/arsaboo/images/camera.patio.jpg b/demo/public/assets/arsaboo/images/camera.patio.jpg index fba5a10f5d..c286cc8b94 100644 Binary files a/demo/public/assets/arsaboo/images/camera.patio.jpg and b/demo/public/assets/arsaboo/images/camera.patio.jpg differ diff --git a/demo/public/assets/arsaboo/images/camera.porch.jpg b/demo/public/assets/arsaboo/images/camera.porch.jpg index 8f1c740dda..bfa6461e02 100644 Binary files a/demo/public/assets/arsaboo/images/camera.porch.jpg and b/demo/public/assets/arsaboo/images/camera.porch.jpg differ diff --git a/demo/public/assets/jimpower/background-15.jpg b/demo/public/assets/jimpower/background-15.jpg index 5bed444fdc..2c868cf286 100644 Binary files a/demo/public/assets/jimpower/background-15.jpg and b/demo/public/assets/jimpower/background-15.jpg differ diff --git a/demo/public/assets/jimpower/cardbackK.png b/demo/public/assets/jimpower/cardbackK.png index 26126fd9a4..608e5ef5f6 100644 Binary files a/demo/public/assets/jimpower/cardbackK.png and b/demo/public/assets/jimpower/cardbackK.png differ diff --git a/demo/public/assets/jimpower/home/bus_10.jpg b/demo/public/assets/jimpower/home/bus_10.jpg index 81fb23cf68..a7971bd7a7 100644 Binary files a/demo/public/assets/jimpower/home/bus_10.jpg and b/demo/public/assets/jimpower/home/bus_10.jpg differ diff --git a/demo/public/assets/jimpower/home/james_10.jpg b/demo/public/assets/jimpower/home/james_10.jpg index 2c37836b23..ae14a5124d 100644 Binary files a/demo/public/assets/jimpower/home/james_10.jpg and b/demo/public/assets/jimpower/home/james_10.jpg differ diff --git a/demo/public/assets/jimpower/home/tina_4.jpg b/demo/public/assets/jimpower/home/tina_4.jpg index cecaadeacc..b539da5242 100644 Binary files a/demo/public/assets/jimpower/home/tina_4.jpg and b/demo/public/assets/jimpower/home/tina_4.jpg differ diff --git a/demo/public/assets/jimpower/security/motion_3.jpg b/demo/public/assets/jimpower/security/motion_3.jpg index 5c1c3ab733..2788f7cf8a 100644 Binary files a/demo/public/assets/jimpower/security/motion_3.jpg and b/demo/public/assets/jimpower/security/motion_3.jpg differ diff --git a/demo/public/assets/kernehed/bella.jpg b/demo/public/assets/kernehed/bella.jpg index d169a247e2..b7375a66e0 100644 Binary files a/demo/public/assets/kernehed/bella.jpg and b/demo/public/assets/kernehed/bella.jpg differ diff --git a/demo/public/assets/kernehed/camera.entre.jpg b/demo/public/assets/kernehed/camera.entre.jpg index 23a33f2dce..a3187ff6e3 100644 Binary files a/demo/public/assets/kernehed/camera.entre.jpg and b/demo/public/assets/kernehed/camera.entre.jpg differ diff --git a/demo/public/assets/kernehed/oscar.jpg b/demo/public/assets/kernehed/oscar.jpg index 1fbb68683a..ae014ecba3 100644 Binary files a/demo/public/assets/kernehed/oscar.jpg and b/demo/public/assets/kernehed/oscar.jpg differ diff --git a/demo/public/assets/teachingbirds/House_square.jpg b/demo/public/assets/teachingbirds/House_square.jpg index 7a11c2dd08..9c1024ba1d 100644 Binary files a/demo/public/assets/teachingbirds/House_square.jpg and b/demo/public/assets/teachingbirds/House_square.jpg differ diff --git a/demo/public/assets/teachingbirds/Stefan_square.jpg b/demo/public/assets/teachingbirds/Stefan_square.jpg index 24527b24e6..0fe6e02503 100644 Binary files a/demo/public/assets/teachingbirds/Stefan_square.jpg and b/demo/public/assets/teachingbirds/Stefan_square.jpg differ diff --git a/demo/public/assets/teachingbirds/background_square.png b/demo/public/assets/teachingbirds/background_square.png index 2d59f64922..bfc9303bfb 100644 Binary files a/demo/public/assets/teachingbirds/background_square.png and b/demo/public/assets/teachingbirds/background_square.png differ diff --git a/demo/public/assets/teachingbirds/cleaning_square.jpg b/demo/public/assets/teachingbirds/cleaning_square.jpg index 69e7928767..a2ff470235 100644 Binary files a/demo/public/assets/teachingbirds/cleaning_square.jpg and b/demo/public/assets/teachingbirds/cleaning_square.jpg differ diff --git a/demo/public/assets/teachingbirds/clothes_drying_square.jpg b/demo/public/assets/teachingbirds/clothes_drying_square.jpg index fcfe5c3c43..509c3a9dd8 100644 Binary files a/demo/public/assets/teachingbirds/clothes_drying_square.jpg and b/demo/public/assets/teachingbirds/clothes_drying_square.jpg differ diff --git a/demo/public/assets/teachingbirds/dryer_square.jpg b/demo/public/assets/teachingbirds/dryer_square.jpg index da8505e826..338015602e 100644 Binary files a/demo/public/assets/teachingbirds/dryer_square.jpg and b/demo/public/assets/teachingbirds/dryer_square.jpg differ diff --git a/demo/public/assets/teachingbirds/folded_clothes_square.jpg b/demo/public/assets/teachingbirds/folded_clothes_square.jpg index 3836be2c40..e13219d35d 100644 Binary files a/demo/public/assets/teachingbirds/folded_clothes_square.jpg and b/demo/public/assets/teachingbirds/folded_clothes_square.jpg differ diff --git a/demo/public/assets/teachingbirds/guests_square.jpg b/demo/public/assets/teachingbirds/guests_square.jpg index cb8330dc73..69dbcd1ffa 100644 Binary files a/demo/public/assets/teachingbirds/guests_square.jpg and b/demo/public/assets/teachingbirds/guests_square.jpg differ diff --git a/demo/public/assets/teachingbirds/isa_square.jpg b/demo/public/assets/teachingbirds/isa_square.jpg index 89838d6b63..253953689d 100644 Binary files a/demo/public/assets/teachingbirds/isa_square.jpg and b/demo/public/assets/teachingbirds/isa_square.jpg differ diff --git a/demo/public/assets/teachingbirds/laundry_clean_2_square.jpg b/demo/public/assets/teachingbirds/laundry_clean_2_square.jpg index 4bb70995ce..06ed031901 100644 Binary files a/demo/public/assets/teachingbirds/laundry_clean_2_square.jpg and b/demo/public/assets/teachingbirds/laundry_clean_2_square.jpg differ diff --git a/demo/public/assets/teachingbirds/laundry_running_square.jpg b/demo/public/assets/teachingbirds/laundry_running_square.jpg index 0756330296..98856b6de9 100644 Binary files a/demo/public/assets/teachingbirds/laundry_running_square.jpg and b/demo/public/assets/teachingbirds/laundry_running_square.jpg differ diff --git a/demo/public/assets/teachingbirds/mailbox_bw_square.jpg b/demo/public/assets/teachingbirds/mailbox_bw_square.jpg index fac00d5d11..79abee43d6 100644 Binary files a/demo/public/assets/teachingbirds/mailbox_bw_square.jpg and b/demo/public/assets/teachingbirds/mailbox_bw_square.jpg differ diff --git a/demo/public/assets/teachingbirds/mailbox_square.jpg b/demo/public/assets/teachingbirds/mailbox_square.jpg index 9d9a02afe5..c501c934d1 100644 Binary files a/demo/public/assets/teachingbirds/mailbox_square.jpg and b/demo/public/assets/teachingbirds/mailbox_square.jpg differ diff --git a/demo/public/assets/teachingbirds/meteogram.png b/demo/public/assets/teachingbirds/meteogram.png index 11b7d05fed..ab1614d3f4 100644 Binary files a/demo/public/assets/teachingbirds/meteogram.png and b/demo/public/assets/teachingbirds/meteogram.png differ diff --git a/demo/public/assets/teachingbirds/roomba_bw_square.jpg b/demo/public/assets/teachingbirds/roomba_bw_square.jpg index e87c54586f..789f953bb6 100644 Binary files a/demo/public/assets/teachingbirds/roomba_bw_square.jpg and b/demo/public/assets/teachingbirds/roomba_bw_square.jpg differ diff --git a/demo/public/assets/teachingbirds/roomba_square.jpg b/demo/public/assets/teachingbirds/roomba_square.jpg index b41c715438..21396a89a7 100644 Binary files a/demo/public/assets/teachingbirds/roomba_square.jpg and b/demo/public/assets/teachingbirds/roomba_square.jpg differ diff --git a/demo/public/assets/teachingbirds/trash_bear_bw_square.jpg b/demo/public/assets/teachingbirds/trash_bear_bw_square.jpg index 3d1dc8e810..fe3eed011d 100644 Binary files a/demo/public/assets/teachingbirds/trash_bear_bw_square.jpg and b/demo/public/assets/teachingbirds/trash_bear_bw_square.jpg differ diff --git a/demo/public/assets/teachingbirds/trash_square.jpg b/demo/public/assets/teachingbirds/trash_square.jpg index 285e2626db..71e940a5a9 100644 Binary files a/demo/public/assets/teachingbirds/trash_square.jpg and b/demo/public/assets/teachingbirds/trash_square.jpg differ diff --git a/demo/public/assets/teachingbirds/washer_square.jpg b/demo/public/assets/teachingbirds/washer_square.jpg index 7fd71f09d7..dbf8adb0ee 100644 Binary files a/demo/public/assets/teachingbirds/washer_square.jpg and b/demo/public/assets/teachingbirds/washer_square.jpg differ diff --git a/gallery/public/images/album_cover.jpg b/gallery/public/images/album_cover.jpg index cb0eb97462..1b9d865c92 100644 Binary files a/gallery/public/images/album_cover.jpg and b/gallery/public/images/album_cover.jpg differ diff --git a/gallery/public/images/album_cover_2.jpg b/gallery/public/images/album_cover_2.jpg index 4a2339f733..dd0ef2176f 100644 Binary files a/gallery/public/images/album_cover_2.jpg and b/gallery/public/images/album_cover_2.jpg differ diff --git a/gallery/public/images/netflix.jpg b/gallery/public/images/netflix.jpg index 3549edd995..4915660172 100644 Binary files a/gallery/public/images/netflix.jpg and b/gallery/public/images/netflix.jpg differ diff --git a/gallery/src/components/demo-card.js b/gallery/src/components/demo-card.js index e52cf02890..921030dee3 100644 --- a/gallery/src/components/demo-card.js +++ b/gallery/src/components/demo-card.js @@ -16,7 +16,8 @@ class DemoCard extends PolymerElement { color: var(--primary-color); } #card { - width: 400px; + max-width: 400px; + width: 100vw; } pre { width: 400px; diff --git a/public/static/images/image-broken.svg b/public/static/images/image-broken.svg index cbd2559a26..2124e00f22 100644 --- a/public/static/images/image-broken.svg +++ b/public/static/images/image-broken.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/static/images/weather/cloudy.png b/public/static/images/weather/cloudy.png new file mode 100644 index 0000000000..257b958bfc Binary files /dev/null and b/public/static/images/weather/cloudy.png differ diff --git a/public/static/images/weather/lightning-rainy.png b/public/static/images/weather/lightning-rainy.png new file mode 100644 index 0000000000..e28b7667bb Binary files /dev/null and b/public/static/images/weather/lightning-rainy.png differ diff --git a/public/static/images/weather/lightning.png b/public/static/images/weather/lightning.png new file mode 100644 index 0000000000..c4966bd5c9 Binary files /dev/null and b/public/static/images/weather/lightning.png differ diff --git a/public/static/images/weather/night.png b/public/static/images/weather/night.png new file mode 100644 index 0000000000..d7e79d2ac9 Binary files /dev/null and b/public/static/images/weather/night.png differ diff --git a/public/static/images/weather/partly-cloudy.png b/public/static/images/weather/partly-cloudy.png new file mode 100644 index 0000000000..a39a400ba1 Binary files /dev/null and b/public/static/images/weather/partly-cloudy.png differ diff --git a/public/static/images/weather/pouring.png b/public/static/images/weather/pouring.png new file mode 100644 index 0000000000..06bef9bc4c Binary files /dev/null and b/public/static/images/weather/pouring.png differ diff --git a/public/static/images/weather/rainy.png b/public/static/images/weather/rainy.png new file mode 100644 index 0000000000..e0c9a9a975 Binary files /dev/null and b/public/static/images/weather/rainy.png differ diff --git a/public/static/images/weather/snowy.png b/public/static/images/weather/snowy.png new file mode 100644 index 0000000000..059f8da70e Binary files /dev/null and b/public/static/images/weather/snowy.png differ diff --git a/public/static/images/weather/sunny.png b/public/static/images/weather/sunny.png new file mode 100644 index 0000000000..6c67835dce Binary files /dev/null and b/public/static/images/weather/sunny.png differ diff --git a/public/static/images/weather/windy.png b/public/static/images/weather/windy.png new file mode 100644 index 0000000000..9b4b20f227 Binary files /dev/null and b/public/static/images/weather/windy.png differ diff --git a/setup.py b/setup.py index fa64d28d9b..265afb020f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200330.0", + version="20200401.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index 172c56261b..276ab53580 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -60,6 +60,7 @@ export class StateBadge extends LitElement { const iconStyle: Partial = { color: "", filter: "", + display: "", }; const hostStyle: Partial = { backgroundImage: "", @@ -76,7 +77,7 @@ export class StateBadge extends LitElement { } hostStyle.backgroundImage = `url(${imageUrl})`; iconStyle.display = "none"; - } else { + } else if (stateObj.state === "on") { if (stateObj.attributes.hs_color && this.stateColor !== false) { const hue = stateObj.attributes.hs_color[0]; const sat = stateObj.attributes.hs_color[1]; diff --git a/src/data/history.ts b/src/data/history.ts index 72a25dde92..1055ed0472 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -55,7 +55,8 @@ export const fetchRecent = ( entityId, startTime, endTime, - skipInitialState = false + skipInitialState = false, + significantChangesOnly?: boolean ): Promise => { let url = "history/period"; if (startTime) { @@ -68,6 +69,9 @@ export const fetchRecent = ( if (skipInitialState) { url += "&skip_initial_state"; } + if (significantChangesOnly !== undefined) { + url += `&significant_changes_only=${Number(significantChangesOnly)}`; + } return hass.callApi("GET", url); }; diff --git a/src/data/lovelace_custom_cards.ts b/src/data/lovelace_custom_cards.ts new file mode 100644 index 0000000000..2e021e7a6f --- /dev/null +++ b/src/data/lovelace_custom_cards.ts @@ -0,0 +1,23 @@ +export interface CustomCardEntry { + type: string; + name?: string; + description?: string; + preview?: boolean; +} + +export interface CustomCardsWindow { + customCards?: CustomCardEntry[]; +} + +export const CUSTOM_TYPE_PREFIX = "custom:"; + +const customCardsWindow = window as CustomCardsWindow; + +if (!("customCards" in customCardsWindow)) { + customCardsWindow.customCards = []; +} + +export const customCards = customCardsWindow.customCards!; + +export const getCustomCardEntry = (type: string) => + customCards.find((card) => card.type === type); diff --git a/src/data/weather.ts b/src/data/weather.ts new file mode 100644 index 0000000000..62a4ed423d --- /dev/null +++ b/src/data/weather.ts @@ -0,0 +1,80 @@ +import { HomeAssistant } from "../types"; + +export const weatherImages = { + "clear-night": "/static/images/weather/night.png", + cloudy: "/static/images/weather/cloudy.png", + fog: "/static/images/weather/cloudy.png", + hail: "/static/images/weather/rainy.png", + lightning: "/static/images/weather/lightning.png", + "lightning-rainy": "/static/images/weather/lightning-rainy.png", + partlycloudy: "/static/images/weather/partly-cloudy.png", + pouring: "/static/images/weather/pouring.png", + rainy: "/static/images/weather/rainy.png", + snowy: "/static/images/weather/snowy.png", + "snowy-rainy": "/static/images/weather/rainy.png", + sunny: "/static/images/weather/sunny.png", + windy: "/static/images/weather/windy.png", + "windy-variant": "/static/images/weather/windy.png", +}; + +export const weatherIcons = { + exceptional: "hass:alert-circle-outline", +}; + +export const cardinalDirections = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + "N", +]; + +const getWindBearingText = (degree: string): string => { + const degreenum = parseInt(degree, 10); + if (isFinite(degreenum)) { + // tslint:disable-next-line: no-bitwise + return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16]; + } + return degree; +}; + +export const getWindBearing = (bearing: string): string => { + if (bearing != null) { + return getWindBearingText(bearing); + } + return ""; +}; + +export const getWeatherUnit = ( + hass: HomeAssistant, + measure: string +): string => { + const lengthUnit = hass.config.unit_system.length || ""; + switch (measure) { + case "pressure": + return lengthUnit === "km" ? "hPa" : "inHg"; + case "wind_speed": + return `${lengthUnit}/h`; + case "length": + return lengthUnit; + case "precipitation": + return lengthUnit === "km" ? "mm" : "in"; + case "humidity": + case "precipitation_probability": + return "%"; + default: + return hass.config.unit_system[measure] || ""; + } +}; diff --git a/src/dialogs/notifications/persistent-notification-item.ts b/src/dialogs/notifications/persistent-notification-item.ts index 3ef90bba41..6689db8283 100644 --- a/src/dialogs/notifications/persistent-notification-item.ts +++ b/src/dialogs/notifications/persistent-notification-item.ts @@ -31,7 +31,7 @@ export class HuiPersistentNotificationItem extends LitElement { return html` - ${this.notification.title || this.notification.notification_id} + ${this.notification.title} diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index d1c23d2615..4cd12f4b75 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -116,13 +116,9 @@ class OnboardingCoreConfig extends LitElement { @value-changed=${this._handleChange} > - ${this._unitSystem === "metric" - ? this.hass.localize( - "ui.panel.config.core.section.core.core_config.elevation_meters" - ) - : this.hass.localize( - "ui.panel.config.core.section.core.core_config.elevation_feet" - )} + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.elevation_meters" + )} diff --git a/src/panels/config/core/ha-config-core-form.ts b/src/panels/config/core/ha-config-core-form.ts index 1d3fc487af..5e16bce60d 100644 --- a/src/panels/config/core/ha-config-core-form.ts +++ b/src/panels/config/core/ha-config-core-form.ts @@ -102,13 +102,9 @@ class ConfigCoreForm extends LitElement { @value-changed=${this._handleChange} > - ${this._unitSystem === "metric" - ? this.hass.localize( - "ui.panel.config.core.section.core.core_config.elevation_meters" - ) - : this.hass.localize( - "ui.panel.config.core.section.core.core_config.elevation_feet" - )} + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.elevation_meters" + )} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index bee4982244..290a43790b 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -97,6 +97,15 @@ export class HaConfigDevicePage extends LitElement { ) ); + private _computeArea = memoizeOne((areas, device): + | AreaRegistryEntry + | undefined => { + if (!areas || !device || !device.area_id) { + return undefined; + } + return areas.find((area) => area.area_id === device.area_id); + }); + private _batteryEntity = memoizeOne((entities: EntityRegistryEntry[]): | EntityRegistryEntry | undefined => findBatteryEntity(this.hass, entities)); @@ -132,7 +141,7 @@ export class HaConfigDevicePage extends LitElement { const batteryState = batteryEntity ? this.hass.states[batteryEntity.entity_id] : undefined; - const areaName = this._computeAreaName(this.areas, device); + const area = this._computeArea(this.areas, device); return html`

${computeDeviceName(device, this.hass)}

- ${areaName - ? this.hass.localize( - "ui.panel.config.integrations.config_entry.area", - "area", - areaName - ) + ${area + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.area", + "area", + area.name || "Unnamed Area" + )} + ` : ""} ` @@ -437,13 +450,6 @@ export class HaConfigDevicePage extends LitElement { return state ? computeStateName(state) : null; } - private _computeAreaName(areas, device): string | undefined { - if (!areas || !device || !device.area_id) { - return undefined; - } - return areas.find((area) => area.area_id === device.area_id).name; - } - private _onImageLoad(ev) { ev.target.style.display = "inline-block"; } @@ -648,6 +654,10 @@ export class HaConfigDevicePage extends LitElement { a { text-decoration: none; + color: var(--primary-color); + } + + ha-card a { color: var(--primary-text-color); } `; diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.ts b/src/panels/config/integrations/ha-config-entries-dashboard.ts index 6a6bc4377e..0b6387d3fa 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.ts +++ b/src/panels/config/integrations/ha-config-entries-dashboard.ts @@ -200,6 +200,15 @@ export class HaConfigManagerDashboard extends LitElement { href="/config/integrations/config_entry/${item.entry_id}" > +
${this.hass.localize( @@ -342,6 +351,14 @@ export class HaConfigManagerDashboard extends LitElement { return states; } + private _onImageLoad(ev) { + ev.target.style.visibility = "initial"; + } + + private _onImageError(ev) { + ev.target.style.visibility = "hidden"; + } + static get styles(): CSSResult { return css` mwc-button { @@ -355,6 +372,9 @@ export class HaConfigManagerDashboard extends LitElement { cursor: pointer; margin: 8px; } + .configured { + padding-bottom: 24px; + } .configured a { color: var(--primary-text-color); text-decoration: none; @@ -375,6 +395,10 @@ export class HaConfigManagerDashboard extends LitElement { .overflow { width: 56px; } + img { + width: 50px; + margin-right: 16px; + } `; } } diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts index e30d00eca3..f4f62402af 100644 --- a/src/panels/config/users/dialog-add-user.ts +++ b/src/panels/config/users/dialog-add-user.ts @@ -1,8 +1,7 @@ import "@material/mwc-button"; import "@polymer/paper-spinner/paper-spinner"; - +import "../../../components/ha-switch"; import "../../../components/ha-dialog"; -import "../../../resources/ha-style"; import { LitElement, html, @@ -10,6 +9,8 @@ import { customElement, property, PropertyValues, + CSSResult, + css, } from "lit-element"; import { HomeAssistant } from "../../../types"; import { PolymerChangedEvent } from "../../../polymer-types"; @@ -17,11 +18,12 @@ import { AddUserDialogParams } from "./show-dialog-add-user"; import { User, SYSTEM_GROUP_ID_USER, - GROUPS, createUser, deleteUser, + SYSTEM_GROUP_ID_ADMIN, } from "../../../data/user"; import { createAuthForUser } from "../../../data/auth"; +import { haStyleDialog } from "../../../resources/styles"; @customElement("dialog-add-user") export class DialogAddUser extends LitElement { @@ -33,14 +35,14 @@ export class DialogAddUser extends LitElement { @property() private _name?: string; @property() private _username?: string; @property() private _password?: string; - @property() private _group?: string; + @property() private _isAdmin?: boolean; public showDialog(params: AddUserDialogParams) { this._params = params; this._name = ""; this._username = ""; this._password = ""; - this._group = SYSTEM_GROUP_ID_USER; + this._isAdmin = false; this._error = undefined; this._loading = false; } @@ -106,25 +108,10 @@ export class DialogAddUser extends LitElement { @value-changed=${this._passwordChanged} error-message="Required" > - - - ${GROUPS.map( - (groupId) => html` - - ${this.hass.localize(`groups.${groupId}`)} - - ` - )} - - - ${this._group === SYSTEM_GROUP_ID_USER + + ${this.hass.localize("ui.panel.config.users.editor.admin")} + + ${!this._isAdmin ? html`
The users group is a work in progress. The user will be unable @@ -191,8 +178,8 @@ export class DialogAddUser extends LitElement { this._password = ev.detail.value; } - private async _handleGroupChange(ev): Promise { - this._group = ev.detail.item.getAttribute("group-id"); + private async _adminChanged(ev): Promise { + this._isAdmin = ev.target.checked; } private async _createUser(ev) { @@ -207,7 +194,7 @@ export class DialogAddUser extends LitElement { let user: User; try { const userResponse = await createUser(this.hass, this._name, [ - this._group!, + this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, ]); user = userResponse.user; } catch (err) { @@ -233,6 +220,20 @@ export class DialogAddUser extends LitElement { this._params!.userAddedCallback(user); this._close(); } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 500px; + } + ha-switch { + margin-top: 8px; + } + `, + ]; + } } declare global { diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index b9ef3de155..7cc23b449b 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -10,20 +10,22 @@ import { TemplateResult, css, } from "lit-element"; -import "../../../components/entity/ha-entities-picker"; -import "../../../components/user/ha-user-picker"; import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { UserDetailDialogParams } from "./show-dialog-user-detail"; +import "../../../components/ha-switch"; import { createCloseHeading } from "../../../components/ha-dialog"; -import { GROUPS, SYSTEM_GROUP_ID_USER } from "../../../data/user"; +import { + SYSTEM_GROUP_ID_ADMIN, + SYSTEM_GROUP_ID_USER, +} from "../../../data/user"; @customElement("dialog-user-detail") class DialogUserDetail extends LitElement { @property() public hass!: HomeAssistant; @property() private _name!: string; - @property() private _group?: string; + @property() private _isAdmin?: boolean; @property() private _error?: string; @property() private _params?: UserDetailDialogParams; @property() private _submitting: boolean = false; @@ -32,7 +34,7 @@ class DialogUserDetail extends LitElement { this._params = params; this._error = undefined; this._name = params.entry.name || ""; - this._group = params.entry.group_ids[0]; + this._isAdmin = params.entry.group_ids[0] === SYSTEM_GROUP_ID_ADMIN; await this.updateComplete; } @@ -55,31 +57,55 @@ class DialogUserDetail extends LitElement {
${this._error}
` : ""} +
+ ${this.hass.localize("ui.panel.config.users.editor.id")}: ${user.id} +
+
+ ${user.is_owner + ? html` + ${this.hass.localize( + "ui.panel.config.users.editor.owner" + )} + ` + : ""} + ${user.system_generated + ? html` + + ${this.hass.localize( + "ui.panel.config.users.editor.system_generated" + )} + + ` + : ""} + ${user.is_active + ? html` + ${this.hass.localize( + "ui.panel.config.users.editor.active" + )} + ` + : ""} +
- - - ${GROUPS.map( - (groupId) => html` - - ${this.hass.localize(`groups.${groupId}`)} - - ` - )} - - - ${this._group === SYSTEM_GROUP_ID_USER + ${this.hass.localize("ui.panel.config.users.editor.admin")} + + ${!this._isAdmin ? html`
The users group is a work in progress. The user will be unable @@ -88,34 +114,6 @@ class DialogUserDetail extends LitElement { limit access to administrators. ` : ""} - - - - - - - - - - - - - - - - - -
- ${this.hass.localize("ui.panel.config.users.editor.id")} - ${user.id}
- ${this.hass.localize("ui.panel.config.users.editor.owner")} - ${user.is_owner}
- ${this.hass.localize("ui.panel.config.users.editor.active")} - ${user.is_active}
- ${this.hass.localize( - "ui.panel.config.users.editor.system_generated" - )} - ${user.system_generated}
@@ -129,21 +127,33 @@ class DialogUserDetail extends LitElement { ${user.system_generated ? html` - ${this.hass.localize( + + ${this.hass.localize( "ui.panel.config.users.editor.system_generated_users_not_removable" - )} + )} + + ` + : ""} + +
+ + ${this.hass!.localize("ui.panel.config.users.editor.update_user")} + + ${user.system_generated + ? html` + + ${this.hass.localize( + "ui.panel.config.users.editor.system_generated_users_not_editable" + )} + ` : ""}
- - ${this.hass!.localize("ui.panel.config.users.editor.update_user")} - `; } @@ -153,8 +163,8 @@ class DialogUserDetail extends LitElement { this._name = ev.detail.value; } - private async _handleGroupChange(ev): Promise { - this._group = ev.detail.item.getAttribute("group-id"); + private async _adminChanged(ev): Promise { + this._isAdmin = ev.target.checked; } private async _updateEntry() { @@ -162,7 +172,9 @@ class DialogUserDetail extends LitElement { try { await this._params!.updateEntry({ name: this._name.trim(), - group_ids: [this._group!], + group_ids: [ + this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, + ], }); this._close(); } catch (err) { @@ -194,8 +206,24 @@ class DialogUserDetail extends LitElement { ha-dialog { --mdc-dialog-min-width: 500px; } - table { - width: 100%; + .form { + padding-top: 16px; + } + .secondary { + color: var(--secondary-text-color); + } + .state { + background-color: rgba(var(--rgb-primary-text-color), 0.15); + border-radius: 16px; + padding: 4px 8px; + margin-top: 8px; + display: inline-block; + } + .state:not(:first-child) { + margin-left: 8px; + } + ha-switch { + margin-top: 8px; } `, ]; diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 2609d7a1b3..13776ea117 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement { showAddUserDialog(this, { userAddedCallback: async (user: User) => { if (user) { - this._users = { ...this._users, ...user }; + this._users = [...this._users, user]; } }, }); diff --git a/src/panels/config/zha/zha-add-group-page.ts b/src/panels/config/zha/zha-add-group-page.ts index 733c040297..e5bdd726f0 100644 --- a/src/panels/config/zha/zha-add-group-page.ts +++ b/src/panels/config/zha/zha-add-group-page.ts @@ -6,6 +6,7 @@ import { css, CSSResult, PropertyValues, + query, } from "lit-element"; import "../../../layouts/hass-subpage"; @@ -18,7 +19,7 @@ import { addGroup, ZHAGroup, } from "../../../data/zha"; -import "./zha-devices-data-table"; +import { ZHADevicesDataTable } from "./zha-devices-data-table"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { navigate } from "../../../common/navigate"; import { PolymerChangedEvent } from "../../../polymer-types"; @@ -34,6 +35,8 @@ export class ZHAAddGroupPage extends LitElement { @property() public devices: ZHADevice[] = []; @property() private _processingAdd: boolean = false; @property() private _groupName: string = ""; + @query("zha-devices-data-table") + private _zhaDevicesDataTable!: ZHADevicesDataTable; private _firstUpdatedCalled: boolean = false; private _selectedDevicesToAdd: string[] = []; @@ -130,6 +133,7 @@ export class ZHAAddGroupPage extends LitElement { this._selectedDevicesToAdd = []; this._processingAdd = false; this._groupName = ""; + this._zhaDevicesDataTable.clearSelection(); navigate(this, `/config/zha/group/${group.group_id}`, true); } diff --git a/src/panels/config/zha/zha-clusters-data-table.ts b/src/panels/config/zha/zha-clusters-data-table.ts index 817fc71310..9216b832e5 100644 --- a/src/panels/config/zha/zha-clusters-data-table.ts +++ b/src/panels/config/zha/zha-clusters-data-table.ts @@ -9,10 +9,14 @@ import { TemplateResult, property, customElement, + query, } from "lit-element"; import { HomeAssistant } from "../../../types"; // tslint:disable-next-line -import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { + DataTableColumnContainer, + HaDataTable, +} from "../../../components/data-table/ha-data-table"; // tslint:disable-next-line import { Cluster } from "../../../data/zha"; import { formatAsPaddedHex } from "./functions"; @@ -27,6 +31,7 @@ export class ZHAClustersDataTable extends LitElement { @property() public hass!: HomeAssistant; @property() public narrow = false; @property() public clusters: Cluster[] = []; + @query("ha-data-table") private _dataTable!: HaDataTable; private _clusters = memoizeOne((clusters: Cluster[]) => { let outputClusters: ClusterRowData[] = clusters; @@ -77,6 +82,10 @@ export class ZHAClustersDataTable extends LitElement { } ); + public clearSelection() { + this._dataTable.clearSelection(); + } + protected render(): TemplateResult { return html` { let outputDevices: DeviceRowData[] = devices; @@ -89,6 +94,10 @@ export class ZHADevicesDataTable extends LitElement { } ); + public clearSelection() { + this._dataTable.clearSelection(); + } + protected render(): TemplateResult { return html` { let outputGroups: GroupRowData[] = groups; @@ -98,6 +103,10 @@ export class ZHAGroupsDataTable extends LitElement { } ); + public clearSelection() { + this._dataTable.clearSelection(); + } + protected render(): TemplateResult { return html` { + await import( + /* webpackChunkName: "hui-entity-card-editor" */ "../editor/config-elements/hui-entity-card-editor" + ); + return document.createElement("hui-entity-card-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFill: string[] + ) { + const includeDomains = ["sensor", "light", "switch"]; + const maxEntities = 1; + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFill, + includeDomains + ); + + return { + entity: foundEntities[0] || "", + }; + } + + @property() public hass?: HomeAssistant; + @property() private _config?: EntityCardConfig; + private _footerElement?: HuiErrorCard | LovelaceHeaderFooter; + + public setConfig(config: EntityCardConfig): void { + if (config.entity && !isValidEntityId(config.entity)) { + throw new Error("Invalid Entity"); + } + + this._config = config; + + if (this._config.footer) { + this._footerElement = createHeaderFooterElement(this._config.footer); + } else if (this._footerElement) { + this._footerElement = undefined; + } + } + + public getCardSize(): number { + return 1 + (this._config?.footer ? 1 : 0); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + const showUnit = this._config.attribute + ? this._config.attribute in stateObj.attributes + : stateObj.state !== UNKNOWN && stateObj.state !== UNAVAILABLE; + + return html` + +
+
+
+ ${this._config.name || computeStateName(stateObj)} +
+
+ +
+
+
+ ${"attribute" in this._config + ? stateObj.attributes[this._config.attribute!] || + this.hass.localize("state.default.unknown") + : this.hass.localize(`state.default.${stateObj.state}`) || + this.hass.localize( + `state.${this._config.entity.split(".")[0]}.${ + stateObj.state + }` + ) || + stateObj.state}${showUnit + ? html` + ${this._config.unit || + (this._config.attribute + ? "" + : stateObj.attributes.unit_of_measurement)} + ` + : ""} +
+
+ ${this._footerElement} +
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + // Side Effect used to update footer hass while keeping optimizations + if (this._footerElement) { + this._footerElement.hass = this.hass; + } + + return hasConfigOrEntityChanged(this, changedProps); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as + | EntityCardConfig + | undefined; + + if ( + !oldHass || + !oldConfig || + oldHass.themes !== this.hass.themes || + oldConfig.theme !== this._config.theme + ) { + applyThemesOnElement(this, this.hass.themes, this._config!.theme); + } + } + + private _handleClick(): void { + fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); + } + + static get styles(): CSSResult { + return css` + ha-card > div { + cursor: pointer; + } + + .header { + display: flex; + padding: 8px 16px 0; + justify-content: space-between; + } + + .name { + color: var(--secondary-text-color); + line-height: 40px; + font-weight: 500; + font-size: 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .icon { + color: var(--state-icon-color, #44739e); + line-height: 40px; + } + + .info { + padding: 0px 16px 16px; + margin-top: -4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 28px; + } + + .value { + font-size: 28px; + margin-right: 4px; + } + + .measurement { + font-size: 18px; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-card": HuiEntityCard; + } +} diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts index 59fb921970..74a4ba1100 100644 --- a/src/panels/lovelace/cards/hui-gauge-card.ts +++ b/src/panels/lovelace/cards/hui-gauge-card.ts @@ -82,7 +82,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { if (!isValidEntityId(config.entity)) { throw new Error("Invalid Entity"); } - this._config = { min: 0, max: 100, theme: "default", ...config }; + this._config = { min: 0, max: 100, ...config }; } public connectedCallback(): void { diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 5062eb4c11..c892148259 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -74,7 +74,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { } public setConfig(config: GlanceCardConfig): void { - this._config = { theme: "default", state_color: true, ...config }; + this._config = { state_color: true, ...config }; const entities = processConfigEntities(config.entities); for (const entity of entities) { diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index cd7d756fb1..c38bf1a98a 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -70,7 +70,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { throw new Error("Entities need to be an array"); } - this._config = { theme: "default", ...config }; + this._config = config; this._configEntities = config.entities ? processConfigEntities(config.entities) : []; diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 133bf7245f..70e807d45e 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -25,12 +25,15 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { HomeAssistant, LightEntity } from "../../../types"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; -import { toggleEntity } from "../common/entity/toggle-entity"; import { LightCardConfig } from "./types"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { SUPPORT_BRIGHTNESS } from "../../../data/light"; import { findEntities } from "../common/find-entites"; import { UNAVAILABLE } from "../../../data/entity"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { hasAction } from "../common/has-action"; +import { ActionHandlerEvent } from "../../../data/lovelace"; +import { handleAction } from "../common/handle-action"; @customElement("hui-light-card") export class HuiLightCard extends LitElement implements LovelaceCard { @@ -74,7 +77,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard { throw new Error("Specify an entity from within the light domain."); } - this._config = { theme: "default", ...config }; + this._config = { + ...config, + tap_action: { action: "toggle" }, + }; } protected render(): TemplateResult { @@ -143,7 +149,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard { filter: this._computeBrightness(stateObj), color: this._computeColor(stateObj), })} - @click=${this._handleClick} + @action=${this._handleAction} + .actionHandler=${actionHandler({ + hasHold: hasAction(this._config!.hold_action), + hasDoubleClick: hasAction(this._config!.double_tap_action), + })} tabindex="0" > @@ -222,7 +232,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } private _computeBrightness(stateObj: LightEntity): string { - if (!stateObj.attributes.brightness) { + if (stateObj.state === "off" || !stateObj.attributes.brightness) { return ""; } const brightness = stateObj.attributes.brightness; @@ -230,7 +240,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } private _computeColor(stateObj: LightEntity): string { - if (!stateObj.attributes.hs_color) { + if (stateObj.state === "off" || !stateObj.attributes.hs_color) { return ""; } const [hue, sat] = stateObj.attributes.hs_color; @@ -240,8 +250,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard { return `hsl(${hue}, 100%, ${100 - sat / 2}%)`; } - private _handleClick() { - toggleEntity(this.hass!, this._config!.entity!); + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); } private _handleMoreInfo() { diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 6c751696d7..445177e44b 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -1,5 +1,13 @@ import "@polymer/paper-icon-button/paper-icon-button"; -import { Layer, Marker, Circle, Map } from "leaflet"; +import { + Layer, + Marker, + Circle, + Map, + CircleMarker, + Polyline, + LatLngTuple, +} from "leaflet"; import { LitElement, TemplateResult, @@ -10,7 +18,6 @@ import { CSSResult, customElement, } from "lit-element"; - import "../../map/ha-entity-marker"; import { @@ -32,6 +39,9 @@ import { MapCardConfig } from "./types"; import { classMap } from "lit-html/directives/class-map"; import { findEntities } from "../common/find-entites"; +import { HassEntity } from "home-assistant-js-websocket"; +import { fetchRecent } from "../../../data/history"; + @customElement("hui-map-card") class HuiMapCard extends LitElement implements LovelaceCard { public static async getConfigElement() { @@ -66,6 +76,10 @@ class HuiMapCard extends LitElement implements LovelaceCard { @property({ type: Boolean, reflect: true }) public editMode = false; + @property() + private _history?: HassEntity[][]; + private _date?: Date; + @property() private _config?: MapCardConfig; private _configEntities?: EntityConfig[]; @@ -86,7 +100,24 @@ class HuiMapCard extends LitElement implements LovelaceCard { ); private _mapItems: Array = []; private _mapZones: Array = []; + private _mapPaths: Array = []; private _connected = false; + private _colorDict: { [key: string]: string } = {}; + private _colorIndex: number = 0; + private _colors: string[] = [ + "#0288D1", + "#00AA00", + "#984ea3", + "#00d2d5", + "#ff7f00", + "#af8d00", + "#7f80cd", + "#b3e900", + "#c42e60", + "#a65628", + "#f781bf", + "#8dd3c7", + ]; public setConfig(config: MapCardConfig): void { if (!config) { @@ -112,6 +143,8 @@ class HuiMapCard extends LitElement implements LovelaceCard { this._configEntities = config.entities ? processConfigEntities(config.entities) : []; + + this._cleanupHistory(); } public getCardSize(): number { @@ -223,7 +256,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { } protected updated(changedProps: PropertyValues): void { - if (changedProps.has("hass")) { + if (changedProps.has("hass") || changedProps.has("_history")) { this._drawEntities(); this._fitMap(); } @@ -233,6 +266,15 @@ class HuiMapCard extends LitElement implements LovelaceCard { ) { this.updateMap(changedProps.get("_config") as MapCardConfig); } + + if (this._config!.hours_to_show && this._configEntities?.length) { + const minute = 60000; + if (changedProps.has("_config")) { + this._getHistory(); + } else if (Date.now() - this._date!.getTime() >= minute) { + this._getHistory(); + } + } } private get _mapEl(): HTMLDivElement { @@ -285,9 +327,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { return; } - const bounds = this.Leaflet.latLngBounds( - this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : [] - ); + const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds(); this._leafletMap.fitBounds(bounds.pad(0.5)); if (zoom && this._leafletMap.getZoom() > zoom) { @@ -295,6 +335,18 @@ class HuiMapCard extends LitElement implements LovelaceCard { } } + private _getColor(entityId: string) { + let color; + if (this._colorDict[entityId]) { + color = this._colorDict[entityId]; + } else { + color = this._colors[this._colorIndex]; + this._colorIndex = (this._colorIndex + 1) % this._colors.length; + this._colorDict[entityId] = color; + } + return color; + } + private _drawEntities(): void { const hass = this.hass; const map = this._leafletMap; @@ -314,6 +366,11 @@ class HuiMapCard extends LitElement implements LovelaceCard { } const mapZones: Layer[] = (this._mapZones = []); + if (this._mapPaths) { + this._mapPaths.forEach((marker) => marker.remove()); + } + const mapPaths: Layer[] = (this._mapPaths = []); + const allEntities = this._configEntities!.concat(); // Calculate visible geo location sources @@ -331,6 +388,60 @@ class HuiMapCard extends LitElement implements LovelaceCard { } } + // DRAW history + if (this._config!.hours_to_show && this._history) { + for (const entityStates of this._history) { + if (entityStates?.length <= 1) { + continue; + } + const entityId = entityStates[0].entity_id; + + // filter location data from states and remove all invalid locations + const path = entityStates.reduce( + (accumulator: LatLngTuple[], state) => { + const latitude = state.attributes.latitude; + const longitude = state.attributes.longitude; + if (latitude && longitude) { + accumulator.push([latitude, longitude] as LatLngTuple); + } + return accumulator; + }, + [] + ) as LatLngTuple[]; + + // DRAW HISTORY + for ( + let markerIndex = 0; + markerIndex < path.length - 1; + markerIndex++ + ) { + const opacityStep = 0.8 / (path.length - 2); + const opacity = 0.2 + markerIndex * opacityStep; + + // DRAW history path dots + mapPaths.push( + Leaflet.circleMarker(path[markerIndex], { + radius: 3, + color: this._getColor(entityId), + opacity, + interactive: false, + }) + ); + + // DRAW history path lines + const line = [path[markerIndex], path[markerIndex + 1]]; + mapPaths.push( + Leaflet.polyline(line, { + color: this._getColor(entityId), + opacity, + interactive: false, + }) + ); + } + } + } + + // DRAW entities for (const entity of allEntities) { const entityId = entity.entity; const stateObj = hass.states[entityId]; @@ -414,6 +525,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { entity-id="${entityId}" entity-name="${entityName}" entity-picture="${entityPicture || ""}" + entity-color="${this._getColor(entityId)}" > `, iconSize: [48, 48], @@ -428,7 +540,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { mapItems.push( Leaflet.circle([latitude, longitude], { interactive: false, - color: "#0288D1", + color: this._getColor(entityId), radius: gpsAccuracy, }) ); @@ -437,6 +549,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { this._mapItems.forEach((marker) => map.addLayer(marker)); this._mapZones.forEach((marker) => map.addLayer(marker)); + this._mapPaths.forEach((marker) => map.addLayer(marker)); } private _attachObserver(): void { @@ -455,6 +568,62 @@ class HuiMapCard extends LitElement implements LovelaceCard { } } + private async _getHistory(): Promise { + this._date = new Date(); + + if (!this._configEntities) { + return; + } + + const entityIds = this._configEntities!.map((entity) => entity.entity).join( + "," + ); + const endTime = new Date(); + const startTime = new Date(); + startTime.setHours(endTime.getHours() - this._config!.hours_to_show!); + const skipInitialState = false; + const significantChangesOnly = false; + + const stateHistory = await fetchRecent( + this.hass, + entityIds, + startTime, + endTime, + skipInitialState, + significantChangesOnly + ); + + if (stateHistory.length < 1) { + return; + } + + this._history = stateHistory; + } + + private _cleanupHistory() { + if (!this._history) { + return; + } + if (this._config!.hours_to_show! <= 0) { + this._history = undefined; + } else { + // remove unused entities + const configEntityIds = this._configEntities?.map( + (configEntity) => configEntity.entity + ); + this._history = this._history!.reduce( + (accumulator: HassEntity[][], entityStates) => { + const entityId = entityStates[0].entity_id; + if (configEntityIds?.includes(entityId)) { + accumulator.push(entityStates); + } + return accumulator; + }, + [] + ) as HassEntity[][]; + } + } + static get styles(): CSSResult { return css` :host([ispanel]) ha-card { diff --git a/src/panels/lovelace/cards/hui-markdown-card.ts b/src/panels/lovelace/cards/hui-markdown-card.ts index 4ccdc3dbae..ec317d0ce4 100644 --- a/src/panels/lovelace/cards/hui-markdown-card.ts +++ b/src/panels/lovelace/cards/hui-markdown-card.ts @@ -119,7 +119,10 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { { template: this._config.content, entity_ids: this._config.entity_id, - variables: { config: this._config }, + variables: { + config: this._config, + user: this._hass.user!.name, + }, } ); this._unsubRenderTemplate.catch(() => { diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 63e16b9e16..9e60be8de2 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -212,7 +212,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { throw new Error("Specify an entity from within the media_player domain."); } - this._config = { theme: "default", ...config }; + this._config = config; } public connectedCallback(): void { diff --git a/src/panels/lovelace/cards/hui-sensor-card.ts b/src/panels/lovelace/cards/hui-sensor-card.ts index 2d5858d49f..b3718308c4 100644 --- a/src/panels/lovelace/cards/hui-sensor-card.ts +++ b/src/panels/lovelace/cards/hui-sensor-card.ts @@ -1,174 +1,15 @@ -import { - html, - svg, - LitElement, - PropertyValues, - TemplateResult, - customElement, - property, - css, - CSSResult, -} from "lit-element"; -import "@polymer/paper-spinner/paper-spinner"; - -import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import { stateIcon } from "../../../common/entity/state_icon"; - -import "../../../components/ha-card"; -import "../../../components/ha-icon"; -import "../components/hui-warning"; - -import { LovelaceCard, LovelaceCardEditor } from "../types"; -import { HomeAssistant } from "../../../types"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { fetchRecent } from "../../../data/history"; -import { SensorCardConfig } from "./types"; -import { hasConfigOrEntityChanged } from "../common/has-changed"; -import { actionHandler } from "../common/directives/action-handler-directive"; -import { findEntities } from "../common/find-entites"; +import { customElement } from "lit-element"; import { HassEntity } from "home-assistant-js-websocket/dist/types"; -const strokeWidth = 5; - -const average = (items): number => { - return ( - items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / - items.length - ); -}; - -const lastValue = (items): number => { - return parseFloat(items[items.length - 1].state) || 0; -}; - -const midPoint = ( - _Ax: number, - _Ay: number, - _Bx: number, - _By: number -): number[] => { - const _Zx = (_Ax - _Bx) / 2 + _Bx; - const _Zy = (_Ay - _By) / 2 + _By; - return [_Zx, _Zy]; -}; - -const getPath = (coords: number[][]): string => { - let next; - let Z; - const X = 0; - const Y = 1; - let path = ""; - let last = coords.filter(Boolean)[0]; - - path += `M ${last[X]},${last[Y]}`; - - for (const coord of coords) { - next = coord; - Z = midPoint(last[X], last[Y], next[X], next[Y]); - path += ` ${Z[X]},${Z[Y]}`; - path += ` Q${next[X]},${next[Y]}`; - last = next; - } - - path += ` ${next[X]},${next[Y]}`; - return path; -}; - -const calcPoints = ( - history: any, - hours: number, - width: number, - detail: number, - min: number, - max: number -): number[][] => { - const coords = [] as number[][]; - const height = 80; - let yRatio = (max - min) / height; - yRatio = yRatio !== 0 ? yRatio : height; - let xRatio = width / (hours - (detail === 1 ? 1 : 0)); - xRatio = isFinite(xRatio) ? xRatio : width; - - const first = history.filter(Boolean)[0]; - let last = [average(first), lastValue(first)]; - - const getCoords = (item, i, offset = 0, depth = 1) => { - if (depth > 1 && item) { - return item.forEach((subItem, index) => - getCoords(subItem, i, index, depth - 1) - ); - } - - const x = xRatio * (i + offset / 6); - - if (item) { - last = [average(item), lastValue(item)]; - } - const y = - height + strokeWidth / 2 - ((item ? last[0] : last[1]) - min) / yRatio; - return coords.push([x, y]); - }; - - for (let i = 0; i < history.length; i += 1) { - getCoords(history[i], i, 0, detail); - } - - if (coords.length === 1) { - coords[1] = [width, coords[0][1]]; - } - - coords.push([width, coords[coords.length - 1][1]]); - return coords; -}; - -const coordinates = ( - history: any, - hours: number, - width: number, - detail: number -): number[][] => { - history.forEach((item) => (item.state = Number(item.state))); - history = history.filter((item) => !Number.isNaN(item.state)); - - const min = Math.min.apply( - Math, - history.map((item) => item.state) - ); - const max = Math.max.apply( - Math, - history.map((item) => item.state) - ); - const now = new Date().getTime(); - - const reduce = (res, item, point) => { - const age = now - new Date(item.last_changed).getTime(); - - let key = Math.abs(age / (1000 * 3600) - hours); - if (point) { - key = (key - Math.floor(key)) * 60; - key = Number((Math.round(key / 10) * 10).toString()[0]); - } else { - key = Math.floor(key); - } - if (!res[key]) { - res[key] = []; - } - res[key].push(item); - return res; - }; - - history = history.reduce((res, item) => reduce(res, item, false), []); - if (detail > 1) { - history = history.map((entry) => - entry.reduce((res, item) => reduce(res, item, true), []) - ); - } - return calcPoints(history, hours, width, detail, min, max); -}; +import { LovelaceCardEditor } from "../types"; +import { HomeAssistant } from "../../../types"; +import { SensorCardConfig, EntityCardConfig } from "./types"; +import { GraphHeaderFooterConfig } from "../header-footer/types"; +import { findEntities } from "../common/find-entites"; +import { HuiEntityCard } from "./hui-entity-card"; @customElement("hui-sensor-card") -class HuiSensorCard extends LitElement implements LovelaceCard { +class HuiSensorCard extends HuiEntityCard { public static async getConfigElement(): Promise { await import( /* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor" @@ -202,304 +43,30 @@ class HuiSensorCard extends LitElement implements LovelaceCard { return { type: "sensor", entity: foundEntities[0] || "", graph: "line" }; } - @property() public hass?: HomeAssistant; - - @property() private _config?: SensorCardConfig; - - @property() private _history?: any; - - private _date?: Date; - public setConfig(config: SensorCardConfig): void { if (!config.entity || config.entity.split(".")[0] !== "sensor") { throw new Error("Specify an entity from within the sensor domain."); } - const cardConfig = { - detail: 1, - theme: "default", - hours_to_show: 24, - ...config, + const { graph, detail, hours_to_show, ...cardConfig } = config; + + const entityCardConfig: EntityCardConfig = { + ...cardConfig, + type: "entity", }; - cardConfig.hours_to_show = Number(cardConfig.hours_to_show); - cardConfig.detail = - cardConfig.detail === 1 || cardConfig.detail === 2 - ? cardConfig.detail - : 1; + if (graph === "line") { + const footerConfig: GraphHeaderFooterConfig = { + type: "graph", + entity: config.entity, + detail: detail || 1, + hours_to_show: hours_to_show || 24, + }; - this._config = cardConfig; - } - - public getCardSize(): number { - return 3; - } - - protected render(): TemplateResult { - if (!this._config || !this.hass) { - return html``; + entityCardConfig.footer = footerConfig; } - const stateObj = this.hass.states[this._config.entity]; - - if (!stateObj) { - return html` - ${this.hass.localize( - "ui.panel.lovelace.warning.entity_not_found", - "entity", - this._config.entity - )} - `; - } - - let graph; - - if (stateObj && this._config.graph === "line") { - if (!stateObj.attributes.unit_of_measurement) { - return html` - Entity: ${this._config.entity} - Has no Unit of Measurement and - therefore can not display a line graph. - `; - } else if (!this._history) { - graph = svg` - - `; - } else { - graph = svg` - - - - - - - - - - - - - `; - } - } else { - graph = ""; - } - return html` - -
-
- ${this._config.name || computeStateName(stateObj)} -
-
- -
-
-
- ${stateObj.state} - ${this._config.unit || - stateObj.attributes.unit_of_measurement} -
-
${graph}
-
- `; - } - - protected firstUpdated(): void { - this._date = new Date(); - } - - protected shouldUpdate(changedProps: PropertyValues): boolean { - if (changedProps.has("_history")) { - return true; - } - - return hasConfigOrEntityChanged(this, changedProps); - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (!this._config || !this.hass) { - return; - } - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - const oldConfig = changedProps.get("_config") as - | SensorCardConfig - | undefined; - - if ( - !oldHass || - !oldConfig || - oldHass.themes !== this.hass.themes || - oldConfig.theme !== this._config.theme - ) { - applyThemesOnElement(this, this.hass.themes, this._config!.theme); - } - - if (this._config.graph === "line") { - const minute = 60000; - if (changedProps.has("_config")) { - this._getHistory(); - } else if (Date.now() - this._date!.getTime() >= minute) { - this._getHistory(); - } - } - } - - private _handleClick(): void { - fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); - } - - private async _getHistory(): Promise { - const endTime = new Date(); - const startTime = new Date(); - startTime.setHours(endTime.getHours() - this._config!.hours_to_show!); - - const stateHistory = await fetchRecent( - this.hass, - this._config!.entity, - startTime, - endTime - ); - - if (stateHistory.length < 1 || stateHistory[0].length < 1) { - return; - } - - const coords = coordinates( - stateHistory[0], - this._config!.hours_to_show!, - 500, - this._config!.detail! - ); - - this._history = getPath(coords); - this._date = new Date(); - } - - static get styles(): CSSResult { - return css` - :host { - display: flex; - flex-direction: column; - } - - ha-card { - display: flex; - flex-direction: column; - flex: 1; - position: relative; - cursor: pointer; - overflow: hidden; - } - - ha-card:focus { - outline: none; - background: var(--divider-color); - } - - .flex { - display: flex; - } - - .header { - margin: 8px 16px 0; - justify-content: space-between; - } - - .name { - align-items: center; - display: flex; - min-width: 0; - opacity: 0.8; - position: relative; - } - - .name > span { - display: block; - display: -webkit-box; - font-size: 1.2rem; - font-weight: 500; - max-height: 1.4rem; - top: 2px; - opacity: 0.8; - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - word-wrap: break-word; - word-break: break-all; - } - - .icon { - color: var(--paper-item-icon-color, #44739e); - line-height: 40px; - } - - .info { - flex-wrap: wrap; - margin: 0 16px 16px; - } - - #value { - display: inline-block; - font-size: 2rem; - font-weight: 400; - line-height: 1em; - margin-right: 4px; - } - - #measurement { - align-self: flex-end; - display: inline-block; - font-size: 1.3rem; - line-height: 1.2em; - margin-top: 0.1em; - opacity: 0.6; - vertical-align: bottom; - } - - .graph { - align-self: flex-end; - margin: auto; - margin-bottom: 0px; - position: relative; - width: 100%; - overflow: hidden; - } - - .graph > div { - align-self: flex-end; - margin: auto 0px; - display: flex; - } - - .fill { - opacity: 0.1; - } - `; + super.setConfig(entityCardConfig); } } diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index fe8161ecc0..f6f3a73866 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -87,7 +87,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { throw new Error("Specify an entity from within the climate domain."); } - this._config = { theme: "default", ...config }; + this._config = config; } public connectedCallback(): void { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index e00b58b83f..77f910c082 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -22,6 +22,11 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig { title?: string; } +export interface EntityCardConfig extends LovelaceCardConfig { + attribute?: string; + unit?: string; +} + export interface EntitiesCardEntityConfig extends EntityConfig { type?: string; secondary_info?: "entity-id" | "last-changed"; @@ -133,6 +138,9 @@ export interface LightCardConfig extends LovelaceCardConfig { name?: string; theme?: string; icon?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; } export interface MapCardConfig extends LovelaceCardConfig { @@ -141,6 +149,7 @@ export interface MapCardConfig extends LovelaceCardConfig { aspect_ratio?: string; default_zoom?: number; entities?: Array; + hours_to_show?: number; geo_location_sources?: string[]; dark_mode?: boolean; } diff --git a/src/panels/lovelace/common/call-service.ts b/src/panels/lovelace/common/call-service.ts deleted file mode 100644 index de3b39984b..0000000000 --- a/src/panels/lovelace/common/call-service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { HomeAssistant } from "../../../types"; -import { CallServiceConfig } from "../entity-rows/types"; - -export const callService = ( - config: CallServiceConfig, - hass: HomeAssistant -): void => { - const entityId = config.entity; - const [domain, service] = config.service.split(".", 2); - const serviceData = { entity_id: entityId, ...config.service_data }; - hass.callService(domain, service, serviceData); -}; diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index 14edcc629f..a3a1387b7d 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -30,26 +30,19 @@ export class HuiConditionalBase extends UpdatingElement { throw new Error("Conditions are invalid."); } - if (this._element && this._element.parentElement) { - this.removeChild(this._element); - } - this._config = config; + this.style.display = "none"; } protected update(): void { - if (!this._element || !this.hass) { + if (!this._element || !this.hass || !this._config) { return; } - const visible = - this._config && checkConditionsMet(this._config.conditions, this.hass); + const visible = checkConditionsMet(this._config.conditions, this.hass); if (visible) { this._element.hass = this.hass; - if (!this._element.parentElement) { - this.appendChild(this._element); - } } this.style.setProperty("display", visible ? "" : "none"); diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 2fab4496c0..3a2ed953d0 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -1,4 +1,5 @@ import "../cards/hui-entities-card"; +import "../cards/hui-entity-card"; import "../cards/hui-button-card"; import "../cards/hui-entity-button-card"; import "../cards/hui-glance-card"; @@ -16,6 +17,7 @@ import { } from "./create-element-base"; const ALWAYS_LOADED_TYPES = new Set([ + "entity", "entities", "button", "entity-button", diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 4c8879b93c..c2c01cb279 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -17,8 +17,8 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { LovelaceElementConfig, LovelaceElement } from "../elements/types"; import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; +import { CUSTOM_TYPE_PREFIX } from "../../../data/lovelace_custom_cards"; -const CUSTOM_TYPE_PREFIX = "custom:"; const TIMEOUT = 2000; interface CreateElementConfigTypes { diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index d23464f7fb..8fd0e6019e 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -4,6 +4,8 @@ import "../entity-rows/hui-script-entity-row"; import "../entity-rows/hui-sensor-entity-row"; import "../entity-rows/hui-text-entity-row"; import "../entity-rows/hui-toggle-entity-row"; +import "../special-rows/hui-button-row"; +import "../special-rows/hui-attribute-row"; import "../special-rows/hui-call-service-row"; import { EntityConfig } from "../entity-rows/types"; import { createLovelaceElement } from "./create-element-base"; @@ -15,6 +17,7 @@ const ALWAYS_LOADED_TYPES = new Set([ "sensor-entity", "text-entity", "toggle-entity", + "button", "call-service", ]); const LAZY_LOAD_TYPES = { @@ -31,11 +34,13 @@ const LAZY_LOAD_TYPES = { "lock-entity": () => import("../entity-rows/hui-lock-entity-row"), "timer-entity": () => import("../entity-rows/hui-timer-entity-row"), conditional: () => import("../special-rows/hui-conditional-row"), + "weather-entity": () => import("../entity-rows/hui-weather-entity-row"), divider: () => import("../special-rows/hui-divider-row"), section: () => import("../special-rows/hui-section-row"), weblink: () => import("../special-rows/hui-weblink-row"), cast: () => import("../special-rows/hui-cast-row"), buttons: () => import("../special-rows/hui-buttons-row"), + attribute: () => import("../special-rows/hui-attribute-row"), }; const DOMAIN_TO_ELEMENT_TYPE = { _domain_not_found: "text", @@ -63,6 +68,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { // water heater should get it's own row. water_heater: "climate", input_datetime: "input-datetime", + weather: "weather", }; export const createRowElement = (config: EntityConfig) => diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts index 8738df3943..90043f69e8 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts @@ -23,6 +23,7 @@ import { HaCodeEditor } from "../../../../components/ha-code-editor"; import { fireEvent } from "../../../../common/dom/fire_event"; import { EntityConfig } from "../../entity-rows/types"; import { getCardElementClass } from "../../create-element/create-card-element"; +import { GUIModeChangedEvent } from "../types"; declare global { interface HASSDomEvents { @@ -33,6 +34,7 @@ declare global { config: LovelaceCardConfig; error?: string; }; + "GUImode-changed": GUIModeChangedEvent; } } @@ -85,16 +87,29 @@ export class HuiCardEditor extends LitElement { } } + public get hasWarning(): boolean { + return this._warning !== undefined; + } + public get hasError(): boolean { return this._error !== undefined; } + public get GUImode(): boolean { + return this._GUImode; + } + + public set GUImode(guiMode: boolean) { + this._GUImode = guiMode; + fireEvent(this as HTMLElement, "GUImode-changed", { guiMode }); + } + private get _yamlEditor(): HaCodeEditor { return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor; } public toggleMode() { - this._GUImode = !this._GUImode; + this.GUImode = !this.GUImode; } public connectedCallback() { @@ -105,7 +120,7 @@ export class HuiCardEditor extends LitElement { protected render(): TemplateResult { return html`
- ${this._GUImode + ${this.GUImode ? html`
${this._loading @@ -145,18 +160,6 @@ export class HuiCardEditor extends LitElement {
` : ""} -
- - ${this.hass!.localize( - this._GUImode - ? "ui.panel.lovelace.editor.edit_card.show_code_editor" - : "ui.panel.lovelace.editor.edit_card.show_visual_editor" - )} - -
`; } @@ -165,7 +168,7 @@ export class HuiCardEditor extends LitElement { super.updated(changedProperties); if (changedProperties.has("_GUImode")) { - if (this._GUImode === false) { + if (this.GUImode === false) { // Refresh code editor when switching to yaml mode this._refreshYamlEditor(true); } @@ -245,6 +248,7 @@ export class HuiCardEditor extends LitElement { this._handleUIConfigChanged(ev as UIConfigChangedEvent) ); + this.GUImode = true; return; } catch (err) { if (err.message.startsWith("WARNING:")) { @@ -252,7 +256,7 @@ export class HuiCardEditor extends LitElement { } else { this._error = err; } - this._GUImode = false; + this.GUImode = false; } finally { this._loading = false; fireEvent(this, "iron-resize"); @@ -277,10 +281,6 @@ export class HuiCardEditor extends LitElement { .warning { color: #ffa726; } - .buttons { - text-align: right; - padding: 8px 0px; - } paper-spinner { display: block; margin: auto; diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 78e279b095..b69ab18973 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -23,11 +23,17 @@ import { calcUnusedEntities, } from "../../common/compute-unused-entities"; import { UNKNOWN, UNAVAILABLE } from "../../../../data/entity"; +import { + customCards, + getCustomCardEntry, + CUSTOM_TYPE_PREFIX, +} from "../../../../data/lovelace_custom_cards"; const previewCards: string[] = [ "alarm-panel", "button", "entities", + "entity", "gauge", "glance", "history-graph", @@ -99,6 +105,24 @@ export class HuiCardPicker extends LitElement { `; })} + ${customCards.length + ? html` +
+ ${customCards.map((card) => { + return html` + ${until( + this._renderCardElement(card.type, true, true), + html` +
+ +
+ ` + )} + `; + })} +
+ ` + : ""}
{ + const customCard = isCustom ? getCustomCardEntry(type) : undefined; + if (isCustom) { + type = `${CUSTOM_TYPE_PREFIX}${type}`; + } + let element: LovelaceCard | undefined; let cardConfig: LovelaceCardConfig = { type }; @@ -261,7 +291,7 @@ export class HuiCardPicker extends LitElement { this._usedEntities! ); - if (!noElement) { + if (!noElement || customCard?.preview) { element = this._createCardElement(cardConfig); } } @@ -273,20 +303,25 @@ export class HuiCardPicker extends LitElement { description: !element || element.tagName === "HUI-ERROR-CARD", })}" > - ${!element || element.tagName === "HUI-ERROR-CARD" - ? html` - ${this.hass!.localize( - `ui.panel.lovelace.editor.card.${cardConfig.type}.description` - )} - ` - : html` - ${element} - `} + ${element && element.tagName !== "HUI-ERROR-CARD" + ? element + : customCard + ? customCard.description || + this.hass!.localize( + `ui.panel.lovelace.editor.cardpicker.no_description` + ) + : this.hass!.localize( + `ui.panel.lovelace.editor.card.${cardConfig.type}.description` + )}
- ${this.hass!.localize( - `ui.panel.lovelace.editor.card.${cardConfig.type}.name` - )} + ${customCard + ? `${this.hass!.localize( + "ui.panel.lovelace.editor.cardpicker.custom_card" + )}: ${customCard.name || customCard.type}` + : this.hass!.localize( + `ui.panel.lovelace.editor.card.${cardConfig.type}.name` + )}
`; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index 9e8ff26856..092482c5fe 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -6,6 +6,7 @@ import { CSSResultArray, customElement, property, + query, } from "lit-element"; import deepFreeze from "deep-freeze"; @@ -27,6 +28,7 @@ import { addCard, replaceCard } from "../config-util"; import "../../../../components/dialog/ha-paper-dialog"; import { haStyleDialog } from "../../../../resources/styles"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import { GUIModeChangedEvent } from "../types"; declare global { // for fire event @@ -51,6 +53,9 @@ export class HuiDialogEditCard extends LitElement { @property() private _saving: boolean = false; @property() private _error?: string; + @query("hui-card-editor") private _cardEditorEl?: HuiCardEditor; + @property() private _GUImode?: boolean; + public async showDialog(params: EditCardDialogParams): Promise { this._params = params; const [view, card] = params.path; @@ -62,10 +67,6 @@ export class HuiDialogEditCard extends LitElement { } } - private get _cardEditorEl(): HuiCardEditor | null { - return this.shadowRoot!.querySelector("hui-card-editor"); - } - protected render(): TemplateResult { if (!this._params) { return html``; @@ -99,9 +100,9 @@ export class HuiDialogEditCard extends LitElement { ${this._cardConfig === undefined ? html` ` : html` @@ -109,15 +110,16 @@ export class HuiDialogEditCard extends LitElement {
${this._error @@ -133,14 +135,30 @@ export class HuiDialogEditCard extends LitElement { `}
- + ${this._cardConfig !== undefined + ? html` + + ${this.hass!.localize( + !this._cardEditorEl || this._GUImode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + + ` + : ""} + ${this.hass!.localize("ui.common.cancel")} ${this._cardConfig !== undefined ? html` ${this._saving ? html` @@ -250,6 +268,9 @@ export class HuiDialogEditCard extends LitElement { display: block; width: 100%; } + .gui-mode-button { + margin-right: auto; + } `, ]; } @@ -278,6 +299,15 @@ export class HuiDialogEditCard extends LitElement { } } + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._GUImode = ev.detail.guiMode; + } + + private _toggleMode(): void { + this._cardEditorEl?.toggleMode(); + } + private _close(): void { this._params = undefined; this._cardConfig = undefined; diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index b1e1a1f342..a849401772 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -56,7 +56,7 @@ export class HuiAlarmPanelCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "Backend-selected"; + return this._config!.theme || ""; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index f8f292bb0e..fc80cde874 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -84,7 +84,7 @@ export class HuiButtonCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "default"; + return this._config!.theme || ""; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index 0fd383e2ba..02cd1b8574 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -63,7 +63,7 @@ export class HuiEntitiesCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "Backend-selected"; + return this._config!.theme || ""; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts new file mode 100644 index 0000000000..511be106e3 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -0,0 +1,189 @@ +import { + html, + LitElement, + TemplateResult, + customElement, + property, +} from "lit-element"; +import "@polymer/paper-input/paper-input"; + +import "../../components/hui-action-editor"; +import "../../components/hui-theme-select-editor"; +import "../../components/hui-entity-editor"; + +import { struct } from "../../common/structs/struct"; +import { EntitiesEditorEvent, EditorTarget } from "../types"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCardEditor } from "../../types"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { configElementStyle } from "./config-elements-style"; +import { EntityCardConfig } from "../../cards/types"; +import { headerFooterConfigStructs } from "../../header-footer/types"; + +const cardConfigStruct = struct({ + type: "string", + entity: "string?", + name: "string?", + icon: "string?", + attribute: "string?", + unit: "string?", + theme: "string?", + header: struct.optional(headerFooterConfigStructs), + footer: struct.optional(headerFooterConfigStructs), +}); + +@customElement("hui-entity-card-editor") +export class HuiEntityCardEditor extends LitElement + implements LovelaceCardEditor { + @property() public hass?: HomeAssistant; + + @property() private _config?: EntityCardConfig; + + public setConfig(config: EntityCardConfig): void { + config = cardConfigStruct(config); + this._config = config; + } + + get _entity(): string { + return this._config!.entity || ""; + } + + get _name(): string { + return this._config!.name || ""; + } + + get _icon(): string { + return this._config!.icon || ""; + } + + get _attribute(): string { + return this._config!.attribute || ""; + } + + get _unit(): string { + return this._config!.unit || ""; + } + + get _theme(): string { + return this._config!.theme || ""; + } + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + return html` + ${configElementStyle} +
+ +
+ + +
+
+ + +
+ +
+ `; + } + + private _valueChanged(ev: EntitiesEditorEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + + if ( + this[`_${target.configValue}`] === target.value || + this[`_${target.configValue}`] === target.config + ) { + return; + } + if (target.configValue) { + if (target.value === "") { + delete this._config[target.configValue!]; + } else { + let newValue: string | undefined; + if ( + target.configValue === "icon_height" && + !isNaN(Number(target.value)) + ) { + newValue = `${String(target.value)}px`; + } + this._config = { + ...this._config, + [target.configValue!]: + target.checked !== undefined + ? target.checked + : newValue !== undefined + ? newValue + : target.value + ? target.value + : target.config, + }; + } + } + fireEvent(this, "config-changed", { config: this._config }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-card-editor": HuiEntityCardEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts index 178dd504f6..8e17c179cd 100644 --- a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts @@ -57,7 +57,7 @@ export class HuiGaugeCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "default"; + return this._config!.theme || ""; } get _min(): number { diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index 5ec82a3237..a4cb511a20 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -60,7 +60,7 @@ export class HuiGlanceCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "Backend-selected"; + return this._config!.theme || ""; } get _columns(): number { diff --git a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts index 88992579a6..3564980a63 100644 --- a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts @@ -8,18 +8,23 @@ import { import "@polymer/paper-input/paper-input"; import "../../components/hui-theme-select-editor"; - +import "../../components/hui-action-editor"; import "../../../../components/ha-icon-input"; import "../../components/hui-entity-editor"; import { struct } from "../../common/structs/struct"; -import { EntitiesEditorEvent, EditorTarget } from "../types"; +import { + EntitiesEditorEvent, + EditorTarget, + actionConfigStruct, +} from "../types"; import { HomeAssistant } from "../../../../types"; import { LovelaceCardEditor } from "../../types"; import { fireEvent } from "../../../../common/dom/fire_event"; import { configElementStyle } from "./config-elements-style"; import { LightCardConfig } from "../../cards/types"; import { stateIcon } from "../../../../common/entity/state_icon"; +import { ActionConfig } from "../../../../data/lovelace"; const cardConfigStruct = struct({ type: "string", @@ -27,6 +32,8 @@ const cardConfigStruct = struct({ entity: "string?", theme: "string?", icon: "string?", + hold_action: struct.optional(actionConfigStruct), + double_tap_action: struct.optional(actionConfigStruct), }); @customElement("hui-light-card-editor") @@ -45,7 +52,7 @@ export class HuiLightCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "default"; + return this._config!.theme || ""; } get _entity(): string { @@ -56,11 +63,28 @@ export class HuiLightCardEditor extends LitElement return this._config!.icon || ""; } + get _hold_action(): ActionConfig { + return this._config!.hold_action || { action: "none" }; + } + + get _double_tap_action(): ActionConfig { + return this._config!.double_tap_action || { action: "none" }; + } + protected render(): TemplateResult { if (!this.hass) { return html``; } + const actions = [ + "more-info", + "toggle", + "navigate", + "url", + "call-service", + "none", + ]; + return html` ${configElementStyle}
@@ -71,10 +95,10 @@ export class HuiLightCardEditor extends LitElement "ui.panel.lovelace.editor.card.config.required" )})" .hass=${this.hass} - .value="${this._entity}" + .value=${this._entity} .configValue=${"entity"} include-domains='["light"]' - @change="${this._valueChanged}" + @change=${this._valueChanged} allow-custom-entity >
@@ -84,9 +108,9 @@ export class HuiLightCardEditor extends LitElement )} (${this.hass.localize( "ui.panel.lovelace.editor.card.config.optional" )})" - .value="${this._name}" - .configValue="${"name"}" - @value-changed="${this._valueChanged}" + .value=${this._name} + .configValue=${"name"} + @value-changed=${this._valueChanged} >
+ + + +
`; } @@ -118,7 +168,10 @@ export class HuiLightCardEditor extends LitElement } const target = ev.target! as EditorTarget; - if (this[`_${target.configValue}`] === target.value) { + if ( + this[`_${target.configValue}`] === target.value || + this[`_${target.configValue}`] === target.config + ) { return; } if (target.configValue) { @@ -127,7 +180,7 @@ export class HuiLightCardEditor extends LitElement } else { this._config = { ...this._config, - [target.configValue!]: target.value, + [target.configValue!]: target.value ? target.value : target.config, }; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 17735537a1..58a880eebf 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -34,6 +34,7 @@ const cardConfigStruct = struct({ default_zoom: "number?", dark_mode: "boolean?", entities: [entitiesConfigStruct], + hours_to_show: "number?", geo_location_sources: "array?", }); @@ -48,7 +49,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { public setConfig(config: MapCardConfig): void { config = cardConfigStruct(config); this._config = config; - this._configEntities = processEditorEntities(config.entities); + this._configEntities = config.entities + ? processEditorEntities(config.entities) + : []; } get _title(): string { @@ -67,6 +70,10 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { return this._config!.geo_location_sources || []; } + get _hours_to_show(): number { + return this._config!.hours_to_show || 0; + } + get _dark_mode(): boolean { return this._config!.dark_mode || false; } @@ -112,14 +119,27 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { @value-changed="${this._valueChanged}" >
- ${this.hass.localize( - "ui.panel.lovelace.editor.card.map.dark_mode" - )} +
+ ${this.hass.localize( + "ui.panel.lovelace.editor.card.map.dark_mode" + )} + +
+ + ${this.hass!.localize( + !this._cardEditorEl || this._GUImode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + ` : html` @@ -162,6 +180,15 @@ export class HuiStackCardEditor extends LitElement fireEvent(this, "config-changed", { config: this._config }); } + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._GUImode = ev.detail.guiMode; + } + + private _toggleMode(): void { + this._cardEditorEl?.toggleMode(); + } + static get styles(): CSSResult { return css` .toolbar { @@ -194,6 +221,10 @@ export class HuiStackCardEditor extends LitElement margin: 0 -12px; } } + + .gui-mode-button { + margin-right: auto; + } `; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index 862613d987..8372fda9df 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -46,7 +46,7 @@ export class HuiThermostatCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "default"; + return this._config!.theme || ""; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index d4ae31d7ea..408091c741 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -45,7 +45,7 @@ export class HuiWeatherForecastCardEditor extends LitElement } get _theme(): string { - return this._config!.theme || "Backend-selected"; + return this._config!.theme || ""; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/editor/hui-badge-preview.ts b/src/panels/lovelace/editor/hui-badge-preview.ts new file mode 100644 index 0000000000..b8d0dc728d --- /dev/null +++ b/src/panels/lovelace/editor/hui-badge-preview.ts @@ -0,0 +1,95 @@ +import { HomeAssistant } from "../../../types"; +import { LovelaceBadgeConfig } from "../../../data/lovelace"; +import { ConfigError } from "./types"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import { LovelaceBadge } from "../types"; +import { createBadgeElement } from "../create-element/create-badge-element"; +import { createErrorBadgeConfig } from "../badges/hui-error-badge"; + +import "../../../components/entity/ha-state-label-badge"; + +export class HuiBadgePreview extends HTMLElement { + private _hass?: HomeAssistant; + private _element?: LovelaceBadge; + private _config?: LovelaceBadgeConfig; + + private get _error() { + return this._element?.tagName === "HUI-ERROR-CARD"; + } + + constructor() { + super(); + this.addEventListener("ll-rebuild", () => { + this._cleanup(); + if (this._config) { + this.config = this._config; + } + }); + } + + set hass(hass: HomeAssistant) { + if (!this._hass || this._hass.language !== hass.language) { + this.style.direction = computeRTL(hass) ? "rtl" : "ltr"; + } + + this._hass = hass; + if (this._element) { + this._element.hass = hass; + } + } + + set error(error: ConfigError) { + this._createBadge( + createErrorBadgeConfig(`${error.type}: ${error.message}`) + ); + } + + set config(configValue: LovelaceBadgeConfig) { + const curConfig = this._config; + this._config = configValue; + + if (!configValue) { + this._cleanup(); + return; + } + + if (!this._element) { + this._createBadge(configValue); + return; + } + + // in case the element was an error element we always want to recreate it + if (!this._error && curConfig && configValue.type === curConfig.type) { + this._element.setConfig(configValue); + } else { + this._createBadge(configValue); + } + } + + private _createBadge(configValue: LovelaceBadgeConfig): void { + this._cleanup(); + this._element = createBadgeElement(configValue); + + if (this._hass) { + this._element!.hass = this._hass; + } + + this.appendChild(this._element!); + } + + private _cleanup() { + if (!this._element) { + return; + } + this.removeChild(this._element); + this._element = undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge-preview": HuiBadgePreview; + } +} + +customElements.define("hui-badge-preview", HuiBadgePreview); diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 3936d22337..b48deb1bff 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -14,6 +14,10 @@ export interface YamlChangedEvent extends Event { }; } +export interface GUIModeChangedEvent { + guiMode: boolean; +} + export interface ViewEditEvent extends Event { detail: { config: LovelaceViewConfig; diff --git a/src/panels/lovelace/editor/view-editor/hui-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts index 50f54e8d18..e2cf4064fc 100644 --- a/src/panels/lovelace/editor/view-editor/hui-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts @@ -23,6 +23,7 @@ import { haStyleDialog } from "../../../../resources/styles"; import "../../components/hui-entity-editor"; import "./hui-view-editor"; import "./hui-view-visibility-editor"; +import "../hui-badge-preview"; import { HomeAssistant } from "../../../../types"; import { LovelaceViewConfig, @@ -123,6 +124,20 @@ export class HuiEditView extends LitElement { break; case "tab-badges": content = html` + ${this._badges?.length + ? html` +
+ ${this._badges.map((badgeConfig) => { + return html` + + `; + })} +
+ ` + : ""} - `, + .preview-badges { + display: flex; + justify-content: center; + margin: 8px 16px; + flex-wrap: wrap; + } + `, ]; } } diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts index 755b8b706a..792cdfef7c 100644 --- a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.ts @@ -12,6 +12,7 @@ import "@polymer/paper-icon-button/paper-icon-button"; import "../components/hui-generic-entity-row"; import "../components/hui-warning"; +import "../../../components/ha-slider"; import { LovelaceRow, EntityConfig } from "./types"; import { HomeAssistant } from "../../../types"; @@ -31,6 +32,7 @@ import { import { hasConfigOrEntityChanged } from "../common/has-changed"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { debounce } from "../../../common/util/debounce"; +import { UNAVAILABLE, UNKNOWN } from "../../../data/entity"; @customElement("hui-media-player-entity-row") class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { @@ -41,8 +43,8 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { private _resizeObserver?: ResizeObserver; private _debouncedResizeListener = debounce( () => { - this._narrow = (this.parentElement?.clientWidth || 0) < 350; - this._veryNarrow = (this.parentElement?.clientWidth || 0) < 300; + this._narrow = (this.clientWidth || 0) < 300; + this._veryNarrow = (this.clientWidth || 0) < 225; }, 250, false @@ -82,6 +84,34 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { const stateObj = this.hass.states[this._config.entity]; + const buttons = html` + ${!this._narrow && supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) + ? html` + + ` + : ""} + ${stateObj.state !== "playing" && + !supportsFeature(stateObj, SUPPORTS_PLAY) + ? "" + : html` + + `} + ${supportsFeature(stateObj, SUPPORT_NEXT_TRACK) + ? html` + + ` + : ""} + `; + if (!stateObj) { return html` - ${stateObj.state === "off" || stateObj.state === "idle" - ? supportsFeature(stateObj, SUPPORT_TURN_ON) - : supportsFeature(stateObj, SUPPORT_TURN_OFF) - ? html` - - ` - : ""} - -
-
- ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE) - ? html` - - ` - : ""} - ${!this._narrow && supportsFeature(stateObj, SUPPORT_VOLUME_SET) - ? html` - - ` - : !this._veryNarrow && - supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS) - ? html` - - - ` - : ""} -
- ${!this._veryNarrow && - supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) + ${supportsFeature(stateObj, SUPPORT_TURN_ON) && + stateObj.state === "off" ? html` ` - : ""} - ${stateObj.state !== "playing" && - !supportsFeature(stateObj, SUPPORTS_PLAY) - ? "" - : html` - - `} - ${supportsFeature(stateObj, SUPPORT_NEXT_TRACK) + : !supportsFeature(stateObj, SUPPORT_VOLUME_SET) && + !supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS) + ? buttons + : supportsFeature(stateObj, SUPPORT_TURN_OFF) && + stateObj.state !== "off" ? html` ` : ""}
-
- `; - } + + ${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) || + supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) && + ![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) + ? html` +
+
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE) + ? html` + + ` + : ""} + ${!this._veryNarrow && + supportsFeature(stateObj, SUPPORT_VOLUME_SET) + ? html` + + ` + : !this._veryNarrow && + supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS) + ? html` + + + ` + : ""} +
- static get styles(): CSSResult { - return css` - :host { - display: block; - } - .flex { - display: flex; - align-items: center; - padding-left: 48px; - justify-content: space-between; - } - .volume { - display: flex; - flex-grow: 2; - flex-shrink: 2; - } - .controls { - white-space: nowrap; - } - ha-slider { - flex-grow: 2; - flex-shrink: 2; - width: 100%; - } +
+ ${buttons} +
+
+ ` + : ""} `; } @@ -316,6 +314,33 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow { volume_level: ev.target.value / 100, }); } + + static get styles(): CSSResult { + return css` + :host { + display: block; + } + .flex { + display: flex; + align-items: center; + justify-content: space-between; + } + .volume { + display: flex; + flex-grow: 2; + flex-shrink: 2; + } + .controls { + white-space: nowrap; + } + ha-slider { + flex-grow: 2; + flex-shrink: 2; + width: 100%; + margin: 0 -8px 0 1px; + } + `; + } } declare global { diff --git a/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts new file mode 100644 index 0000000000..0bdb3786af --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-weather-entity-row.ts @@ -0,0 +1,182 @@ +import { + html, + LitElement, + TemplateResult, + css, + CSSResult, + property, + customElement, + PropertyValues, +} from "lit-element"; + +import "../../../components/entity/state-badge"; +import "../components/hui-warning"; + +import { LovelaceRow } from "./types"; +import { HomeAssistant, WeatherEntity } from "../../../types"; +import { EntitiesCardEntityConfig } from "../cards/types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { + weatherIcons, + getWeatherUnit, + weatherImages, +} from "../../../data/weather"; + +@customElement("hui-weather-entity-row") +class HuiWeatherEntityRow extends LitElement implements LovelaceRow { + @property() public hass?: HomeAssistant; + @property() private _config?: EntitiesCardEntityConfig; + + public setConfig(config: EntitiesCardEntityConfig): void { + if (!config?.entity) { + throw new Error("Invalid Configuration: 'entity' required"); + } + + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity] as WeatherEntity; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + const weatherRowConfig = { + ...this._config, + icon: weatherIcons[stateObj.state], + image: weatherImages[stateObj.state], + }; + + return html` + +
+
+ ${stateObj.attributes.temperature} + ${getWeatherUnit(this.hass, "temperature")} +
+
+ ${this._getSecondaryAttribute(stateObj)} +
+
+
+ `; + } + + private _getSecondaryAttribute(stateObj: WeatherEntity): string | undefined { + const extrema = this._getExtrema(stateObj); + + if (extrema) { + return extrema; + } + + let value: number; + let attribute: string; + + if ( + stateObj.attributes.forecast?.length && + stateObj.attributes.forecast[0].precipitation !== undefined && + stateObj.attributes.forecast[0].precipitation !== null + ) { + value = stateObj.attributes.forecast[0].precipitation!; + attribute = "precipitation"; + } else if ("humidity" in stateObj.attributes) { + value = stateObj.attributes.humidity!; + attribute = "humidity"; + } else { + return undefined; + } + + return ` + ${this.hass!.localize( + `ui.card.weather.attributes.${attribute}` + )} ${value} ${getWeatherUnit(this.hass!, attribute)} + `; + } + + private _getExtrema(stateObj: WeatherEntity): string | undefined { + if (!stateObj.attributes.forecast?.length) { + return undefined; + } + + let tempLow: number | undefined; + let tempHigh: number | undefined; + const today = new Date().getDate(); + + for (const forecast of stateObj.attributes.forecast!) { + if (new Date(forecast.datetime).getDate() !== today) { + break; + } + if (!tempHigh || forecast.temperature > tempHigh) { + tempHigh = forecast.temperature; + } + if (!tempLow || (forecast.templow && forecast.templow < tempLow)) { + tempLow = forecast.templow; + } + if (!forecast.templow && (!tempLow || forecast.temperature < tempLow)) { + tempLow = forecast.temperature; + } + } + + if (!tempLow && !tempHigh) { + return undefined; + } + + const unit = getWeatherUnit(this.hass!, "temperature"); + + return ` + ${ + tempHigh + ? ` + ${this.hass!.localize(`ui.card.weather.high`)} ${tempHigh} ${unit} + ` + : "" + } + ${tempLow && tempHigh ? " / " : ""} + ${ + tempLow + ? ` + ${this.hass!.localize(`ui.card.weather.low`)} ${tempLow} ${unit} + ` + : "" + } + `; + } + + static get styles(): CSSResult { + return css` + .attributes { + display: flex; + flex-direction: column; + justify-content: center; + text-align: right; + } + + .secondary { + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-weather-entity-row": HuiWeatherEntityRow; + } +} diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts index f620fb4d49..618db52747 100644 --- a/src/panels/lovelace/entity-rows/types.ts +++ b/src/panels/lovelace/entity-rows/types.ts @@ -1,5 +1,6 @@ import { HomeAssistant } from "../../../types"; import { Condition } from "../common/validate-condition"; +import { ActionConfig } from "../../../data/lovelace"; export interface EntityConfig { entity: string; @@ -30,9 +31,16 @@ export interface WeblinkConfig { } export interface CallServiceConfig extends EntityConfig { type: "call-service"; - action_name?: string; service: string; service_data?: { [key: string]: any }; + action_name?: string; +} +export interface ButtonRowConfig extends EntityConfig { + type: "button"; + action_name?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; } export interface CastConfig { type: "cast"; @@ -54,8 +62,10 @@ export type LovelaceRowConfig = | WeblinkConfig | CallServiceConfig | CastConfig + | ButtonRowConfig | ButtonsRowConfig - | ConditionalRowConfig; + | ConditionalRowConfig + | AttributeRowConfig; export interface LovelaceRow extends HTMLElement { hass?: HomeAssistant; @@ -66,3 +76,8 @@ export interface ConditionalRowConfig extends EntityConfig { row: EntityConfig; conditions: Condition[]; } +export interface AttributeRowConfig extends EntityConfig { + attribute: string; + prefix?: string; + suffix?: string; +} diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts new file mode 100644 index 0000000000..289c1f5a64 --- /dev/null +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -0,0 +1,83 @@ +import { + html, + LitElement, + TemplateResult, + property, + CSSResult, + css, + customElement, + PropertyValues, +} from "lit-element"; + +import "../components/hui-generic-entity-row"; +import "../components/hui-warning"; + +import { HomeAssistant } from "../../../types"; +import { LovelaceRow, AttributeRowConfig } from "../entity-rows/types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; + +@customElement("hui-attribute-row") +class HuiAttributeRow extends LitElement implements LovelaceRow { + @property() public hass?: HomeAssistant; + @property() private _config?: AttributeRowConfig; + + public setConfig(config: AttributeRowConfig): void { + if (!config) { + throw new Error("Configuration error"); + } + if (!config.entity) { + throw new Error("Entity not defined"); + } + if (!config.attribute) { + throw new Error("Attribute not defined"); + } + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + const attribute = stateObj.attributes[this._config.attribute]; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + return html` + +
+ ${this._config.prefix} ${attribute || "-"} ${this._config.suffix} +
+
+ `; + } + + static get styles(): CSSResult { + return css` + div { + text-align: right; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-attribute-row": HuiAttributeRow; + } +} diff --git a/src/panels/lovelace/special-rows/hui-button-row.ts b/src/panels/lovelace/special-rows/hui-button-row.ts new file mode 100644 index 0000000000..58fa793b62 --- /dev/null +++ b/src/panels/lovelace/special-rows/hui-button-row.ts @@ -0,0 +1,103 @@ +import { + html, + LitElement, + TemplateResult, + customElement, + property, + css, + CSSResult, +} from "lit-element"; +import "@material/mwc-button"; + +import "../../../components/ha-icon"; + +import { LovelaceRow, ButtonRowConfig } from "../entity-rows/types"; +import { HomeAssistant } from "../../../types"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { hasAction } from "../common/has-action"; +import { ActionHandlerEvent } from "../../../data/lovelace"; +import { handleAction } from "../common/handle-action"; + +@customElement("hui-button-row") +export class HuiButtonRow extends LitElement implements LovelaceRow { + public hass?: HomeAssistant; + @property() private _config?: ButtonRowConfig; + + public setConfig(config: ButtonRowConfig): void { + if (!config) { + throw new Error("Error in card configuration."); + } + + if (!config.name) { + throw new Error("Error in card configuration. No name specified."); + } + + if (!config.tap_action) { + throw new Error("Error in card configuration. No action specified."); + } + + this._config = config; + } + + protected render(): TemplateResult { + if (!this._config) { + return html``; + } + + return html` + +
+
${this._config.name}
+ ${this._config.action_name + ? this._config.action_name + : this.hass!.localize("ui.card.service.run")} +
+ `; + } + + static get styles(): CSSResult { + return css` + :host { + display: flex; + align-items: center; + } + ha-icon { + padding: 8px; + color: var(--paper-item-icon-color); + } + .flex { + flex: 1; + overflow: hidden; + margin-left: 16px; + display: flex; + justify-content: space-between; + align-items: center; + } + .flex div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + mwc-button { + margin-right: -0.57em; + } + `; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-button-row": HuiButtonRow; + } +} diff --git a/src/panels/lovelace/special-rows/hui-call-service-row.ts b/src/panels/lovelace/special-rows/hui-call-service-row.ts index a1e20b5fb2..7f9fab6051 100644 --- a/src/panels/lovelace/special-rows/hui-call-service-row.ts +++ b/src/panels/lovelace/special-rows/hui-call-service-row.ts @@ -1,83 +1,34 @@ -import { - html, - LitElement, - TemplateResult, - customElement, - property, - css, - CSSResult, -} from "lit-element"; -import "@material/mwc-button"; +import { customElement } from "lit-element"; -import "../../../components/ha-icon"; - -import { callService } from "../common/call-service"; -import { LovelaceRow, CallServiceConfig } from "../entity-rows/types"; -import { HomeAssistant } from "../../../types"; +import { CallServiceConfig } from "../entity-rows/types"; +import { HuiButtonRow } from "./hui-button-row"; @customElement("hui-call-service-row") -class HuiCallServiceRow extends LitElement implements LovelaceRow { - public hass?: HomeAssistant; +export class HuiCallServiceRow extends HuiButtonRow { + public setConfig(config: any): void { + const callServiceConfig: CallServiceConfig = config; - @property() private _config?: CallServiceConfig; - - public setConfig(config: CallServiceConfig): void { - if (!config || !config.name || !config.service) { + if (!callServiceConfig) { throw new Error("Error in card configuration."); } - this._config = { icon: "hass:remote", ...config }; - } - - protected render(): TemplateResult { - if (!this._config) { - return html``; + if (!callServiceConfig.name) { + throw new Error("Error in card configuration. No name specified."); } - return html` - -
-
${this._config.name}
- ${this._config.action_name - ? this._config.action_name - : this.hass!.localize("ui.card.service.run")} -
- `; - } + if (!callServiceConfig.service) { + throw new Error("Error in card configuration. No service specified."); + } - static get styles(): CSSResult { - return css` - :host { - display: flex; - align-items: center; - } - ha-icon { - padding: 8px; - color: var(--paper-item-icon-color); - } - .flex { - flex: 1; - overflow: hidden; - margin-left: 16px; - display: flex; - justify-content: space-between; - align-items: center; - } - .flex div { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - mwc-button { - margin-right: -0.57em; - } - `; - } - - private _callService() { - callService(this._config!, this.hass!); + super.setConfig({ + tap_action: { + action: "call-service", + service: callServiceConfig.service, + service_data: callServiceConfig.service_data, + }, + ...callServiceConfig, + type: "button", + }); } } diff --git a/src/panels/lovelace/special-rows/hui-conditional-row.ts b/src/panels/lovelace/special-rows/hui-conditional-row.ts index e735f13870..e7caca11e3 100644 --- a/src/panels/lovelace/special-rows/hui-conditional-row.ts +++ b/src/panels/lovelace/special-rows/hui-conditional-row.ts @@ -13,7 +13,12 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow { throw new Error("No row configured."); } + if (this._element && this._element.parentElement) { + this.removeChild(this._element); + } + this._element = createRowElement(config.row) as LovelaceRow; + this.appendChild(this._element); } } diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index e0d7c087de..9ecbeb3163 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -117,7 +117,11 @@ export class HUIView extends LitElement { padding: 4px 4px 0; transform: translateZ(0); position: relative; - background: var(--lovelace-background); + color: var(--primary-text-color); + background: var( + --lovelace-background, + var(--primary-background-color) + ); } #badges { diff --git a/src/panels/map/ha-entity-marker.js b/src/panels/map/ha-entity-marker.js index fde1bc0997..d910b7b3a4 100644 --- a/src/panels/map/ha-entity-marker.js +++ b/src/panels/map/ha-entity-marker.js @@ -33,7 +33,7 @@ class HaEntityMarker extends EventsMixin(PolymerElement) { } -
+