20221205.0 (#14562)

This commit is contained in:
Bram Kragten 2022-12-05 22:49:02 +01:00 committed by GitHub
commit bbdb84482a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 419 additions and 147 deletions

View File

@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3.0.0
- uses: dessant/lock-threads@v4.0.0
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"

View File

@ -28,6 +28,7 @@ class HassioDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.header=${this.supervisor.localize("panel.addons")}
>
<hassio-addons

View File

@ -24,7 +24,7 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.2",
"@braintree/sanitize-url": "^6.0.0",
"@codemirror/autocomplete": "^0.19.12",
"@codemirror/commands": "^0.19.8",
"@codemirror/gutter": "^0.19.9",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20221201.1"
version = "20221205.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -0,0 +1,5 @@
// Creates a type predicate function for determining if an array literal includes a given value
export const arrayLiteralIncludes =
<T extends readonly unknown[]>(array: T) =>
(searchElement: unknown, fromIndex?: number): searchElement is T[number] =>
array.includes(searchElement as T[number], fromIndex);

View File

@ -7,10 +7,12 @@ if (__BUILD__ === "latest" && polyfillsLoaded) {
}
// Tuesday, August 10
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj);
export const formatDateWeekdayDay = (
dateObj: Date,
locale: FrontendLocaleData
) => formatDateWeekdayDayMem(locale).format(dateObj);
const formatDateWeekdayMem = memoizeOne(
const formatDateWeekdayDayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
@ -92,3 +94,14 @@ const formatDateYearMem = memoizeOne(
year: "numeric",
})
);
// Monday
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj);
const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
})
);

View File

@ -261,6 +261,11 @@ export const getStates = (
result.push(...state.attributes.activity_list);
}
break;
case "sensor":
if (!attribute && state.attributes.device_class === "enum") {
result.push(...state.attributes.options);
}
break;
case "vacuum":
if (attribute === "fan_speed") {
result.push(...state.attributes.fan_speed_list);

View File

@ -0,0 +1,10 @@
import { addDays, startOfWeek } from "date-fns";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { formatDateWeekday } from "../datetime/format_date";
export const dayNames = memoizeOne((locale: FrontendLocaleData): string[] =>
Array.from({ length: 7 }, (_, d) =>
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
)
);

View File

@ -18,6 +18,7 @@ export type LocalizeKeys =
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
@ -30,7 +31,6 @@ export type LocalizeKeys =
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}`
| `ui.panel.config.helpers.${string}`
| `ui.panel.config.info.${string}`
| `ui.panel.config.logs.${string}`
| `ui.panel.config.lovelace.${string}`

View File

@ -0,0 +1,10 @@
import { addMonths, startOfYear } from "date-fns";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import { formatDateMonth } from "../datetime/format_date";
export const monthNames = memoizeOne((locale: FrontendLocaleData): string[] =>
Array.from({ length: 12 }, (_, m) =>
formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
)
);

View File

@ -40,7 +40,7 @@ import {
formatDateMonth,
formatDateMonthYear,
formatDateShort,
formatDateWeekday,
formatDateWeekdayDay,
formatDateYear,
} from "../../common/datetime/format_date";
import {
@ -92,7 +92,7 @@ _adapters._date.override({
case "hour":
return formatTime(new Date(time), this.options.locale);
case "weekday":
return formatDateWeekday(new Date(time), this.options.locale);
return formatDateWeekdayDay(new Date(time), this.options.locale);
case "date":
return formatDate(new Date(time), this.options.locale);
case "day":

View File

@ -3,7 +3,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/ensure-array";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import {

View File

@ -77,6 +77,10 @@ export class HaChip extends LitElement {
span[role="gridcell"] {
line-height: 14px;
}
:host {
outline: none;
}
`;
}
}

View File

@ -31,7 +31,7 @@ export class HaExpansionPanel extends LitElement {
protected render(): TemplateResult {
return html`
<div class="top">
<div class="top ${classMap({ expanded: this.expanded })}">
<div
id="summary"
@click=${this._toggleContainer}
@ -147,6 +147,12 @@ export class HaExpansionPanel extends LitElement {
.top {
display: flex;
align-items: center;
border-radius: var(--ha-card-border-radius, 12px);
}
.top.expanded {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.top.focused {

View File

@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
.selector=${item.selector}
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${this.disabled || item.disabled}
.disabled=${item.disabled || this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
.context=${this._generateContext(item)}

View File

@ -18,7 +18,7 @@ import { css, CSSResultGroup, html, LitElement, unsafeCSS } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { ensureArray } from "../common/ensure-array";
import { ensureArray } from "../common/array/ensure-array";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import {

View File

@ -25,7 +25,7 @@ import {
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { ensureArray } from "../../common/ensure-array";
import { ensureArray } from "../../common/array/ensure-array";
import { Condition, Trigger } from "../../data/automation";
import {
Action,

View File

@ -1,6 +1,6 @@
import { formatDuration } from "../common/datetime/format_duration";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/ensure-array";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation";

View File

@ -29,6 +29,7 @@ export interface CalendarEventData {
dtstart: string;
dtend: string;
rrule?: string;
description?: string;
}
export interface CalendarEventMutableParams {
@ -36,6 +37,7 @@ export interface CalendarEventMutableParams {
dtstart: string;
dtend: string;
rrule?: string;
description?: string;
}
// The scope of a delete/update for a recurring event
@ -84,6 +86,7 @@ export const fetchCalendarEvents = async (
const eventData: CalendarEventData = {
uid: ev.uid,
summary: ev.summary,
description: ev.description,
dtstart: eventStart,
dtend: eventEnd,
recurrence_id: ev.recurrence_id,

View File

@ -60,7 +60,7 @@ export const getCloudTtsSupportedGenders = (
genders.push([
gender,
gender === "male" || gender === "female"
? localize(`ui.panel.config.cloud.account.tts.${gender}`)
? localize(`ui.components.media-browser.tts.gender_${gender}`)
: gender,
]);
}

View File

@ -210,7 +210,10 @@ export const getCurrentProgress = (stateObj: MediaPlayerEntity): number => {
(Date.now() -
new Date(stateObj.attributes.media_position_updated_at!).getTime()) /
1000.0;
return progress;
// Prevent negative values, so we do not go back to 59:59 at the start
// for example if there are slight clock sync deltas between backend and frontend and
// therefore media_position_updated_at might be slightly larger than Date.now().
return progress < 0 ? 0 : progress;
};
export const computeMediaDescription = (
@ -402,7 +405,13 @@ export const cleanupMediaTitle = (title?: string): string | undefined => {
}
const index = title.indexOf("?authSig=");
return index > 0 ? title.slice(0, index) : title;
let cleanTitle = index > 0 ? title.slice(0, index) : title;
if (cleanTitle.startsWith("http")) {
cleanTitle = decodeURIComponent(cleanTitle.split("/").pop()!);
}
return cleanTitle;
};
/**

View File

@ -267,7 +267,7 @@ export const adjustStatisticsSum = (
return hass.callWS({
type: "recorder/adjust_sum_statistics",
statistic_id,
start_time_iso,
start_time: start_time_iso,
adjustment,
adjustment_unit_of_measurement,
});

View File

@ -15,6 +15,7 @@ import {
Describe,
boolean,
} from "superstruct";
import { arrayLiteralIncludes } from "../common/array/literal-includes";
import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import {
@ -28,11 +29,7 @@ import { BlueprintInput } from "./blueprint";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
export const isMaxMode = (
mode: typeof MODES[number]
): mode is typeof MODES_MAX[number] =>
MODES_MAX.includes(mode as typeof MODES_MAX[number]);
export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
export const baseActionStruct = object({
alias: optional(string()),

View File

@ -1,6 +1,6 @@
import { formatDuration } from "../common/datetime/format_duration";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/ensure-array";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name";
import { isTemplate } from "../common/string/has-template";
import { HomeAssistant } from "../types";

View File

@ -39,6 +39,15 @@ export const addItem = (
name,
});
export const removeItem = (
hass: HomeAssistant,
item_id: string
): Promise<ShoppingListItem> =>
hass.callWS({
type: "shopping_list/items/remove",
item_id,
});
export const reorderItems = (
hass: HomeAssistant,
itemIds: string[]

View File

@ -14,7 +14,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateWeekday } from "../../../common/datetime/format_date";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon";
@ -170,7 +170,7 @@ class MoreInfoWeather extends LitElement {
`
: html`
<div class="main">
${formatDateWeekday(
${formatDateWeekdayDay(
new Date(item.datetime),
this.hass.locale
)}

View File

@ -3,12 +3,15 @@ import { mdiCalendarClock, mdiClose } from "@mdi/js";
import { addDays, isSameDay } from "date-fns/esm";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { RRule } from "rrule";
import { RRule, Weekday } from "rrule";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { formatTime } from "../../common/datetime/format_time";
import { fireEvent } from "../../common/dom/fire_event";
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
import { isDate } from "../../common/string/is_date";
import { dayNames } from "../../common/translations/day_names";
import { monthNames } from "../../common/translations/month_names";
import "../../components/entity/state-info";
import "../../components/ha-date-input";
import "../../components/ha-time-input";
@ -84,8 +87,13 @@ class DialogCalendarEventDetail extends LitElement {
<div class="value">
${this._formatDateRange()}<br />
${this._data!.rrule
? this._renderRruleAsText(this._data.rrule)
? this._renderRRuleAsText(this._data.rrule)
: ""}
${this._data.description
? html`<br />
<div class="description">${this._data.description}</div>
<br />`
: html``}
</div>
</div>
@ -108,7 +116,8 @@ class DialogCalendarEventDetail extends LitElement {
${this.hass.localize("ui.components.calendar.event.delete")}
</mwc-button>
`
: ""}${this._params.canEdit
: ""}
${this._params.canEdit
? html`<mwc-button
slot="primaryAction"
@click=${this._editEvent}
@ -121,17 +130,59 @@ class DialogCalendarEventDetail extends LitElement {
`;
}
private _renderRruleAsText(value: string) {
// TODO: Make sure this handles translations
try {
const readableText =
value === "" ? "" : RRule.fromString(`RRULE:${value}`).toText();
return html`<div id="text">${readableText}</div>`;
} catch (e) {
private _renderRRuleAsText(value: string) {
if (!value) {
return "";
}
try {
const rule = RRule.fromString(`RRULE:${value}`);
if (rule.isFullyConvertibleToText()) {
return html`<div id="text">
${capitalizeFirstLetter(
rule.toText(
this._translateRRuleElement,
{
dayNames: dayNames(this.hass.locale),
monthNames: monthNames(this.hass.locale),
tokens: {},
},
this._formatDate
)
)}
</div>`;
}
return html`<div id="text">Cannot convert recurrence rule</div>`;
} catch (e) {
return "Error while processing the rule";
}
}
private _translateRRuleElement = (id: string | number | Weekday): string => {
if (typeof id === "string") {
return this.hass.localize(`ui.components.calendar.event.rrule.${id}`);
}
return "";
};
private _formatDate = (year: number, month: string, day: number): string => {
if (!year || !month || !day) {
return "";
}
// Build date so we can then format it
const date = new Date();
date.setFullYear(year);
// As input we already get the localized month name, so we now unfortunately
// need to convert it back to something Date can work with. The already localized
// months names are a must in the RRule.Language structure (an empty string[] would
// mean we get undefined months input in this method here).
date.setMonth(monthNames(this.hass.locale).indexOf(month));
date.setDate(day);
return formatDate(date, this.hass.locale);
};
private _formatDateRange() {
const start = new Date(this._data!.dtstart);
// All day events should be displayed as a day earlier
@ -227,7 +278,7 @@ class DialogCalendarEventDetail extends LitElement {
ha-svg-icon {
width: 40px;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-end: 16px;
margin-inline-start: initial;
direction: var(--direction);
vertical-align: top;
@ -235,6 +286,11 @@ class DialogCalendarEventDetail extends LitElement {
.field {
display: flex;
}
.description {
color: var(--secondary-text-color);
max-width: 300px;
overflow-wrap: break-word;
}
`,
];
}

View File

@ -7,6 +7,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isDate } from "../../common/string/is_date";
import "../../components/ha-date-input";
import "../../components/ha-textarea";
import "../../components/ha-time-input";
import {
Calendar,
@ -42,6 +43,8 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _summary = "";
@state() private _description = "";
@state() private _rrule?: string;
@state() private _allDay = false;
@ -73,7 +76,11 @@ class DialogCalendarEventEditor extends LitElement {
}
} else {
this._allDay = false;
this._dtstart = startOfHour(new Date());
// If we have been provided a selected date (e.g. based on the currently displayed
// day in a calendar view), use that as the starting value.
this._dtstart = startOfHour(
params.selectedDate ? params.selectedDate : new Date()
);
this._dtend = addHours(this._dtstart, 1);
}
}
@ -123,6 +130,15 @@ class DialogCalendarEventEditor extends LitElement {
error-message=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textarea
class="description"
name="description"
.label=${this.hass.localize(
"ui.components.calendar.event.description"
)}
@change=${this._handleDescriptionChanged}
autogrow
></ha-textarea>
<ha-combo-box
name="calendar"
.hass=${this.hass}
@ -189,6 +205,7 @@ class DialogCalendarEventEditor extends LitElement {
</div>
<ha-recurrence-rule-editor
.locale=${this.hass.locale}
.timezone=${this.hass.config.time_zone}
.value=${this._rrule || ""}
@value-changed=${this._handleRRuleChanged}
>
@ -247,6 +264,10 @@ class DialogCalendarEventEditor extends LitElement {
this._summary = ev.target.value;
}
private _handleDescriptionChanged(ev) {
this._description = ev.target.value;
}
private _handleRRuleChanged(ev) {
this._rrule = ev.detail.value;
}
@ -286,6 +307,7 @@ class DialogCalendarEventEditor extends LitElement {
);
const data: CalendarEventMutableParams = {
summary: this._summary,
description: this._description,
rrule: this._rrule,
dtstart: "",
dtend: "",
@ -308,6 +330,13 @@ class DialogCalendarEventEditor extends LitElement {
}
private async _createEvent() {
if (!this._summary || !this._calendarId) {
this._error = this.hass.localize(
"ui.components.calendar.event.not_all_required_fields"
);
return;
}
this._submitting = true;
try {
await createCalendarEvent(
@ -385,6 +414,7 @@ class DialogCalendarEventEditor extends LitElement {
this._dtstart = undefined;
this._dtend = undefined;
this._summary = "";
this._description = "";
this._rrule = undefined;
}
@ -395,9 +425,16 @@ class DialogCalendarEventEditor extends LitElement {
state-info {
line-height: 40px;
}
ha-textfield {
ha-alert {
display: block;
margin-bottom: 24px;
margin-bottom: 16px;
}
ha-textfield,
ha-textarea {
display: block;
}
ha-textarea {
margin-bottom: 16px;
}
ha-formfield {
display: block;
@ -430,12 +467,11 @@ class DialogCalendarEventEditor extends LitElement {
}
ha-combo-box {
display: block;
margin-bottom: 24px;
}
ha-svg-icon {
width: 40px;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-end: 16px;
margin-inline-start: initial;
direction: var(--direction);
vertical-align: top;

View File

@ -29,28 +29,29 @@ import {
} from "lit";
import { property, state } from "lit/decorators";
import memoize from "memoize-one";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import { supportsFeature } from "../../common/entity/supports-feature";
import { LocalizeFunc } from "../../common/translations/localize";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-button-toggle-group";
import "../../components/ha-fab";
import "../../components/ha-icon-button-prev";
import "../../components/ha-icon-button-next";
import "../../components/ha-icon-button-prev";
import type {
Calendar as CalendarData,
CalendarEvent,
} from "../../data/calendar";
import { CalendarEntityFeature } from "../../data/calendar";
import { haStyle } from "../../resources/styles";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
CalendarViewChanged,
FullCalendarView,
HomeAssistant,
ToggleButton,
} from "../../types";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { supportsFeature } from "../../common/entity/supports-feature";
import { showCalendarEventDetailDialog } from "./show-dialog-calendar-event-detail";
import type {
Calendar as CalendarData,
CalendarEvent,
} from "../../data/calendar";
import { CalendarEntityFeature } from "../../data/calendar";
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
declare global {
@ -62,15 +63,6 @@ declare global {
}
}
const getListWeekRange = (currentDate: Date): { start: Date; end: Date } => {
const startDate = new Date(currentDate.valueOf());
const endDate = new Date(currentDate.valueOf());
endDate.setDate(endDate.getDate() + 7);
return { start: startDate, end: endDate };
};
const defaultFullCalendarConfig: CalendarOptions = {
headerToolbar: false,
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
@ -80,19 +72,13 @@ const defaultFullCalendarConfig: CalendarOptions = {
eventDisplay: "list-item",
locales: allLocales,
views: {
list: {
visibleRange: getListWeekRange,
listWeek: {
type: "list",
duration: { days: 7 },
},
},
};
const viewButtons: ToggleButton[] = [
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
{ label: "List View", value: "list", iconPath: mdiViewAgenda },
];
export class HAFullCalendar extends LitElement {
public hass!: HomeAssistant;
@ -106,12 +92,15 @@ export class HAFullCalendar extends LitElement {
"dayGridMonth",
"dayGridWeek",
"dayGridDay",
"listWeek",
];
@property() public initialView: FullCalendarView = "dayGridMonth";
private calendar?: Calendar;
private _viewButtons?: ToggleButton[];
@state() private _activeView = this.initialView;
public updateSize(): void {
@ -119,7 +108,10 @@ export class HAFullCalendar extends LitElement {
}
protected render(): TemplateResult {
const viewToggleButtons = this._viewToggleButtons(this.views);
const viewToggleButtons = this._viewToggleButtons(
this.views,
this.hass.localize
);
return html`
${this.calendar
@ -276,8 +268,19 @@ export class HAFullCalendar extends LitElement {
}
private _createEvent(_info) {
// Logic for selectedDate: In week and day view, use the start of the week or the selected day.
// If we are in month view, we only use the start of the month, if we are not showing the
// current actual month, as for that one the current day is automatically highlighted and
// defaulting to a different day in the event creation dialog would be weird.
showCalendarEventEditDialog(this, {
calendars: this._mutableCalendars,
selectedDate:
this._activeView === "dayGridWeek" ||
this._activeView === "dayGridDay" ||
(this._activeView === "dayGridMonth" &&
this.calendar!.view.currentStart.getMonth() !== new Date().getMonth())
? this.calendar!.view.currentStart
: undefined,
updated: () => {
this._fireViewChanged();
},
@ -338,11 +341,44 @@ export class HAFullCalendar extends LitElement {
});
}
private _viewToggleButtons = memoize((views) =>
viewButtons.filter((button) =>
private _viewToggleButtons = memoize((views, localize: LocalizeFunc) => {
if (!this._viewButtons) {
this._viewButtons = [
{
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridMonth"
),
value: "dayGridMonth",
iconPath: mdiViewModule,
},
{
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridWeek"
),
value: "dayGridWeek",
iconPath: mdiViewWeek,
},
{
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.dayGridDay"
),
value: "dayGridDay",
iconPath: mdiViewDay,
},
{
label: localize(
"ui.panel.lovelace.editor.card.calendar.views.listWeek"
),
value: "listWeek",
iconPath: mdiViewAgenda,
},
];
}
return this._viewButtons.filter((button) =>
views.includes(button.value as FullCalendarView)
)
);
);
});
static get styles(): CSSResultGroup {
return [
@ -380,7 +416,7 @@ export class HAFullCalendar extends LitElement {
}
a {
color: var(--primary-text-color);
color: var(--primary-color);
}
.controls {
@ -442,6 +478,12 @@ export class HAFullCalendar extends LitElement {
.fc-theme-standard .fc-scrollgrid {
border: 1px solid var(--divider-color);
border-radius: var(--mdc-shape-small, 4px);
}
.fc-theme-standard td {
border-bottom-left-radius: var(--mdc-shape-small, 4px);
border-bottom-right-radius: var(--mdc-shape-small, 4px);
}
.fc-scrollgrid-section-header td {
@ -449,9 +491,10 @@ export class HAFullCalendar extends LitElement {
}
th.fc-col-header-cell.fc-day {
color: var(--secondary-text-color);
background-color: var(--table-header-background-color);
color: var(--primary-text-color);
font-size: 11px;
font-weight: 400;
font-weight: bold;
text-transform: uppercase;
}

View File

@ -34,6 +34,8 @@ export class RecurrenceRuleEditor extends LitElement {
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property() public timezone?: string;
@state() private _computedRRule = "";
@state() private _freq?: RepeatFrequency = "none";
@ -292,6 +294,7 @@ export class RecurrenceRuleEditor extends LitElement {
byweekday: ruleByWeekDay(this._weekday),
count: this._count,
until: this._until,
tzid: this.timezone,
};
const contentline = RRule.optionsToString(options);
return contentline.slice(6); // Strip "RRULE:" prefix

View File

@ -2,7 +2,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { Calendar, CalendarEventData } from "../../data/calendar";
export interface CalendarEventDetailDialogParams {
calendars: Calendar[]; // When creating new events, is the list of events that support creation
calendars: Calendar[]; // When creating new events, is the list of calendar entities that support creation
calendarId?: string;
entry?: CalendarEventData;
canDelete?: boolean;

View File

@ -2,8 +2,9 @@ import { fireEvent } from "../../common/dom/fire_event";
import { Calendar, CalendarEventData } from "../../data/calendar";
export interface CalendarEventEditDialogParams {
calendars: Calendar[]; // When creating new events, is the list of events that support creation
calendars: Calendar[]; // When creating new events, is the list of calendar entities that support creation
calendarId?: string;
selectedDate?: Date; // When provided is used as the pre-filled date for the event creation dialog
entry?: CalendarEventData;
canDelete?: boolean;
updated: () => void;

View File

@ -2,7 +2,7 @@ import { mdiDelete, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ensureArray } from "../../../../../common/ensure-array";
import { ensureArray } from "../../../../../common/array/ensure-array";
import "../../../../../components/ha-icon-button";
import { Condition } from "../../../../../data/automation";
import { Action, ChooseAction } from "../../../../../data/script";

View File

@ -1,22 +1,13 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, object, optional, string } from "superstruct";
import { assert } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import { entityIdOrAll } from "../../../../../common/structs/is-entity-id";
import "../../../../../components/ha-service-control";
import { ServiceAction } from "../../../../../data/script";
import { ServiceAction, serviceActionStruct } from "../../../../../data/script";
import type { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
const actionStruct = object({
alias: optional(string()),
service: optional(string()),
entity_id: optional(entityIdOrAll()),
target: optional(any()),
data: optional(any()),
});
@customElement("ha-automation-action-service")
export class HaServiceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -38,7 +29,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
return;
}
try {
assert(this.action, actionStruct);
assert(this.action, serviceActionStruct);
} catch (err: any) {
fireEvent(this, "ui-mode-not-available", err);
return;

View File

@ -10,7 +10,7 @@ import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import "../../../../../components/ha-duration-input";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { TimeChangedEvent } from "../../../../../components/ha-base-time-input";
import { ensureArray } from "../../../../../common/ensure-array";
import { ensureArray } from "../../../../../common/array/ensure-array";
@customElement("ha-automation-action-wait_for_trigger")
export class HaWaitForTriggerAction

View File

@ -3,7 +3,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { ensureArray } from "../../../../../common/ensure-array";
import { ensureArray } from "../../../../../common/array/ensure-array";
import "../../../../../components/ha-select";
import type {
AutomationConfig,

View File

@ -11,7 +11,7 @@ import {
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/ensure-array";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import { StateTrigger } from "../../../../../data/automation";

View File

@ -1,3 +1,4 @@
import { arrayLiteralIncludes } from "../../../common/array/literal-includes";
import type { Counter } from "../../../data/counter";
import type { InputBoolean } from "../../../data/input_boolean";
import type { InputButton } from "../../../data/input_button";
@ -18,7 +19,10 @@ export const HELPER_DOMAINS = [
"counter",
"timer",
"schedule",
];
] as const;
export type HelperDomain = typeof HELPER_DOMAINS[number];
export const isHelperDomain = arrayLiteralIncludes(HELPER_DOMAINS);
export type Helper =
| InputBoolean

View File

@ -25,7 +25,7 @@ import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-c
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { Helper } from "./const";
import { Helper, HelperDomain } from "./const";
import "./forms/ha-counter-form";
import "./forms/ha-input_boolean-form";
import "./forms/ha-input_button-form";
@ -37,7 +37,18 @@ import "./forms/ha-schedule-form";
import "./forms/ha-timer-form";
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
const HELPERS = {
type HelperCreators = {
[domain in HelperDomain]: (
hass: HomeAssistant,
// Not properly typed because there is currently a mismatch for this._item between:
// 1. Type passed to form should be Helper
// 2. Type received by creator should be MutableParams version
// The two are not compatible.
params: any
) => Promise<Helper>;
};
const HELPERS: HelperCreators = {
input_boolean: createInputBoolean,
input_button: createInputButton,
input_text: createInputText,
@ -57,7 +68,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _opened = false;
@state() private _domain?: string;
@state() private _domain?: HelperDomain;
@state() private _error?: string;
@ -127,7 +138,7 @@ export class DialogHelperDetail extends LitElement {
} else {
const items: [string, string][] = [];
for (const helper of Object.keys(HELPERS)) {
for (const helper of Object.keys(HELPERS) as (keyof typeof HELPERS)[]) {
items.push([
helper,
this.hass.localize(`ui.panel.config.helpers.types.${helper}`) ||

View File

@ -208,7 +208,10 @@ class HaScheduleForm extends LitElement {
private get _events() {
const events: any[] = [];
const currentDay = new Date().getDay();
const baseDay = currentDay === 0 ? 7 : currentDay;
const baseDay =
currentDay === 0 && firstWeekdayIndex(this.hass.locale) === 1
? 7
: currentDay;
for (const [i, day] of weekdays.entries()) {
if (!this[`_${day}`].length) {
@ -216,8 +219,11 @@ class HaScheduleForm extends LitElement {
}
this[`_${day}`].forEach((item: ScheduleDay, index: number) => {
// Add 7 to 0 because we start the calendar on Monday
const distance = i - baseDay + (i === 0 ? 7 : 0);
// Add 7 to 0 because we start the calendar on Monday, except when the locale says otherwise (firstWeekdayIndex() != 1)
const distance =
i -
baseDay +
(i === 0 && firstWeekdayIndex(this.hass.locale) === 1 ? 7 : 0);
const start = new Date();
start.setDate(start.getDate() + distance);
@ -388,6 +394,8 @@ class HaScheduleForm extends LitElement {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
--fc-border-color: var(--divider-color);
--fc-event-border-color: var(--divider-color);
}
.fc-scroller {
overflow-x: visible !important;
@ -395,6 +403,18 @@ class HaScheduleForm extends LitElement {
.fc-v-event .fc-event-time {
white-space: inherit;
}
a {
color: inherit !important;
}
th.fc-col-header-cell.fc-day {
background-color: var(--table-header-background-color);
color: var(--primary-text-color);
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
`,
];
}

View File

@ -35,7 +35,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { HELPER_DOMAINS } from "./const";
import { HelperDomain, isHelperDomain } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
// This groups items by a key but only returns last entry per key.
@ -118,7 +118,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
sortable: true,
width: "25%",
filterable: true,
template: (type, row) =>
template: (type: HelperDomain, row) =>
row.configEntry
? domainToName(localize, type)
: html`
@ -243,7 +243,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
if (!domain) {
return;
}
if (HELPER_DOMAINS.includes(domain)) {
if (isHelperDomain(domain)) {
showHelperDetailDialog(this, {
domain,
});
@ -330,7 +330,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const newStates = Object.values(this.hass!.states).filter(
(entity) =>
extraEntities.has(entity.entity_id) ||
HELPER_DOMAINS.includes(computeStateDomain(entity))
isHelperDomain(computeStateDomain(entity))
);
if (

View File

@ -1,10 +1,11 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
import { HelperDomain } from "./const";
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
export interface ShowDialogHelperDetailParams {
domain?: string;
domain?: HelperDomain;
// Only used for config entries
dialogClosedCallback?: DataEntryFlowDialogParams["dialogClosedCallback"];
}

View File

@ -70,7 +70,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "../helpers/const";
import { isHelperDomain } from "../helpers/const";
import "./ha-config-flow-card";
import "./ha-ignored-config-entry-card";
import "./ha-integration-card";
@ -785,7 +785,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
// If not an integration or supported brand, try helper else show alert
if (HELPER_DOMAINS.includes(domain)) {
if (isHelperDomain(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});

View File

@ -548,6 +548,9 @@ export class HaIntegrationCard extends LitElement {
}
private async _handleDisableDebugLogging(ev: MouseEvent) {
// Stop propagation since otherwise we end up here twice while we await the log level change
// and trigger two identical debug log downloads.
ev.stopPropagation();
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
.configEntry;
const integration = configEntry.domain;

View File

@ -82,7 +82,7 @@ class HaPanelDevMqtt extends LitElement {
}
private _handleTopic(ev: CustomEvent) {
this.topic = ev.detail.value;
this.topic = (ev.target! as any).value;
if (localStorage && this.inited) {
localStorage["panel-dev-mqtt-topic"] = this.topic;
}

View File

@ -18,7 +18,7 @@ import { css, html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { LocalStorage } from "../../common/decorators/local-storage";
import { ensureArray } from "../../common/ensure-array";
import { ensureArray } from "../../common/array/ensure-array";
import { navigate } from "../../common/navigate";
import {
createSearchParam,

View File

@ -1,7 +1,7 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { ensureArray } from "../../common/ensure-array";
import { ensureArray } from "../../common/array/ensure-array";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";

View File

@ -119,8 +119,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
}
const views: FullCalendarView[] = this._veryNarrow
? ["list"]
: ["list", "dayGridMonth", "dayGridDay"];
? ["listWeek"]
: ["dayGridMonth", "dayGridDay", "listWeek"];
return html`
<ha-card>

View File

@ -8,32 +8,33 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { guard } from "lit/directives/guard";
import { repeat } from "lit/directives/repeat";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import "../../../components/ha-checkbox";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import {
addItem,
clearItems,
fetchItems,
removeItem,
reorderItems,
ShoppingListItem,
updateItem,
} from "../../../data/shopping-list";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { SensorCardConfig, ShoppingListCardConfig } from "./types";
import type { HaTextField } from "../../../components/ha-textfield";
import {
loadSortable,
SortableInstance,
} from "../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { SensorCardConfig, ShoppingListCardConfig } from "./types";
@customElement("hui-shopping-list-card")
class HuiShoppingListCard
@ -264,9 +265,14 @@ class HuiShoppingListCard
}
private _saveEdit(ev): void {
updateItem(this.hass!, ev.target.itemId, {
name: ev.target.value,
}).catch(() => this._fetchData());
// If name is not empty, update the item otherwise remove it
if (ev.target.value) {
updateItem(this.hass!, ev.target.itemId, {
name: ev.target.value,
}).catch(() => this._fetchData());
} else {
removeItem(this.hass!, ev.target.itemId).catch(() => this._fetchData());
}
ev.target.blur();
}

View File

@ -31,7 +31,7 @@ const cardConfigStruct = assign(
})
);
const views = ["dayGridMonth", "dayGridDay", "list"] as const;
const views = ["dayGridMonth", "dayGridDay", "listWeek"] as const;
@customElement("hui-calendar-card-editor")
export class HuiCalendarCardEditor

View File

@ -20,7 +20,7 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { ensureArray } from "../../../../common/ensure-array";
import { ensureArray } from "../../../../common/array/ensure-array";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { deepEqual } from "../../../../common/util/deep-equal";
import {

View File

@ -212,7 +212,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
const mediaDescription = computeMediaDescription(stateObj);
const mediaDuration = formatMediaTime(stateObj.attributes.media_duration);
const mediaTitleClean = cleanupMediaTitle(
stateObj.attributes.media_title || ""
stateObj.attributes.media_title || stateObj.attributes.media_content_id
);
const mediaArt =
stateObj.attributes.entity_picture_local ||
@ -232,7 +232,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
<hui-marquee
.text=${mediaTitleClean ||
mediaDescription ||
cleanupMediaTitle(stateObj.attributes.media_content_id) ||
(stateObj.state !== "playing" && stateObj.state !== "on"
? this.hass.localize(`ui.card.media_player.nothing_playing`)
: "")}

View File

@ -75,6 +75,7 @@ export const derivedStyles = {
"paper-listbox-background-color": "var(--card-background-color)",
"paper-item-icon-color": "var(--state-icon-color)",
"paper-item-icon-active-color": "var(--state-icon-active-color)",
"table-header-background-color": "var(--input-fill-color)",
"table-row-background-color": "var(--primary-background-color)",
"table-row-alternative-background-color": "var(--secondary-background-color)",
"paper-slider-knob-color": "var(--slider-color)",

View File

@ -642,6 +642,7 @@
"all_day": "All Day",
"start": "Start",
"end": "End",
"not_all_required_fields": "Not all required fields are filled in.",
"confirm_delete": {
"delete": "Delete Event",
"delete_this": "Delete Only This Event",
@ -666,7 +667,32 @@
"daily": "days"
}
},
"summary": "Summary"
"rrule": {
"every": "every",
"years": "years",
"year": "year",
"months": "months",
"month": "month",
"weeks": "weeks",
"week": "week",
"weekdays": "weekdays",
"weekday": "weekday",
"days": "days",
"day": "day",
"until": "until",
"for": "for",
"in": "in",
"on": "on",
"on the": "on the",
"and": "and",
"or": "or",
"at": "at",
"last": "last",
"time": "time",
"times": "times"
},
"summary": "Summary",
"description": "Description"
}
},
"attributes": {
@ -2579,8 +2605,6 @@
"info": "Bring personality to your home by having it speak to you by using our Text-to-Speech services. You can use this in automations and scripts by using the {service} service.",
"default_language": "Default language to use",
"default_gender": "Default gender to use",
"male": "Male",
"female": "Female",
"try": "Try",
"dialog": {
"header": "Try Text to Speech",
@ -4009,8 +4033,9 @@
"calendar_entities": "Calendar Entities",
"views": {
"dayGridMonth": "Month",
"dayGridWeek": "Week",
"dayGridDay": "Day",
"list": "List"
"listWeek": "List (7 days)"
}
},
"conditional": {

View File

@ -122,7 +122,7 @@ export type FullCalendarView =
| "dayGridMonth"
| "dayGridWeek"
| "dayGridDay"
| "list";
| "listWeek";
export interface ToggleButton {
label: string;

View File

@ -1358,10 +1358,10 @@ __metadata:
languageName: node
linkType: hard
"@braintree/sanitize-url@npm:^5.0.2":
version: 5.0.2
resolution: "@braintree/sanitize-url@npm:5.0.2"
checksum: c033f9a0e6dd6fbd4022df2d3916a278510f759971b1e8ab278b3ce1123a3816d5fdd9d84c5c9fbcd6c94c05f8421c4c669f110c8db67eaf58f3018825af514e
"@braintree/sanitize-url@npm:^6.0.0":
version: 6.0.2
resolution: "@braintree/sanitize-url@npm:6.0.2"
checksum: 6a9dfd4081cc96516eeb281d1a83d3b5f1ad3d2837adf968fcc2ba18889ee833554f9c641b4083c36d3360a932e4504ddf25b0b51e9933c3742622df82cf7c9a
languageName: node
linkType: hard
@ -5729,11 +5729,11 @@ __metadata:
linkType: hard
"async@npm:^2.6.2":
version: 2.6.2
resolution: "async@npm:2.6.2"
version: 2.6.4
resolution: "async@npm:2.6.4"
dependencies:
lodash: ^4.17.11
checksum: e5e90a3bcc4d9bf964bfc6b77d63b8f5bee8c14e9a51c3317dbcace44d5b6b1fe01cd4fd347449704a107da7fcd25e1382ee8545957b2702782ae720605cf7a4
lodash: ^4.17.14
checksum: a52083fb32e1ebe1d63e5c5624038bb30be68ff07a6c8d7dfe35e47c93fc144bd8652cbec869e0ac07d57dde387aa5f1386be3559cdee799cb1f789678d88e19
languageName: node
linkType: hard
@ -7026,9 +7026,9 @@ __metadata:
linkType: hard
"decode-uri-component@npm:^0.2.0":
version: 0.2.0
resolution: "decode-uri-component@npm:0.2.0"
checksum: f3749344ab9305ffcfe4bfe300e2dbb61fc6359e2b736812100a3b1b6db0a5668cba31a05e4b45d4d63dbf1a18dfa354cd3ca5bb3ededddabb8cd293f4404f94
version: 0.2.2
resolution: "decode-uri-component@npm:0.2.2"
checksum: 95476a7d28f267292ce745eac3524a9079058bbb35767b76e3ee87d42e34cd0275d2eb19d9d08c3e167f97556e8a2872747f5e65cbebcac8b0c98d83e285f139
languageName: node
linkType: hard
@ -9282,7 +9282,7 @@ fsevents@^1.2.7:
"@babel/plugin-syntax-top-level-await": ^7.14.5
"@babel/preset-env": ^7.20.2
"@babel/preset-typescript": ^7.18.6
"@braintree/sanitize-url": ^5.0.2
"@braintree/sanitize-url": ^6.0.0
"@codemirror/autocomplete": ^0.19.12
"@codemirror/commands": ^0.19.8
"@codemirror/gutter": ^0.19.9
@ -11294,7 +11294,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"lodash@npm:^4.17.11, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
"lodash@npm:^4.17.14, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7