Merge branch 'dev' into grid-card-size-algorithm

This commit is contained in:
karwosts 2024-03-24 05:40:35 -07:00 committed by GitHub
commit ecc9ddb1cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
267 changed files with 9756 additions and 4020 deletions

View File

@ -1,5 +1,5 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
FROM mcr.microsoft.com/devcontainers/python:3.12
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
with:
ref: dev
@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
with:
ref: master

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.0.0
uses: actions/cache@v4.0.1
with:
path: |
node_modules/.cache/prettier
@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:
@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
with:

View File

@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
with:
ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
with:
ref: master

View File

@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Setup Node
uses: actions/setup-node@v4.0.2

View File

@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.11"
PYTHON_VERSION: "3.12"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5

View File

@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.11"
PYTHON_VERSION: "3.12"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.15
uses: softprops/action-gh-release@v2.0.4
with:
files: |
dist/*.whl

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.2
- name: Upload Translations
run: |

View File

@ -1,13 +0,0 @@
diff --git a/simple-tooltip.js b/simple-tooltip.js
index 78a87f6a223925f0e29fbedb268c85a142ec6985..3d686dd6a3d5a93342b4b01408089fc316b408ca 100644
--- a/simple-tooltip.js
+++ b/simple-tooltip.js
@@ -195,6 +195,8 @@ class SimpleTooltip extends LitElement {
.hidden {
position: absolute;
left: -10000px;
+ inset-inline-start: -10000px;
+ inset-inline-end: initial;
top: auto;
width: 1px;
height: 1px;

View File

@ -0,0 +1,18 @@
diff --git a/dist/hls.light.mjs b/dist/hls.light.mjs
index eed9d788fafdb159975e1a2eb08ac88ba9c9ac33..ace881935e6665946f1c8110ebd2f739cde4427e 100644
--- a/dist/hls.light.mjs
+++ b/dist/hls.light.mjs
@@ -20523,9 +20523,9 @@ class Hls {
}
Hls.defaultConfig = void 0;
-var KeySystemFormats = empty.KeySystemFormats;
-var KeySystems = empty.KeySystems;
-var SubtitleStreamController = empty.SubtitleStreamController;
-var TimelineController = empty.TimelineController;
+var KeySystemFormats = empty;
+var KeySystems = empty;
+var SubtitleStreamController = empty;
+var TimelineController = empty;
export { AbrController, AttrList, Cues as AudioStreamController, Cues as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, Cues as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, Cues as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, Cues as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
//# sourceMappingURL=hls.light.mjs.map

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.0.cjs
yarnPath: .yarn/releases/yarn-4.1.1.cjs

View File

@ -28,7 +28,7 @@ class HcLaunchScreen extends LitElement {
:host {
display: block;
height: 100vh;
background-color: white;
background-color: #f2f4f9;
font-size: 24px;
}
.container {
@ -43,6 +43,9 @@ class HcLaunchScreen extends LitElement {
max-width: 80%;
object-fit: cover;
}
.status {
color: #1d2126;
}
`;
}
}

View File

@ -270,7 +270,7 @@ export class HcMain extends HassElement {
}
this._error = undefined;
if (msg.urlPath === "lovelace") {
if (msg.urlPath === "lovelace" || msg.urlPath === undefined) {
msg.urlPath = null;
}
this._lovelacePath = msg.viewPath;

View File

@ -4,6 +4,7 @@ import { energyEntities } from "../stubs/entities";
import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [
() => import("./sections").then((mod) => mod.demoSections),
() => import("./arsaboo").then((mod) => mod.demoArsaboo),
() => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
() => import("./kernehed").then((mod) => mod.demoKernehed),

View File

@ -0,0 +1,16 @@
import { html } from "lit";
import { DemoConfig } from "../types";
export const demoLovelaceDescription: DemoConfig["description"] = (
localize
) => html`
<p>
${localize("ui.panel.page-demo.config.sections.description", {
blog_post: html`<a
href="https://www.home-assistant.io/blog/2024/03/04/dashboard-chapter-1/"
target="_blank"
>${localize("ui.panel.page-demo.config.sections.description_blog_post")}
</a>`,
})}
</p>
`;

View File

@ -0,0 +1,474 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = () =>
convertEntities({
"cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Living room garden shutter",
supported_features: 15,
},
},
"cover.living_room_graveyard_shutter": {
entity_id: "cover.living_room_graveyard_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Living room graveyard shutter",
supported_features: 15,
},
},
"cover.living_room_left_shutter": {
entity_id: "cover.living_room_left_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Living room left shutter",
supported_features: 15,
},
},
"cover.living_room_right_shutter": {
entity_id: "cover.living_room_right_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Living room right shutter",
supported_features: 15,
},
},
"light.floor_lamp": {
entity_id: "light.floor_lamp",
state: "on",
attributes: {
min_color_temp_kelvin: 2000,
max_color_temp_kelvin: 6535,
min_mireds: 153,
max_mireds: 500,
supported_color_modes: ["color_temp", "xy"],
color_mode: "color_temp",
brightness: 178,
color_temp_kelvin: 2583,
color_temp: 387,
hs_color: [28.664, 69.597],
rgb_color: [255, 162, 77],
xy_color: [0.538, 0.389],
icon: "mdi:floor-lamp",
friendly_name: "Floor lamp",
supported_features: 44,
},
},
"light.living_room_spotlights": {
entity_id: "light.living_room_spotlights",
state: "on",
attributes: {
supported_color_modes: ["brightness"],
color_mode: "brightness",
brightness: 126,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Living room spotlights",
supported_features: 32,
},
},
"light.bar_lamp": {
entity_id: "light.bar_lamp",
state: "on",
attributes: {
min_color_temp_kelvin: 2202,
max_color_temp_kelvin: 4504,
min_mireds: 222,
max_mireds: 454,
effect_list: ["None", "candle"],
supported_color_modes: ["color_temp"],
effect: null,
color_mode: null,
brightness: null,
color_temp_kelvin: null,
color_temp: null,
hs_color: null,
rgb_color: null,
xy_color: null,
mode: "normal",
dynamics: "none",
icon: "mdi:lightbulb-variant",
friendly_name: "Bar lamp",
supported_features: 44,
},
},
"sensor.living_room_temperature": {
entity_id: "sensor.living_room_temperature",
state: "22.8",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Living room Temperature",
},
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "off",
attributes: {
device_class: "speaker",
friendly_name: "Living room Nest Mini",
supported_features: 152461,
},
},
"cover.kitchen_shutter": {
entity_id: "cover.kitchen_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Kitchen shutter ",
supported_features: 15,
},
},
"light.kitchen_spotlights": {
entity_id: "light.kitchen_spotlights",
state: "off",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Kitchen spotlights ",
supported_features: 32,
},
},
"light.worktop_spotlights": {
entity_id: "light.worktop_spotlights",
state: "off",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Worktop spotlights ",
supported_features: 32,
},
},
"binary_sensor.fridge_door": {
entity_id: "binary_sensor.fridge_door",
state: "off",
attributes: {
device_class: "door",
icon: "mdi:fridge",
friendly_name: "Fridge door",
},
},
"media_player.kitchen_nest_audio": {
entity_id: "media_player.kitchen_nest_audio",
state: "on",
attributes: {
device_class: "speaker",
friendly_name: "Kitchen Nest Audio",
supported_features: 152461,
},
},
"binary_sensor.tesla_wall_connector_vehicle_connected": {
entity_id: "binary_sensor.tesla_wall_connector_vehicle_connected",
state: "off",
attributes: {
device_class: "plug",
friendly_name: "Wall Connector Vehicle connected",
},
},
"sensor.tesla_wall_connector_session_energy": {
entity_id: "sensor.tesla_wall_connector_session_energy",
state: "16.3",
attributes: {
state_class: "total_increasing",
unit_of_measurement: "kWh",
device_class: "energy",
friendly_name: "Tesla Wall Connector Session energy",
},
},
"sensor.electric_meter_power": {
entity_id: "sensor.electric_meter_power",
state: "797.86",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
device_class: "power",
icon: "mdi:meter-electric",
friendly_name: "Electric meter Power",
},
},
"sensor.eletric_meter_voltage": {
entity_id: "sensor.eletric_meter_voltage",
state: "232.19",
attributes: {
state_class: "measurement",
unit_of_measurement: "V",
device_class: "voltage",
friendly_name: "Electric meter voltage",
},
},
"sensor.electricity_maps_grid_fossil_fuel_percentage": {
entity_id: "sensor.electricity_maps_grid_fossil_fuel_percentage",
state: "9.84",
attributes: {
state_class: "measurement",
country_code: "FR",
unit_of_measurement: "%",
attribution: "Data provided by Electricity Maps",
icon: "mdi:barrel",
friendly_name: "Electricity Maps Grid fossil fuel percentage",
},
},
"sensor.electricity_maps_co2_intensity": {
entity_id: "sensor.electricity_maps_co2_intensity",
state: "62.0",
attributes: {
state_class: "measurement",
country_code: "FR",
unit_of_measurement: "gCO2eq/kWh",
attribution: "Data provided by Electricity Maps",
friendly_name: "Electricity Maps CO2 intensity",
icon: "mdi:molecule-co2",
},
},
"sun.sun": {
entity_id: "sun.sun",
state: "above_horizon",
attributes: {
next_dawn: "2024-03-05T05:50:21.964405+00:00",
next_dusk: "2024-03-04T18:08:54.311334+00:00",
next_midnight: "2024-03-05T00:00:00+00:00",
next_noon: "2024-03-05T12:00:05+00:00",
next_rising: "2024-03-05T06:23:42.739159+00:00",
next_setting: "2024-03-04T17:35:26.271171+00:00",
elevation: 30.38,
azimuth: 204.42,
rising: false,
friendly_name: "Sun",
},
},
"sensor.rain": {
entity_id: "sensor.moon_phase",
state: "7.2",
attributes: {
state_class: "total_increasing",
unit_of_measurement: "mm",
device_class: "precipitation",
friendly_name: "Rain",
},
},
"climate.ground_floor": {
entity_id: "climate.ground_floor",
state: "heat",
attributes: {
hvac_modes: ["auto", "heat", "off"],
min_temp: 7,
max_temp: 35,
preset_modes: [
"comfort",
"away",
"eco",
"frost_protection",
"external",
"home",
],
current_temperature: 20.8,
temperature: 21,
preset_mode: "comfort",
icon: "mdi:home-floor-0",
friendly_name: "Ground floor Thermostat",
supported_features: 401,
},
},
"climate.first_floor": {
entity_id: "climate.first_floor",
state: "heat",
attributes: {
hvac_modes: ["auto", "heat", "off"],
min_temp: 7,
max_temp: 35,
preset_modes: [
"comfort",
"away",
"eco",
"frost_protection",
"external",
"home",
],
current_temperature: 21.7,
temperature: 21,
preset_mode: "comfort",
icon: "mdi:home-floor-1",
friendly_name: "First floor Thermostat",
supported_features: 401,
},
},
"cover.study_shutter": {
entity_id: "cover.study_shutter",
state: "open",
attributes: {
current_position: 100,
device_class: "shutter",
friendly_name: "Study shutter",
supported_features: 15,
},
},
"light.study_spotlights": {
entity_id: "light.study_spotlights",
state: "off",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: null,
icon: "mdi:ceiling-light-multiple",
friendly_name: "Study spotlights",
supported_features: 32,
},
},
"media_player.study_nest_hub": {
entity_id: "media_player.study_nest_hub",
state: "off",
attributes: {
friendly_name: "Study Nest Hub",
supported_features: 152461,
},
},
"sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height",
state: "72",
attributes: {
unit_of_measurement: "cm",
icon: "mdi:tape-measure",
friendly_name: "Standing desk Height",
},
},
"light.outdoor_light": {
entity_id: "light.outdoor_light",
state: "on",
attributes: {
supported_color_modes: ["brightness"],
color_mode: null,
brightness: 255,
icon: "mdi:outdoor-lamp",
friendly_name: "Outdoor light",
supported_features: 32,
},
},
"light.flood_light": {
entity_id: "light.flood_light",
state: "off",
attributes: {
effect_list: ["None", "candle"],
supported_color_modes: ["brightness"],
effect: null,
color_mode: null,
brightness: null,
mode: "normal",
dynamics: "none",
icon: "mdi:light-flood-down",
friendly_name: "Flood light",
supported_features: 44,
},
},
"sensor.outdoor_motion_sensor_temperature": {
entity_id: "sensor.outdoor_motion_sensor_temperature",
state: "10.2",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor motion sensor Temperature",
},
},
"binary_sensor.outdoor_motion_sensor_motion": {
entity_id: "binary_sensor.outdoor_motion_sensor_motion",
state: "off",
attributes: {
device_class: "motion",
friendly_name: "Outdoor motion sensor Motion",
},
},
"sensor.outdoor_motion_sensor_illuminance": {
entity_id: "sensor.outdoor_motion_sensor_illuminance",
state: "555",
attributes: {
state_class: "measurement",
light_level: 27444,
unit_of_measurement: "lx",
device_class: "illuminance",
friendly_name: "Outdoor motion sensor Illuminance",
},
},
"automation.home_assistant_auto_update": {
entity_id: "automation.home_assistant_auto_update",
state: "off",
attributes: {
id: "1700669321947",
last_triggered: "2024-02-29T18:02:05.343139+00:00",
mode: "queued",
current: 0,
max: 50,
icon: "mdi:auto-mode",
friendly_name: "Home Assistant Auto-update",
},
},
"update.home_assistant_operating_system_update": {
entity_id: "update.home_assistant_operating_system_update",
state: "off",
attributes: {
auto_update: false,
installed_version: "12.1",
in_progress: false,
latest_version: "12.1",
release_summary: null,
release_url:
"https://github.com/home-assistant/operating-system/commits/dev",
skipped_version: null,
title: "Home Assistant Operating System",
entity_picture:
"https://brands.home-assistant.io/homeassistant/icon.png",
friendly_name: "Home Assistant Operating System Update",
supported_features: 3,
},
},
"update.home_assistant_supervisor_update": {
entity_id: "update.home_assistant_supervisor_update",
state: "off",
attributes: {
auto_update: true,
installed_version: "2024.02.2",
in_progress: false,
latest_version: "2024.02.2",
release_summary: null,
release_url:
"https://github.com/home-assistant/supervisor/commits/main",
skipped_version: null,
title: "Home Assistant Supervisor",
entity_picture: "https://brands.home-assistant.io/hassio/icon.png",
friendly_name: "Home Assistant Supervisor Update",
supported_features: 1,
},
},
"update.home_assistant_core_update": {
entity_id: "update.home_assistant_supervisor_update",
state: "off",
attributes: {
auto_update: false,
installed_version: "2024.4.0",
in_progress: false,
latest_version: "2024.4.0",
release_summary: null,
release_url: "https://github.com/home-assistant/core/commits/dev",
skipped_version: null,
title: "Home Assistant Core",
entity_picture:
"https://brands.home-assistant.io/homeassistant/icon.png",
friendly_name: "Home Assistant Core Update",
supported_features: 11,
},
},
});

View File

@ -0,0 +1,14 @@
import { DemoConfig } from "../types";
import { demoLovelaceDescription } from "./description";
import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace";
export const demoSections: DemoConfig = {
authorName: "Home Assistant",
authorUrl: "https://github.com/home-assistant/frontend/",
name: "Home Demo",
description: demoLovelaceDescription,
lovelace: demoLovelaceSections,
entities: demoEntitiesSections,
theme: () => ({}),
};

View File

@ -0,0 +1,281 @@
import { DemoConfig } from "../types";
export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
title: "Home Assistant Demo",
views: [
{
type: "sections",
title: "Demo",
path: "home",
icon: "mdi:home-assistant",
sections: [
{
title: "Welcome 👋",
cards: [{ type: "custom:ha-demo-card" }],
},
{
cards: [
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Garden",
},
{
type: "tile",
entity: "cover.living_room_graveyard_shutter",
name: "Rear",
},
{
type: "tile",
entity: "cover.living_room_left_shutter",
name: "Left",
},
{
type: "tile",
entity: "cover.living_room_right_shutter",
name: "Right",
},
{
type: "tile",
entity: "light.floor_lamp",
},
{
type: "tile",
entity: "light.living_room_spotlights",
name: "Spotlights",
features: [
{
type: "light-brightness",
},
],
},
{
type: "tile",
entity: "light.bar_lamp",
},
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "media_player.living_room_nest_mini",
name: "Nest Mini",
},
],
title: "🛋️ Living room ",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "cover.kitchen_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.kitchen_spotlights",
name: "Spotlights",
features: [
{
type: "light-brightness",
},
],
},
{
type: "tile",
entity: "light.worktop_spotlights",
name: "Worktop",
},
{
type: "tile",
entity: "binary_sensor.fridge_door",
name: "Fridge",
},
{
type: "tile",
entity: "media_player.kitchen_nest_audio",
name: "Nest Audio",
},
],
title: "👩‍🍳 Kitchen",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
name: "EV",
icon: "mdi:car",
},
{
type: "tile",
entity: "sensor.tesla_wall_connector_session_energy",
name: "Last charge",
color: "green",
},
{
type: "tile",
entity: "sensor.electric_meter_power",
color: "deep-orange",
name: "Home power",
},
{
type: "tile",
entity: "sensor.eletric_meter_voltage",
name: "Voltage",
color: "deep-orange",
},
{
type: "tile",
entity: "sensor.electricity_maps_grid_fossil_fuel_percentage",
name: "Fossil fuel",
color: "brown",
},
{
type: "tile",
entity: "sensor.electricity_maps_co2_intensity",
name: "CO2 Intensity",
color: "dark-grey",
},
],
title: "⚡️ Energy",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "sun.sun",
},
{
type: "tile",
entity: "sensor.rain",
color: "blue",
},
{
features: [
{
type: "target-temperature",
},
],
type: "tile",
name: "Downstairs",
entity: "climate.ground_floor",
state_content: ["preset_mode", "current_temperature"],
},
{
features: [
{
type: "target-temperature",
},
],
type: "tile",
name: "Upstairs",
entity: "climate.first_floor",
state_content: ["preset_mode", "current_temperature"],
},
],
title: "🌤️ Climate",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "cover.study_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.study_spotlights",
name: "Spotlights",
},
{
type: "tile",
entity: "media_player.study_nest_hub",
name: "Nest Hub",
},
{
type: "tile",
entity: "sensor.standing_desk_height",
name: "Desk",
color: "brown",
icon: "mdi:desk",
},
],
title: "🧑‍💻 Study",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "light.outdoor_light",
name: "Door light",
},
{
type: "tile",
entity: "light.flood_light",
},
{
graph: "line",
type: "sensor",
entity: "sensor.outdoor_motion_sensor_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "binary_sensor.outdoor_motion_sensor_motion",
name: "Motion",
color: "blue",
},
{
type: "tile",
entity: "sensor.outdoor_motion_sensor_illuminance",
color: "amber",
name: "Illuminance",
},
],
title: "🌳 Outdoor",
},
{
type: "grid",
cards: [
{
type: "tile",
entity: "automation.home_assistant_auto_update",
name: "Auto-update",
color: "green",
},
{
type: "tile",
entity: "update.home_assistant_operating_system_update",
name: "OS",
icon: "mdi:home-assistant",
},
{
type: "tile",
entity: "update.home_assistant_supervisor_update",
icon: "mdi:home-assistant",
name: "Supervisor",
},
{
type: "tile",
entity: "update.home_assistant_core_update",
name: "Core",
icon: "mdi:home-assistant",
},
],
title: "🎉 Updates",
},
],
},
],
});

View File

@ -1,3 +1,4 @@
import { TemplateResult } from "lit";
import { LocalizeFunc } from "../../../src/common/translations/localize";
import { LovelaceConfig } from "../../../src/data/lovelace/config/types";
import { Entity } from "../../../src/fake_data/entity";
@ -7,6 +8,9 @@ export interface DemoConfig {
name: string;
authorName: string;
authorUrl: string;
description?:
| string
| ((localize: LocalizeFunc) => string | TemplateResult<1>);
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => Entity[];
theme: () => Record<string, string> | null;

View File

@ -39,32 +39,51 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<div class="picker">
<div class="label">
${this._switching
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>`
? html`
<ha-circular-progress indeterminate></ha-circular-progress>
`
: until(
selectedDemoConfig.then(
(conf) => html`
${conf.name}
<small>
<a target="_blank" href=${conf.authorUrl}>
${this.hass.localize(
"ui.panel.page-demo.cards.demo.demo_by",
{ name: conf.authorName }
)}
</a>
${this.hass.localize(
"ui.panel.page-demo.cards.demo.demo_by",
{
name: html`
<a target="_blank" href=${conf.authorUrl}>
${conf.authorName}
</a>
`,
}
)}
</small>
`
),
""
)}
</div>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</mwc-button>
</div>
<div class="content small-hidden">
${this.hass.localize("ui.panel.page-demo.cards.demo.introduction")}
<div class="content">
<p class="small-hidden">
${this.hass.localize("ui.panel.page-demo.cards.demo.introduction")}
</p>
${until(
selectedDemoConfig.then((conf) => {
if (typeof conf.description === "function") {
return conf.description(this.hass.localize);
}
if (conf.description) {
return html`<p>${conf.description}</p>`;
}
return nothing;
}),
nothing
)}
</div>
<div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank">
@ -108,6 +127,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
css`
a {
color: var(--primary-color);
display: inline-block;
}
.actions a {
@ -115,7 +135,11 @@ export class HADemoCard extends LitElement implements LovelaceCard {
}
.content {
padding: 16px;
padding: 0 16px;
}
.content p {
margin: 16px 0;
}
.picker {
@ -138,9 +162,8 @@ export class HADemoCard extends LitElement implements LovelaceCard {
}
.actions {
padding-left: 8px;
padding: 0px 8px 4px 8px;
}
@media only screen and (max-width: 500px) {
.small-hidden {
display: none;

View File

@ -10,6 +10,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
supports_options: false,
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,

View File

@ -17,6 +17,7 @@ export const basicTrace: DemoTrace = {
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
changed_variables: {},
},
],
"condition/0": [

View File

@ -17,6 +17,7 @@ export const motionLightTrace: DemoTrace = {
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
changed_variables: {},
},
],
"action/0": [

View File

@ -21,10 +21,10 @@ const ENTITIES = [
}),
];
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
const conditions: Condition[] = [
{ condition: "and", conditions: [] },
{ condition: "not", conditions: [] },
{ condition: "or", conditions: [] },
{ condition: "state", entity_id: "light.kitchen", state: "on" },
{
condition: "numeric_state",
@ -34,11 +34,11 @@ const conditions = [
above: 20,
},
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise", offset: "-01:00" },
{ condition: "sun", after: "sunrise", before_offset: 3600 },
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "trigger", id: "motion" },
{ condition: "time" },
{ condition: "template" },
{ condition: "template", value_template: "" },
];
const initialCondition: Condition = {

View File

@ -55,6 +55,7 @@ export class DemoAutomationTraceTimeline extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
}
static get styles() {

View File

@ -60,6 +60,7 @@ export class DemoAutomationTrace extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
}
static get styles() {

View File

@ -11,7 +11,7 @@ const ENTITIES = [
latitude: 32.877105,
longitude: 117.232185,
gps_accuracy: 91,
battery: 71,
battery: 25,
friendly_name: "Paulus",
}),
getEntity("device_tracker", "demo_anne_therese", "school", {
@ -19,7 +19,7 @@ const ENTITIES = [
latitude: 32.877105,
longitude: 117.232185,
gps_accuracy: 91,
battery: 71,
battery: 50,
friendly_name: "Anne Therese",
}),
getEntity("device_tracker", "demo_home_boy", "home", {
@ -27,7 +27,7 @@ const ENTITIES = [
latitude: 32.877105,
longitude: 117.232185,
gps_accuracy: 91,
battery: 71,
battery: 75,
friendly_name: "Home Boy",
}),
getEntity("light", "bed_light", "on", {
@ -39,21 +39,53 @@ const ENTITIES = [
getEntity("light", "ceiling_lights", "off", {
friendly_name: "Ceiling Lights",
}),
getEntity("sensor", "battery_1", 20, {
device_class: "battery",
friendly_name: "Battery 1",
unit_of_measurement: "%",
}),
getEntity("sensor", "battery_2", 35, {
device_class: "battery",
friendly_name: "Battery 2",
unit_of_measurement: "%",
}),
getEntity("sensor", "battery_3", 40, {
device_class: "battery",
friendly_name: "Battery 3",
unit_of_measurement: "%",
}),
getEntity("sensor", "battery_4", 80, {
device_class: "battery",
friendly_name: "Battery 4",
unit_of_measurement: "%",
}),
getEntity("input_number", "min_battery_level", 30, {
mode: "slider",
step: 10,
min: 0,
max: 100,
icon: "mdi:battery-alert-variant",
friendly_name: "Minimum Battery Level",
unit_of_measurement: "%",
}),
];
const CONFIGS = [
{
heading: "Unfiltered controller",
heading: "Unfiltered entities",
config: `
- type: entities
entities:
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
`,
},
{
heading: "Filtered entities card",
heading: "On and home entities",
config: `
- type: entity-filter
entities:
@ -63,9 +95,28 @@ const CONFIGS = [
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
state_filter:
- "on"
- home
conditions:
- condition: state
state:
- "on"
- home
`,
},
{
heading: "Same state as Bed Light",
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- light.bed_light
`,
},
{
@ -79,9 +130,11 @@ const CONFIGS = [
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
state_filter:
- "on"
- not_home
conditions:
- condition: state
state:
- "on"
- home
card:
type: entities
title: Custom Title
@ -99,15 +152,101 @@ const CONFIGS = [
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
state_filter:
- "on"
- not_home
conditions:
- condition: state
state:
- "on"
- home
card:
type: glance
show_state: true
title: Custom Title
`,
},
{
heading:
"Filtered entities by battery attribute (< '30') using state filter",
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
state_filter:
- operator: <
attribute: battery
value: "30"
`,
},
{
heading: "Unfiltered number entities",
config: `
- type: entities
entities:
- input_number.min_battery_level
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
`,
},
{
heading: "Battery lower than 50%",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: 50
`,
},
{
heading: "Battery lower than min battery level",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: input_number.min_battery_level
`,
},
{
heading: "Battery between min battery level and 70%",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
above: input_number.min_battery_level
below: 70
`,
},
{
heading: "Error: Entities must be specified",
config: `
- type: entity-filter
`,
},
{
heading: "Error: Incorrect filter config",
config: `
- type: entity-filter
entities:
- sensor.gas_station_lowest_price
`,
},
];
@customElement("demo-lovelace-entity-filter-card")

View File

@ -46,7 +46,9 @@ const ENTITIES = [
friendly_name: "Sensibo purifier",
fan_modes: ["low", "high"],
fan_mode: "low",
supported_features: 9,
swing_modes: ["on", "off", "both", "vertical", "horizontal"],
swing_mode: "vertical",
supported_features: 41,
}),
getEntity("climate", "unavailable", "unavailable", {
supported_features: 43,
@ -85,6 +87,14 @@ const CONFIGS = [
fan_modes:
- low
- high
- type: climate-swing-modes
style: icons
swing_modes:
- 'on'
- 'off'
- 'both'
- 'vertical'
- 'horizontal'
`,
},
{

View File

@ -31,6 +31,7 @@ const createConfigEntry = (
supports_options: false,
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,

View File

@ -1263,6 +1263,7 @@ class HassioAddonInfo extends LitElement {
.card-actions {
justify-content: space-between;
display: flex;
direction: var(--direction);
}
.changelog {
display: contents;

View File

@ -154,12 +154,16 @@ class HassioHardwareDialog extends LitElement {
ha-icon-button {
position: absolute;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
margin-inline-start: 18px;
margin-inline-end: 42px;
color: var(--primary-text-color);
}

View File

@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -27,6 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@ -106,44 +106,46 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
${repositories.length
? repositories.map(
(repo) => html`
<paper-item class="option">
<paper-item-body three-line>
<div>${repo.name}</div>
<div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div>
</paper-item-body>
<div class="delete">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
</paper-item>
`
)
: html`<paper-item> No repositories </paper-item>`}
<ha-list-new>
${repositories.length
? repositories.map(
(repo) => html`
<ha-list-item-new class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
<div>${repo.url}</div>
</div>
<div class="delete" slot="end">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
</ha-list-item-new>
`
)
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@ -206,6 +208,9 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-list-item-new {
position: relative;
}
`,
];
}

View File

@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.23.9",
"@braintree/sanitize-url": "7.0.0",
"@codemirror/autocomplete": "6.12.0",
"@babel/runtime": "7.24.1",
"@braintree/sanitize-url": "7.0.1",
"@codemirror/autocomplete": "6.15.0",
"@codemirror/commands": "6.3.3",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.0",
"@codemirror/view": "6.24.0",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.2",
"@formatjs/intl-displaynames": "6.6.6",
@ -43,18 +43,18 @@
"@formatjs/intl-numberformat": "8.10.0",
"@formatjs/intl-pluralrules": "5.2.12",
"@formatjs/intl-relativetimeformat": "11.2.12",
"@fullcalendar/core": "6.1.10",
"@fullcalendar/daygrid": "6.1.10",
"@fullcalendar/interaction": "6.1.10",
"@fullcalendar/list": "6.1.10",
"@fullcalendar/luxon3": "6.1.10",
"@fullcalendar/timegrid": "6.1.10",
"@fullcalendar/core": "6.1.11",
"@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/interaction": "6.1.11",
"@fullcalendar/list": "6.1.11",
"@fullcalendar/luxon3": "6.1.11",
"@fullcalendar/timegrid": "6.1.11",
"@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.2",
"@lit-labs/virtualizer": "2.0.12",
"@lrnwebcomponents/simple-tooltip": "patch:@lrnwebcomponents/simple-tooltip@npm%3A8.0.0#~/.yarn/patches/@lrnwebcomponents-simple-tooltip-npm-8.0.0-77591f2e0c.patch",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
@ -72,6 +72,7 @@
"@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0",
"@material/mwc-tab": "0.27.0",
"@material/mwc-tab-bar": "0.27.0",
@ -80,17 +81,16 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.2.0",
"@material/web": "=1.3.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.6",
"@vaadin/vaadin-themable-mixin": "24.3.6",
"@vaadin/combo-box": "24.3.9",
"@vaadin/vaadin-themable-mixin": "24.3.9",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -98,18 +98,19 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "4.4.1",
"chart.js": "4.4.2",
"color-name": "2.0.0",
"comlink": "4.4.1",
"core-js": "3.35.1",
"core-js": "3.36.1",
"cropperjs": "1.6.1",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"date-fns-tz": "2.0.1",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "1.5.4",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
@ -118,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "12.0.0",
"marked": "12.0.1",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -129,7 +130,7 @@
"rrule": "2.8.1",
"sortablejs": "1.15.2",
"stacktrace-js": "2.0.2",
"superstruct": "1.0.3",
"superstruct": "1.0.4",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
@ -146,20 +147,20 @@
"workbox-precaching": "7.0.0",
"workbox-routing": "7.0.0",
"workbox-strategies": "7.0.0",
"xss": "1.0.14"
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.23.9",
"@babel/helper-define-polyfill-provider": "0.5.0",
"@babel/plugin-proposal-decorators": "7.23.9",
"@babel/plugin-transform-runtime": "7.23.9",
"@babel/preset-env": "7.23.9",
"@babel/preset-typescript": "7.23.3",
"@bundle-stats/plugin-webpack-filter": "4.9.2",
"@babel/core": "7.24.3",
"@babel/helper-define-polyfill-provider": "0.6.1",
"@babel/plugin-proposal-decorators": "7.24.1",
"@babel/plugin-transform-runtime": "7.24.3",
"@babel/preset-env": "7.24.3",
"@babel/preset-typescript": "7.24.1",
"@bundle-stats/plugin-webpack-filter": "4.12.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1",
"@lokalise/node-api": "12.3.0",
"@octokit/auth-oauth-device": "7.0.1",
"@octokit/plugin-retry": "7.0.3",
"@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
@ -169,7 +170,8 @@
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.13",
"@types/chromecast-caf-sender": "1.0.8",
"@types/chromecast-caf-sender": "1.0.9",
"@types/color-name": "1.1.3",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
@ -179,21 +181,21 @@
"@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.7",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.11",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.0.1",
"@typescript-eslint/parser": "7.0.1",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "5.1.0",
"del": "7.1.0",
"eslint": "8.56.0",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3",
@ -207,22 +209,22 @@
"glob": "10.3.10",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
"gulp-merge-json": "2.1.2",
"gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.2.1",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
"husky": "9.0.10",
"husky": "9.0.11",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.2",
"lit-analyzer": "2.0.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.7",
"magic-string": "0.30.8",
"map-stream": "0.0.7",
"mocha": "10.3.0",
"object-hash": "3.0.0",
"open": "10.0.3",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.2.5",
"rollup": "2.79.1",
@ -235,17 +237,17 @@
"systemjs": "6.14.3",
"tar": "6.2.0",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.0.2",
"transform-async-modules-webpack-plugin": "1.0.3",
"ts-lit-plugin": "2.0.2",
"typescript": "5.3.3",
"typescript": "5.4.3",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.90.1",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.0",
"webpackbar": "6.0.1",
"workbox-build": "7.0.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
@ -258,5 +260,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@4.1.0"
"packageManager": "yarn@4.1.1"
}

View File

@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240207.0"
version = "20240228.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.10.0"
requires-python = ">=3.11.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"

View File

@ -1,3 +1,5 @@
import { theme2hex } from "./convert-color";
export const COLORS = [
"#44739e",
"#984ea3",
@ -65,10 +67,10 @@ export function getColorByIndex(index: number) {
export function getGraphColorByIndex(
index: number,
style: CSSStyleDeclaration
) {
): string {
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
return (
const themeColor =
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index)
);
getColorByIndex(index);
return theme2hex(themeColor);
}

View File

@ -1,3 +1,4 @@
import colors from "color-name";
import { expandHex } from "./hex";
const rgb_hex = (component: number): string => {
@ -126,3 +127,18 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
hsv2rgb([hs[0], hs[1], 255]);
export function theme2hex(themeColor: string): string {
if (themeColor.startsWith("#")) {
return themeColor;
}
const rgbFromColorName = colors[themeColor];
if (!rgbFromColorName) {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
}
return rgb2hex(rgbFromColorName);
}

View File

@ -1,19 +1,25 @@
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page);
!hideAdvancedPage(hass, page) &&
isNotLoadedIntegration(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
page.component
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
!page.component ||
ensureArray(page.component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isNotLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

View File

@ -1,8 +1,13 @@
import { MAIN_WINDOW_NAME } from "../../data/main_window";
export const mainWindow =
window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top!;
export const mainWindow = (() => {
try {
return window.name === MAIN_WINDOW_NAME
? window
: parent.name === MAIN_WINDOW_NAME
? parent
: top!;
} catch {
return window;
}
})();

View File

@ -20,14 +20,14 @@ function findNestedItem(
}, obj);
}
export function nestedArrayMove<T>(
obj: T | T[],
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
newIndex: number,
oldPath?: ItemPath,
newPath?: ItemPath
): T | T[] {
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
): A {
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;

View File

@ -75,6 +75,8 @@ export class HaChartBase extends LitElement {
private _paddingYAxisInternal = 0;
private _datasetOrder: number[] = [];
public disconnectedCallback() {
super.disconnectedCallback();
this._releaseCanvas();
@ -165,7 +167,17 @@ export class HaChartBase extends LitElement {
}
}
// put the legend labels in sorted order if provided
if (changedProps.has("data")) {
this._datasetOrder = this.data.datasets.map((_, index) => index);
if (this.data?.datasets.some((dataset) => dataset.order)) {
this._datasetOrder.sort(
(a, b) =>
(this.data.datasets[a].order || 0) -
(this.data.datasets[b].order || 0)
);
}
if (this.externalHidden) {
this._hiddenDatasets = new Set();
if (this.data?.datasets) {
@ -205,8 +217,9 @@ export class HaChartBase extends LitElement {
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
<ul>
${this.data.datasets.map((dataset, index) =>
this.extraData?.[index]?.show_legend === false
${this._datasetOrder.map((index) => {
const dataset = this.data.datasets[index];
return this.extraData?.[index]?.show_legend === false
? nothing
: html`<li
.datasetIndex=${index}
@ -228,8 +241,8 @@ export class HaChartBase extends LitElement {
${this.extraData?.[index]?.legend_label ??
dataset.label}
</div>
</li>`
)}
</li>`;
})}
</ul>
</div>`
: ""}

View File

@ -111,7 +111,7 @@ export class StateHistoryChartLine extends LitElement {
config: this.hass.config,
},
},
suggestedMin: this.startTime,
min: this.startTime,
suggestedMax: this.endTime,
ticks: {
maxRotation: 0,

View File

@ -114,7 +114,7 @@ export class StateHistoryChartTimeline extends LitElement {
config: this.hass.config,
},
},
suggestedMin: this.startTime,
min: this.startTime,
suggestedMax: this.endTime,
ticks: {
autoSkip: true,

View File

@ -233,16 +233,32 @@ export class StateHistoryCharts extends LitElement {
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
(this.historyData?.timeline ?? []).reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
let minTimeAll = (this.historyData?.timeline ?? []).reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
);
minTimeAll = (this.historyData?.line ?? []).reduce(
(minTimeLine, line) =>
Math.min(
minTimeLine,
line.data.reduce(
(minTimeData, data) =>
Math.min(
minTimeData,
new Date(data.states[0].last_changed).getTime()
),
minTimeLine
)
),
minTimeAll
);
this._computedStartTime = new Date(minTimeAll);
}
}
}

View File

@ -16,6 +16,7 @@ import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import {
formatNumber,
numberFormatToLocale,
@ -25,6 +26,7 @@ import {
getDisplayUnit,
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
Statistics,
statisticsHaveType,
StatisticsMetaData,
@ -79,6 +81,8 @@ export class StatisticsChart extends LitElement {
@property({ type: Boolean }) public isLoadingData = false;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property() public period?: string;
@state() private _chartData: ChartData = { datasets: [] };
@ -273,6 +277,33 @@ export class StatisticsChart extends LitElement {
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return;
}
const chart = e.chart;
const points = chart.getElementsAtEventForMode(
e,
"nearest",
{ intersect: true },
true
);
if (points.length) {
const firstPoint = points[0];
const statisticId = this._statisticIds[firstPoint.datasetIndex];
if (!isExternalStatistic(statisticId)) {
fireEvent(this, "hass-more-info", { entityId: statisticId });
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
}
}
},
};
}

View File

@ -205,7 +205,9 @@ export class TimelineController extends BarController {
const y = vScale.getPixelForValue(this.index);
const xStart = iScale.getPixelForValue(data.start.getTime());
const xStart = iScale.getPixelForValue(
Math.max(iScale.min, data.start.getTime())
);
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;

View File

@ -49,7 +49,7 @@ export class TimeLineScale extends TimeScale {
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
// Make sure that max is strictly higher than min (required by the lookup table)
this.min = Math.min(min, max - 1);
this.max = Math.max(min + 1, max);
this.min = adapter.parse(options.min, this) ?? Math.min(min, max - 1);
this.max = adapter.parse(options.max, this) ?? Math.max(min + 1, max);
}
}

View File

@ -44,6 +44,8 @@ class HaDataTableIcon extends LitElement {
div {
position: absolute;
right: 28px;
inset-inline-end: 28px;
inset-inline-start: initial;
z-index: 1002;
outline: none;
font-size: 10px;

View File

@ -32,7 +32,9 @@ export class StateBadge extends LitElement {
@property() public overrideImage?: string;
@property({ type: Boolean }) public stateColor = false;
// Cannot be a boolean attribute because undefined is treated different than
// false. When it is undefined, state is still colored for light entities.
@property({ attribute: false }) public stateColor?: boolean;
@property() public color?: string;
@ -70,7 +72,7 @@ export class StateBadge extends LitElement {
const domain = this.stateObj
? computeStateDomain(this.stateObj)
: undefined;
return this.stateColor || (domain === "light" && this.stateColor !== false);
return this.stateColor ?? domain === "light";
}
protected render() {

View File

@ -156,6 +156,7 @@ class HaClimateState extends LitElement {
.current {
color: var(--secondary-text-color);
direction: var(--direction);
}
.state-label {

View File

@ -127,9 +127,11 @@ export class HaControlButton extends LitElement {
opacity 180ms ease-in-out;
opacity: var(--control-button-background-opacity);
}
.button ::slotted(*) {
.button {
transition: color 180ms ease-in-out;
color: var(--control-button-icon-color);
}
.button ::slotted(*) {
pointer-events: none;
}
.button:disabled {

View File

@ -273,9 +273,13 @@ export class HaControlNumberButton extends LitElement {
}
.button.minus {
left: 0;
inset-inline-start: 0;
inset-inline-end: initial;
}
.button.plus {
right: 0;
inset-inline-start: initial;
inset-inline-end: 0;
}
.unit {
white-space: pre;

View File

@ -1,11 +1,13 @@
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield")
export class HaFormfield extends FormfieldBase {
@property({ type: Boolean, reflect: true }) public disabled = false;
protected _labelClick() {
const input = this.input as HTMLInputElement | undefined;
if (!input) return;
@ -44,6 +46,9 @@ export class HaFormfield extends FormfieldBase {
padding-inline-start: 4px;
padding-inline-end: 0;
}
:host([disabled]) label {
color: var(--disabled-text-color);
}
`,
];
}

View File

@ -136,6 +136,8 @@ class HaMenuButton extends LitElement {
height: 12px;
top: 9px;
right: 7px;
inset-inline-end: 7px;
inset-inline-start: initial;
border-radius: 50%;
border: 2px solid var(--app-header-background-color);
}

View File

@ -19,7 +19,9 @@ class HaMetric extends LitElement {
<ha-settings-row>
<span slot="heading"> ${this.heading} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<span class="value">
<div>${roundedValue} %</div>
</span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
@ -70,6 +72,10 @@ class HaMetric extends LitElement {
padding-inline-start: initial;
flex-shrink: 0;
}
.value > div {
direction: ltr;
text-align: var(--float-start);
}
`;
}
}

View File

@ -17,7 +17,7 @@ export const pushSupported =
class HaPushNotificationsToggle extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _disabled: boolean = false;
@property({ type: Boolean }) public disabled!: boolean;
@state() private _pushChecked: boolean =
"Notification" in window && Notification.permission === "granted";
@ -27,7 +27,7 @@ class HaPushNotificationsToggle extends LitElement {
protected render(): TemplateResult {
return html`
<ha-switch
.disabled=${this._disabled || this._loading}
.disabled=${this.disabled || this._loading}
.checked=${this._pushChecked}
@change=${this._handlePushChange}
></ha-switch>

View File

@ -186,6 +186,8 @@ class HaQrScanner extends LitElement {
position: absolute;
bottom: 8px;
right: 8px;
inset-inline-end: 8px;
inset-inline-start: initial;
background: #727272b2;
color: white;
border-radius: 50%;

View File

@ -102,7 +102,10 @@ export class HaSelectSelector extends LitElement {
${this.label}
${options.map(
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-formfield
.label=${item.label}
.disabled=${item.disabled || this.disabled}
>
<ha-radio
.checked=${item.value === this.value}
.value=${item.value}

View File

@ -93,6 +93,8 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean, reflect: true }) public hidePicker = false;
@property({ type: Boolean }) public hideDescription = false;
@state() private _value!: this["value"];
@state() private _checkedKeys = new Set();
@ -373,7 +375,8 @@ export class HaServiceControl extends LitElement {
)) ||
serviceData?.description;
return html`${this.hidePicker
return html`
${this.hidePicker
? nothing
: html`<ha-service-picker
.hass=${this.hass}
@ -381,29 +384,33 @@ export class HaServiceControl extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
></ha-service-picker>`}
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
? html` <a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: ""}
</div>
${this.hideDescription
? nothing
: html`
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
? html` <a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
`}
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
@ -517,7 +524,8 @@ export class HaServiceControl extends LitElement {
></ha-selector>
</ha-settings-row>`
: "";
})}`;
})}
`;
}
private _localizeValueCallback = (key: string) => {

View File

@ -114,11 +114,14 @@ class HaServicePicker extends LitElement {
if (!filter) {
return processedServices;
}
return processedServices.filter(
(service) =>
service.service.toLowerCase().includes(filter) ||
service.name?.toLowerCase().includes(filter)
);
const split_filter = filter.split(" ");
return processedServices.filter((service) => {
const lower_service_name = service.name.toLowerCase();
const lower_service = service.service.toLowerCase();
return split_filter.every(
(f) => lower_service_name.includes(f) || lower_service.includes(f)
);
});
}
);

View File

@ -1010,8 +1010,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
.profile paper-icon-item {
padding-left: 4px;
margin-inline-start: 4px;
margin-inline-end: auto;
padding-inline-start: 4px;
padding-inline-end: auto;
}
.profile .item-text {
margin-left: 8px;
@ -1040,6 +1040,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
position: absolute;
bottom: 14px;
left: 26px;
inset-inline-start: 26px;
inset-inline-end: initial;
font-size: 0.65em;
}

View File

@ -14,9 +14,16 @@ declare global {
oldPath?: ItemPath;
newPath?: ItemPath;
};
"drag-start": undefined;
"drag-end": undefined;
}
}
export type HaSortableOptions = Omit<
SortableInstance.SortableOptions,
"onStart" | "onChoose" | "onEnd"
>;
@customElement("ha-sortable")
export class HaSortable extends LitElement {
private _sortable?: SortableInstance;
@ -36,14 +43,17 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
@property({ type: String, attribute: "group" })
public group?: string;
@property({ type: Number, attribute: "swap-threshold" })
public swapThreshold?: number;
@property({ type: String })
public group?: string | SortableInstance.GroupOptions;
@property({ type: Boolean, attribute: "invert-swap" })
public invertSwap?: boolean;
public invertSwap: boolean = false;
@property({ attribute: false })
public options?: HaSortableOptions;
@property({ type: Boolean })
public rollback: boolean = true;
protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("disabled")) {
@ -72,6 +82,9 @@ export class HaSortable extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._shouldBeDestroy = false;
if (this.hasUpdated) {
this._createSortable();
}
}
protected createRenderRoot() {
@ -114,26 +127,20 @@ export class HaSortable extends LitElement {
const options: SortableInstance.Options = {
animation: 150,
swapThreshold: 1,
...this.options,
onChoose: this._handleChoose,
onStart: this._handleStart,
onEnd: this._handleEnd,
};
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
}
if (this.swapThreshold !== undefined) {
options.swapThreshold = this.swapThreshold;
}
if (this.invertSwap !== undefined) {
options.invertSwap = this.invertSwap;
}
if (this.handleSelector) {
options.handle = this.handleSelector;
}
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
if (this.invertSwap !== undefined) {
options.invertSwap = this.invertSwap;
}
if (this.group) {
options.group = this.group;
@ -143,8 +150,9 @@ export class HaSortable extends LitElement {
}
private _handleEnd = async (evt: SortableEvent) => {
fireEvent(this, "drag-end");
// put back in original location
if ((evt.item as any).placeholder) {
if (this.rollback && (evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
@ -170,7 +178,12 @@ export class HaSortable extends LitElement {
});
};
private _handleStart = () => {
fireEvent(this, "drag-start");
};
private _handleChoose = (evt: SortableEvent) => {
if (!this.rollback) return;
(evt.item as any).placeholder = document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
};

View File

@ -90,7 +90,7 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
direction: var(--direction);
direction: ltr;
}
.mdc-text-field--with-leading-icon {
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
@ -199,7 +199,6 @@ export class HaTextField extends TextFieldBase {
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"
? css`
.mdc-text-field__affix--suffix,
.mdc-text-field--with-leading-icon,
.mdc-text-field__icon--leading,
.mdc-floating-label,

View File

@ -1,35 +1,8 @@
import "@polymer/paper-toast/paper-toast";
import type { PaperToastElement } from "@polymer/paper-toast/paper-toast";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
const PaperToast = customElements.get(
"paper-toast"
) as Constructor<PaperToastElement>;
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
@customElement("ha-toast")
export class HaToast extends PaperToast {
private _resizeListener?: (obj: { matches: boolean }) => unknown;
private _mediaq?: MediaQueryList;
public connectedCallback() {
super.connectedCallback();
if (!this._resizeListener) {
this._resizeListener = (ev) =>
this.classList.toggle("fit-bottom", ev.matches);
this._mediaq = window.matchMedia("(max-width: 599px");
}
this._mediaq!.addListener(this._resizeListener);
this._resizeListener(this._mediaq!);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._mediaq!.removeListener(this._resizeListener!);
}
}
export class HaToast extends Snackbar {}
declare global {
interface HTMLElementTagNameMap {

View File

@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px);
direction: ltr;
}
:host(.opened) ha-media-player-browse {

View File

@ -879,6 +879,7 @@ export class HaMediaPlayerBrowse extends LitElement {
display: flex;
flex-direction: column;
position: relative;
direction: ltr;
}
ha-circular-progress {

View File

@ -163,21 +163,22 @@ export class HaTracePathDetails extends LitElement {
}
)}
<br />
${error
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
${result
? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>`
: error
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
: nothing}
${Object.keys(rest).length === 0
? nothing
: html`<pre>${dump(rest)}</pre>`}

View File

@ -1,15 +1,16 @@
import { mdiExclamationThick } from "@mdi/js";
import {
css,
LitElement,
PropertyValues,
html,
TemplateResult,
svg,
css,
html,
nothing,
svg,
} from "lit";
import { customElement, property } from "lit/decorators";
import { NODE_SIZE, SPACING } from "./hat-graph-const";
import { isSafari } from "../../util/is_safari";
import { NODE_SIZE, SPACING } from "./hat-graph-const";
/**
* @attribute active
@ -21,6 +22,8 @@ export class HatGraphNode extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public error = false;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ reflect: true, type: Boolean }) graphStart = false;
@ -65,16 +68,28 @@ export class HatGraphNode extends LitElement {
`}
<g class="node">
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
${this.error
? svg`
<g class="error">
<circle
cx="-12"
cy=${-NODE_SIZE / 2}
r="8"
></circle>
<path transform="translate(-18 -21) scale(.5)" class="exclamation" d=${mdiExclamationThick}/>
</g>
`
: nothing}
${this.badge
? svg`
<g class="number">
<circle
cx="8"
cx="12"
cy=${-NODE_SIZE / 2}
r="8"
></circle>
<text
x="8"
x="12"
y=${-NODE_SIZE / 2}
text-anchor="middle"
alignment-baseline="middle"
@ -82,7 +97,7 @@ export class HatGraphNode extends LitElement {
</g>
`
: nothing}
<g style="pointer-events: none" transform="translate(${-12} ${-12})">
<g style="pointer-events: none" transform="translate(-12 -12)">
${this.iconPath
? svg`<path class="icon" d=${this.iconPath}/>`
: svg`<foreignObject><span class="icon"><slot name="icon"></slot></span></foreignObject>`}
@ -143,13 +158,22 @@ export class HatGraphNode extends LitElement {
fill: var(--background-clr);
stroke: var(--circle-clr, var(--stroke-clr));
}
.error circle {
fill: var(--error-color);
stroke: none;
stroke-width: 0;
}
.error .exclamation {
fill: var(--text-primary-color);
}
.number circle {
fill: var(--track-clr);
stroke: none;
stroke-width: 0;
}
.number text {
font-size: smaller;
font-size: 10px;
fill: var(--text-primary-color);
}
path.icon {
fill: var(--icon-clr);

View File

@ -93,6 +93,7 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
`;
@ -171,6 +172,7 @@ export class HatScriptGraph extends LitElement {
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
slot="head"
nofocus
></hat-graph-node>
@ -424,6 +426,7 @@ export class HatScriptGraph extends LitElement {
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
>
${node.service
@ -451,6 +454,7 @@ export class HatScriptGraph extends LitElement {
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -517,6 +521,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node>
`;

View File

@ -153,7 +153,7 @@ class LogbookRenderer {
const parts: TemplateResult[] = [];
let i;
let i: number;
for (
i = 0;
@ -232,7 +232,7 @@ class ActionRenderer {
const value = this._getItem(index);
if (renderAllIterations) {
let i;
let i: number = 0;
value.forEach((item) => {
i = this._renderIteration(index, item, actionType);
});
@ -270,7 +270,12 @@ class ActionRenderer {
} catch (err: any) {
this._renderEntry(
path,
`Unable to extract path ${path}. Download trace and report as bug`
this.hass.localize(
"ui.panel.config.automation.trace.messages.path_error",
{
path: path,
}
)
);
return index + 1;
}
@ -324,20 +329,22 @@ class ActionRenderer {
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
this._renderEntry(
triggerStep.path,
`${
triggerStep.changed_variables.trigger.alias
? `${triggerStep.changed_variables.trigger.alias} triggered`
: "Triggered"
} ${
triggerStep.path === "trigger"
? "manually"
: `by the ${this.trace.trigger}`
} at
${formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale,
this.hass.config
)}`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.triggered_by",
{
triggeredBy: triggerStep.changed_variables.trigger?.alias
? "alias"
: "other",
alias: triggerStep.changed_variables.trigger?.alias,
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
trigger: this.trace.trigger,
time: formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale,
this.hass.config
),
}
),
mdiCircle
);
return index + 1;
@ -367,12 +374,17 @@ class ActionRenderer {
this.keys[index]
) as ChooseAction;
const disabled = chooseConfig.enabled === false;
const name = chooseConfig.alias || "Choose";
const name =
chooseConfig.alias ||
this.hass.localize("ui.panel.config.automation.trace.messages.choose");
if (defaultExecuted) {
this._renderEntry(
choosePath,
`${name}: Default action executed`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.default_action_executed",
{ name: name }
),
undefined,
disabled
);
@ -385,8 +397,17 @@ class ActionRenderer {
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice | undefined;
const choiceName = choiceConfig
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
: `Error: ${chooseTrace.error}`;
? `${
choiceConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.option_executed",
{ option: choiceNumeric }
)
}`
: this.hass.localize(
"ui.panel.config.automation.trace.messages.error",
{ error: chooseTrace.error }
);
this._renderEntry(
choosePath,
`${name}: ${choiceName}`,
@ -396,13 +417,16 @@ class ActionRenderer {
} else {
this._renderEntry(
choosePath,
`${name}: No action taken`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.no_action_executed",
{ name: name }
),
undefined,
disabled
);
}
let i;
let i: number;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
@ -479,26 +503,38 @@ class ActionRenderer {
const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
const disabled = ifConfig.enabled === false;
const name = ifConfig.alias || "If";
const name =
ifConfig.alias ||
this.hass.localize("ui.panel.config.automation.trace.messages.if");
if (ifTrace.result?.choice) {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/${ifTrace.result.choice}/`
) as any;
const choiceName = choiceConfig
? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}`
: `Error: ${ifTrace.error}`;
? choiceConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.action_executed",
{ action: ifTrace.result.choice }
)
: this.hass.localize(
"ui.panel.config.automation.trace.messages.error",
{ error: ifTrace.error }
);
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled);
} else {
this._renderEntry(
ifPath,
`${name}: No action taken`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.no_action_executed",
{ name: name }
),
undefined,
disabled
);
}
let i;
let i: number;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
@ -534,7 +570,11 @@ class ActionRenderer {
const disabled = parallelConfig.enabled === false;
const name = parallelConfig.alias || "Execute in parallel";
const name =
parallelConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.execute_in_parallel"
);
this._renderEntry(parallelPath, name, undefined, disabled);
@ -564,7 +604,11 @@ class ActionRenderer {
this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
${description}${disabled
? html`<span class="disabled"> (disabled)</span>`
? html`<span class="disabled">
${this.hass.localize(
"ui.panel.config.automation.trace.messages.disabled"
)}</span
>`
: ""}
</ha-timeline>
`);
@ -636,13 +680,12 @@ export class HaAutomationTracer extends LitElement {
this.hass.locale,
this.hass.config
);
const renderRuntime = () => `(runtime:
${(
const renderRuntime = () =>
(
(new Date(this.trace!.timestamp.finish!).getTime() -
new Date(this.trace!.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`;
).toFixed(2);
let entry: {
description: TemplateResult | string;
@ -652,57 +695,90 @@ export class HaAutomationTracer extends LitElement {
if (this.trace.state === "running") {
entry = {
description: "Still running",
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.still_running"
),
icon: mdiProgressClock,
};
} else if (this.trace.state === "debugged") {
entry = {
description: "Debugged",
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.debugged"
),
icon: mdiProgressWrench,
};
} else if (this.trace.script_execution === "finished") {
entry = {
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.finished",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiCircle,
};
} else if (this.trace.script_execution === "aborted") {
entry = {
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.aborted",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiAlertCircle,
};
} else if (this.trace.script_execution === "cancelled") {
entry = {
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.cancelled",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiAlertCircle,
};
} else {
let reason: string;
let message:
| "stopped_failed_conditions"
| "stopped_failed_single"
| "stopped_failed_max_runs"
| "stopped_error"
| "stopped_unknown_reason";
let isError = false;
let extra: TemplateResult | undefined;
switch (this.trace.script_execution) {
case "failed_conditions":
reason = "a condition failed";
message = "stopped_failed_conditions";
break;
case "failed_single":
reason = "only a single execution is allowed";
message = "stopped_failed_single";
break;
case "failed_max_runs":
reason = "maximum number of parallel runs reached";
message = "stopped_failed_max_runs";
break;
case "error":
reason = "an error was encountered";
isError = true;
message = "stopped_error";
extra = html`<br /><br />${this.trace.error!}`;
break;
default:
reason = `of unknown reason "${this.trace.script_execution}"`;
isError = true;
message = "stopped_unknown_reason";
}
entry = {
description: html`Stopped because ${reason} at ${renderFinishedAt()}
${renderRuntime()}${extra || ""}`,
description: html`${this.hass.localize(
`ui.panel.config.automation.trace.messages.${message}`,
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
)}
${extra || ""}`,
icon: mdiAlertCircle,
className: isError ? "error" : undefined,
};

View File

@ -219,8 +219,8 @@ export interface NumericStateCondition extends BaseCondition {
condition: "numeric_state";
entity_id: string;
attribute?: string;
above?: number;
below?: number;
above?: string | number;
below?: string | number;
value_template?: string;
}

View File

@ -148,6 +148,11 @@ export const updateCloudPref = (
...prefs,
});
export const removeCloudData = (hass: HomeAssistant) =>
hass.callWS({
type: "cloud/remove_data",
});
export const updateCloudGoogleEntityConfig = (
hass: HomeAssistant,
entity_id: string,

View File

@ -1,5 +1,4 @@
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import { HomeAssistant } from "../../types";
export interface CloudTTSInfo {
@ -27,27 +26,21 @@ export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
return languages;
};
export const getCloudTtsSupportedGenders = (
export const getCloudTtsSupportedVoices = (
language: string,
info: CloudTTSInfo | undefined,
localize: LocalizeFunc
info: CloudTTSInfo | undefined
) => {
const genders: Array<[string, string]> = [];
const voices: Array<string> = [];
if (!info) {
return genders;
return voices;
}
for (const [curLang, gender] of info.languages) {
for (const [curLang, voice] of info.languages) {
if (curLang === language) {
genders.push([
gender,
gender === "male" || gender === "female"
? localize(`ui.components.media-browser.tts.gender_${gender}`)
: gender,
]);
voices.push(voice);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
return voices.sort((a, b) => caseInsensitiveStringCompare(a, b));
};

View File

@ -18,6 +18,7 @@ export interface ConfigEntry {
supports_options: boolean;
supports_remove_device: boolean;
supports_unload: boolean;
supports_reconfigure: boolean;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;

View File

@ -26,13 +26,18 @@ const HEADERS = {
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
};
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
export const createConfigFlow = (
hass: HomeAssistant,
handler: string,
entry_id?: string
) =>
hass.callApi<DataEntryFlowStep>(
"POST",
"config/config_entries/flow",
{
handler,
show_advanced_options: Boolean(hass.userData?.showAdvanced),
entry_id,
},
HEADERS
);

View File

@ -33,6 +33,7 @@ export interface DataEntryFlowStepForm {
description_placeholders?: Record<string, string>;
last_step: boolean | null;
preview?: string;
translation_domain?: string;
}
export interface DataEntryFlowStepExternal {
@ -42,6 +43,7 @@ export interface DataEntryFlowStepExternal {
step_id: string;
url: string;
description_placeholders: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepCreateEntry {
@ -53,6 +55,7 @@ export interface DataEntryFlowStepCreateEntry {
result?: ConfigEntry;
description: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepAbort {
@ -61,6 +64,7 @@ export interface DataEntryFlowStepAbort {
handler: string;
reason: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepProgress {
@ -70,6 +74,7 @@ export interface DataEntryFlowStepProgress {
step_id: string;
progress_action: string;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export interface DataEntryFlowStepMenu {
@ -80,6 +85,7 @@ export interface DataEntryFlowStepMenu {
/** If array, use value to lookup translations in strings.json */
menu_options: string[] | Record<string, string>;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}
export type DataEntryFlowStep =

View File

@ -331,6 +331,9 @@ export const getReferencedStatisticIds = (
}
}
}
if (!(includeTypes && !includeTypes.includes("device"))) {
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
}
return statIDs;
};
@ -383,6 +386,7 @@ const getEnergyData = async (
"solar",
"battery",
"gas",
"device",
]);
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
@ -777,7 +781,7 @@ export const getEnergyGasUnit = (
: "ft³";
};
export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined =>
export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
hass.config.unit_system.length === "km" ? "L" : "gal";
export const energyStatisticHelpUrl =

View File

@ -70,6 +70,7 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
brightness: "%",
},
sun: {
azimuth: "°",
elevation: "°",
},
vacuum: {

View File

@ -81,7 +81,7 @@ export interface EntityHistoryState {
/** attributes */
a: { [key: string]: any };
/** last_changed; if set, also applies to lu */
lc: number;
lc?: number;
/** last_updated */
lu: number;
}
@ -419,17 +419,37 @@ const BLANK_UNIT = " ";
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HistoryStates,
entityIds: string[],
localize: LocalizeFunc,
sensorNumericalDeviceClasses: string[],
splitDeviceClasses = false
): HistoryResult => {
const lineChartDevices: { [unit: string]: HistoryStates } = {};
const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) {
const localStateHistory: HistoryStates = {};
// Create a limited history from stateObj if entity has no recorded history.
const allEntities = new Set([...entityIds, ...Object.keys(stateHistory)]);
allEntities.forEach((entity) => {
if (entity in stateHistory) {
localStateHistory[entity] = stateHistory[entity];
} else if (hass.states[entity]) {
localStateHistory[entity] = [
{
s: hass.states[entity].state,
a: hass.states[entity].attributes,
lu: new Date(hass.states[entity].last_updated).getTime() / 1000,
},
];
}
});
if (!localStateHistory) {
return { line: [], timeline: [] };
}
Object.keys(stateHistory).forEach((entityId) => {
const stateInfo = stateHistory[entityId];
Object.keys(localStateHistory).forEach((entityId) => {
const stateInfo = localStateHistory[entityId];
if (stateInfo.length === 0) {
return;
}

View File

@ -198,8 +198,9 @@ export const entryIcon = async (
if (entry.icon) {
return entry.icon;
}
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
const domain = computeDomain(entry.entity_id);
return getEntityIcon(hass, domain, undefined, undefined, entry);
return getEntityIcon(hass, domain, stateObj, undefined, entry);
};
const getEntityIcon = async (

View File

@ -43,6 +43,7 @@ export interface IntegrationManifest {
| "cloud_push"
| "local_polling"
| "local_push";
single_config_entry?: boolean;
}
export interface IntegrationSetup {
domain: string;

View File

@ -11,6 +11,7 @@ export interface Integration {
iot_class?: string;
supported_by?: string;
is_built_in?: boolean;
single_config_entry?: boolean;
}
export interface Integrations {

View File

@ -3,7 +3,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { getExtendedEntityRegistryEntry } from "./entity_registry";
import { showEnterCodeDialogDialog } from "../dialogs/enter-code/show-enter-code-dialog";
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
import { HomeAssistant } from "../types";
export const FORMAT_TEXT = "text";
@ -38,7 +38,7 @@ export const callProtectedLockService = async (
const defaultCode = lockRegistryEntry?.options?.lock?.default_code;
if (stateObj!.attributes.code_format && !defaultCode) {
const response = await showEnterCodeDialogDialog(element, {
const response = await showEnterCodeDialog(element, {
codeFormat: "text",
codePattern: stateObj!.attributes.code_format,
title: hass.localize(`ui.card.lock.${service}`),

View File

@ -10,8 +10,10 @@ import {
LovelaceCard,
} from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
import { LovelaceViewConfig } from "./lovelace/config/view";
import { HuiSection } from "../panels/lovelace/sections/hui-section";
export interface LovelacePanelConfig {
mode: "yaml" | "storage";
@ -24,10 +26,21 @@ export interface LovelaceViewElement extends HTMLElement {
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
sections?: HuiSection[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;
}
export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
viewIndex?: number;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
isStrategy: boolean;
setConfig(config: LovelaceSectionConfig): void;
}
type LovelaceUpdatedEvent = HassEventBase & {
event_type: "lovelace_updated";
data: {

View File

@ -1,7 +1,10 @@
import { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
view_layout?: any;
layout_options?: LovelaceLayoutOptions;
type: string;
[key: string]: any;
}

View File

@ -0,0 +1,26 @@
import type { LovelaceCardConfig } from "./card";
import type { LovelaceStrategyConfig } from "./strategy";
export interface LovelaceBaseSectionConfig {
title?: string;
}
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
type?: string;
cards?: LovelaceCardConfig[];
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
}
export type LovelaceSectionRawConfig =
| LovelaceSectionConfig
| LovelaceStrategySectionConfig;
export function isStrategySection(
section: LovelaceSectionRawConfig
): section is LovelaceStrategySectionConfig {
return "strategy" in section;
}

View File

@ -1,5 +1,6 @@
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@ -23,6 +24,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string;
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {

View File

@ -26,6 +26,7 @@ import {
Trigger,
} from "./automation";
import { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@ -404,3 +405,11 @@ export const getActionType = (action: Action): ActionType => {
}
return "unknown";
};
export const hasScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return fields !== undefined && Object.keys(fields).length > 0;
};

View File

@ -1,12 +1,17 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { AreaRegistryEntry } from "./area_registry";
const fetchAreaRegistry = (conn: Connection) =>
conn.sendMessagePromise<AreaRegistryEntry[]>({
type: "config/area_registry/list",
});
conn
.sendMessagePromise<AreaRegistryEntry[]>({
type: "config/area_registry/list",
})
.then((areas) =>
areas.sort((ent1, ent2) => stringCompare(ent1.name, ent2.name))
);
const subscribeAreaRegistryUpdates = (
conn: Connection,

View File

@ -23,7 +23,7 @@ export const showConfigFlowDialog = (
loadDevicesAndAreas: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createConfigFlow(hass, handler),
createConfigFlow(hass, handler, dialogParams.entryId),
hass.loadFragmentTranslation("config"),
hass.loadBackendTranslation("config", handler),
hass.loadBackendTranslation("selector", handler),
@ -44,7 +44,7 @@ export const showConfigFlowDialog = (
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.abort.${step.reason}`,
`component.${step.translation_domain || step.handler}.config.abort.${step.reason}`,
step.description_placeholders
);
@ -58,7 +58,7 @@ export const showConfigFlowDialog = (
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${step.handler}.title`)
);
@ -66,7 +66,7 @@ export const showConfigFlowDialog = (
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -84,7 +84,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldHelper(hass, step, field) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders
);
return description
@ -95,7 +95,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${step.handler}.config.error.${error}`,
`component.${step.translation_domain || step.translation_domain || step.handler}.config.error.${error}`,
step.description_placeholders
) || error
);
@ -131,7 +131,7 @@ export const showConfigFlowDialog = (
renderExternalStepDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.${step.step_id}.description`,
step.description_placeholders
);
@ -155,7 +155,7 @@ export const showConfigFlowDialog = (
renderCreateEntryDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.create_entry.${
`component.${step.translation_domain || step.handler}.config.create_entry.${
step.description || "default"
}`,
step.description_placeholders
@ -190,7 +190,7 @@ export const showConfigFlowDialog = (
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.progress.${step.progress_action}`,
`component.${step.translation_domain || step.handler}.config.progress.${step.progress_action}`,
step.description_placeholders
);
return description
@ -210,7 +210,7 @@ export const showConfigFlowDialog = (
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.config.step.${step.step_id}.description`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -222,7 +222,7 @@ export const showConfigFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},

View File

@ -139,6 +139,7 @@ export interface DataEntryFlowDialogParams {
}) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
entryId?: string;
dialogParentElement?: HTMLElement;
}

View File

@ -53,7 +53,7 @@ export const showOptionsFlowDialog = (
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.abort.${step.reason}`,
`component.${step.translation_domain || configEntry.domain}.options.abort.${step.reason}`,
step.description_placeholders
);
@ -71,7 +71,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.title`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`ui.dialogs.options_flow.form.header`)
);
@ -79,7 +79,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -101,7 +101,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldHelper(hass, step, field) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders
);
return description
@ -112,7 +112,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${configEntry.domain}.options.error.${error}`,
`component.${step.translation_domain || configEntry.domain}.options.error.${error}`,
step.description_placeholders
) || error
);
@ -159,7 +159,7 @@ export const showOptionsFlowDialog = (
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.progress.${step.progress_action}`,
`component.${step.translation_domain || configEntry.domain}.options.progress.${step.progress_action}`,
step.description_placeholders
);
return description
@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@ -199,7 +199,7 @@ export const showOptionsFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},

View File

@ -10,7 +10,7 @@ export interface EnterCodeDialogParams {
cancel?: () => void;
}
export const showEnterCodeDialogDialog = (
export const showEnterCodeDialog = (
element: HTMLElement,
dialogParams: EnterCodeDialogParams
) =>

View File

@ -89,7 +89,12 @@ class DialogBox extends LitElement {
</div>
${confirmPrompt &&
html`
<mwc-button @click=${this._dismiss} slot="secondaryAction">
<mwc-button
@click=${this._dismiss}
slot="secondaryAction"
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
@ -97,7 +102,8 @@ class DialogBox extends LitElement {
`}
<mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
slot="primaryAction"
class=${classMap({
destructive: this._params.destructive || false,

View File

@ -16,6 +16,8 @@ export class HaMoreInfoStateHeader extends LitElement {
@property({ attribute: false }) public stateOverride?: string;
@property({ attribute: false }) public changedOverride?: number;
@state() private _absoluteTime = false;
private _localizeState(): TemplateResult | string {
@ -50,13 +52,13 @@ export class HaMoreInfoStateHeader extends LitElement {
? html`
<ha-absolute-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
.datetime=${this.changedOverride ?? this.stateObj.last_changed}
></ha-absolute-time>
`
: html`
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
.datetime=${this.changedOverride ?? this.stateObj.last_changed}
capitalize
></ha-relative-time>
`}

View File

@ -322,6 +322,8 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
position: absolute;
top: -6px;
right: -6px;
inset-inline-end: -6px;
inset-inline-start: initial;
width: 20px;
height: 20px;
outline: none;

View File

@ -446,6 +446,8 @@ class LightRgbColorPicker extends LitElement {
position: absolute;
top: 0;
right: 0;
inset-inline-end: 0;
inset-inline-start: initial;
z-index: 1;
}

View File

@ -26,6 +26,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"light",
"lock",
"siren",
"script",
"switch",
"valve",
"water_heater",

Some files were not shown because too many files have changed in this diff Show More