Merge dev to bump action versions

This commit is contained in:
Steve Repsher 2023-01-09 14:28:18 +00:00
commit 2fdb6f1241
No known key found for this signature in database
GPG Key ID: 776C4F2DACF6131B
130 changed files with 2140 additions and 1065 deletions

View File

@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -60,12 +60,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -32,13 +32,13 @@ jobs:
sha: ${{ steps.get-sha.outputs.sha }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
# Checkout PR head instead of merge commit
# Use ref, not SHA, so reruns get the dedupe commit
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -81,11 +81,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: ${{ needs.dedupe.outputs.sha }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -109,11 +109,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: ${{ needs.dedupe.outputs.sha }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -131,11 +131,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: ${{ needs.dedupe.outputs.sha }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -153,11 +153,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: ${{ needs.dedupe.outputs.sha }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
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

@ -23,12 +23,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: dev
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn
@ -61,12 +61,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
with:
ref: master
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -17,10 +17,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -22,10 +22,10 @@ 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@v3.2.0
uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
@ -29,7 +29,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@ -35,7 +35,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.2.0
uses: actions/checkout@v3.3.0
- name: Upload Translations
run: |

View File

@ -98,7 +98,9 @@ const alerts: {
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
><img
alt="Home Assistant logo"
src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{

View File

@ -29,7 +29,9 @@ class HassioAddonRepositoryEl extends LitElement {
if (filter) {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
});
protected render(): TemplateResult {

View File

@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement {
? html`
<img
class="logo"
alt=""
src="/api/hassio/addons/${this.addon.slug}/logo"
/>
`

View File

@ -35,7 +35,13 @@ class HassioAddons extends LitElement {
</ha-card>
`
: this.supervisor.addon.addons
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) =>
caseInsensitiveStringCompare(
a.name,
b.name,
this.hass.locale.language
)
)
.map(
(addon) => html`
<ha-card

View File

@ -15,7 +15,12 @@ import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
(
showAdvanced: boolean,
hardware: HassioHardwareInfo,
filter: string,
language: string
) =>
hardware.devices
.filter(
(device) =>
@ -28,7 +33,7 @@ const _filterDevices = memoizeOne(
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name))
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@customElement("dialog-hassio-hardware")
@ -56,7 +61,8 @@ class HassioHardwareDialog extends LitElement {
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
(this._filter || "").toLowerCase(),
this.hass.locale.language
);
return html`

View File

@ -68,7 +68,9 @@ class HassioRepositoriesDialog extends LitElement {
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
repo.slug !== "5c53de3b" // The ESPHome repository
)
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
)
);
private _filteredUsedRepositories = memoizeOne(

View File

@ -25,18 +25,13 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^6.0.0",
"@codemirror/autocomplete": "^0.19.12",
"@codemirror/commands": "^0.19.8",
"@codemirror/gutter": "^0.19.9",
"@codemirror/highlight": "^0.19.7",
"@codemirror/history": "^0.19.2",
"@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.6",
"@codemirror/state": "^0.19.6",
"@codemirror/stream-parser": "^0.19.5",
"@codemirror/text": "^0.19.6",
"@codemirror/view": "^0.19.40",
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.1.3",
"@codemirror/language": "^6.3.2",
"@codemirror/legacy-modes": "^6.3.1",
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.1",
"@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40",
@ -49,6 +44,7 @@
"@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0",
"@fullcalendar/timegrid": "5.9.0",
"@lezer/highlight": "^1.1.3",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@material/chips": "14.0.0-canary.261f2db59.0",
@ -75,8 +71,8 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "7.0.96",
"@mdi/svg": "7.0.96",
"@mdi/js": "7.1.96",
"@mdi/svg": "7.1.96",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
@ -97,7 +93,7 @@
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0",
"@vue/web-component-wrapper": "^1.3.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.1.0",
@ -141,12 +137,12 @@
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4",
"xss": "^1.0.9"
},
"devDependencies": {
@ -202,7 +198,7 @@
"eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2",
"fancy-log": "^2.0.0",
"fs-extra": "^7.0.1",
"fs-extra": "^11.1.0",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"gulp-flatmap": "^1.0.2",
@ -222,7 +218,7 @@
"merge-stream": "^1.0.1",
"mocha": "^8.4.0",
"object-hash": "^2.0.3",
"open": "^7.0.4",
"open": "^8.4.0",
"pinst": "^3.0.0",
"prettier": "^2.8.1",
"require-dir": "^1.2.0",
@ -237,7 +233,7 @@
"tar": "^6.1.11",
"terser-webpack-plugin": "^5.2.4",
"ts-lit-plugin": "^1.2.1",
"typescript": "^4.9.3",
"typescript": "^4.9.4",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.55.1",
@ -245,7 +241,7 @@
"webpack-dev-server": "^4.3.0",
"webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3",
"workbox-build": "^6.4.2"
"workbox-build": "^6.5.4"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {

View File

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

View File

@ -201,6 +201,7 @@ export const DOMAINS_WITH_CARD = [
export const SENSOR_ENTITIES = [
"sensor",
"binary_sensor",
"calendar",
"camera",
"device_tracker",
"weather",

View File

@ -1,4 +1,15 @@
export const stringCompare = (a: string, b: string) => {
import memoizeOne from "memoize-one";
const collator = memoizeOne(
(language: string | undefined) => new Intl.Collator(language)
);
const caseInsensitiveCollator = memoizeOne(
(language: string | undefined) =>
new Intl.Collator(language, { sensitivity: "accent" })
);
const fallbackStringCompare = (a: string, b: string) => {
if (a < b) {
return -1;
}
@ -9,5 +20,28 @@ export const stringCompare = (a: string, b: string) => {
return 0;
};
export const caseInsensitiveStringCompare = (a: string, b: string) =>
stringCompare(a.toLowerCase(), b.toLowerCase());
export const stringCompare = (
a: string,
b: string,
language: string | undefined = undefined
) => {
// @ts-ignore
if (Intl?.Collator) {
return collator(language).compare(a, b);
}
return fallbackStringCompare(a, b);
};
export const caseInsensitiveStringCompare = (
a: string,
b: string,
language: string | undefined = undefined
) => {
// @ts-ignore
if (Intl?.Collator) {
return caseInsensitiveCollator(language).compare(a, b);
}
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
};

View File

@ -266,14 +266,16 @@ export const getCountryOptions = memoizeOne((language?: string) => {
value: country,
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
}));
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCountryListEl = () => {
export const createCountryListEl = (language?: string) => {
const list = document.createElement("datalist");
list.id = "countries";
const options = getCountryOptions();
const options = getCountryOptions(language);
for (const country of options) {
const option = document.createElement("option");
option.value = country.value;

View File

@ -157,7 +157,7 @@ export const CURRENCIES = [
"XPF",
"YER",
"ZAR",
"ZMK",
"ZMW",
"ZWL",
];
@ -173,14 +173,16 @@ export const getCurrencyOptions = memoizeOne((language?: string) => {
value: currency,
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
}));
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
export const createCurrencyListEl = () => {
export const createCurrencyListEl = (language: string) => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of getCurrencyOptions()) {
for (const currency of getCurrencyOptions(language)) {
const option = document.createElement("option");
option.value = currency.value;
option.innerText = currency.label;

View File

@ -5,7 +5,6 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
const Component = Vue.extend({
props: {
@ -47,35 +46,26 @@ const Component = Vue.extend({
},
},
render(createElement) {
// @ts-ignore
// @ts-expect-error
return createElement(DateRangePicker, {
props: {
// @ts-ignore
"time-picker": this.timePicker,
// @ts-ignore
"auto-apply": this.autoApply,
opens: "right",
"show-dropdowns": false,
// @ts-ignore
"time-picker24-hour": this.twentyfourHours,
// @ts-ignore
disabled: this.disabled,
// @ts-ignore
ranges: this.ranges ? {} : false,
"locale-data": {
// @ts-ignore
firstDay: this.firstDay,
},
},
model: {
value: {
// @ts-ignore
startDate: this.startDate,
// @ts-ignore
endDate: this.endDate,
},
callback: (value) => {
// @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value);
},
expression: "dateRange",
@ -106,7 +96,11 @@ const Component = Vue.extend({
},
});
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
// Assertion corrects HTMLElement type from package
const WrappedElement = wrap(
Vue,
Component
) as unknown as CustomElementConstructor;
@customElement("date-range-picker")
class DateRangePickerElement extends WrappedElement {

View File

@ -189,7 +189,8 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
.sort((a, b) =>
stringCompare(
devicesByArea[a].name || "",
devicesByArea[b].name || ""
devicesByArea[b].name || "",
this.hass.locale.language
)
)
.map((key) => devicesByArea[key]);

View File

@ -84,6 +84,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of devices to be excluded.
* @type {Array}
* @attr exclude-devices
*/
@property({ type: Array, attribute: "exclude-devices" })
public excludeDevices?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) public disabled?: boolean;
@ -104,7 +112,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"]
deviceFilter: this["deviceFilter"],
excludeDevices: this["excludeDevices"]
): Device[] => {
if (!devices.length) {
return [
@ -164,6 +173,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
});
}
if (excludeDevices) {
inputDevices = inputDevices.filter(
(device) => !excludeDevices!.includes(device.id)
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
@ -216,7 +231,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return outputDevices;
}
return outputDevices.sort((a, b) =>
stringCompare(a.name || "", b.name || "")
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
);
}
);
@ -258,7 +273,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter
this.deviceFilter,
this.excludeDevices
);
}
}

View File

@ -174,7 +174,8 @@ export class HaEntityPicker extends LitElement {
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
entityB.friendly_name,
this.hass.locale.language
)
);
}
@ -205,7 +206,8 @@ export class HaEntityPicker extends LitElement {
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
entityB.friendly_name,
this.hass.locale.language
)
);

View File

@ -223,6 +223,10 @@ export class HaStateLabelBadge extends LitElement {
if (domainStateKey) {
return this.hass!.localize(`state_badge.${domainStateKey}`);
}
// Person and device tracker state can be zone name
if (domain === "person" || domain === "device_tracker") {
return entityState.state;
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}

View File

@ -177,7 +177,9 @@ export class HaStatisticPicker extends LitElement {
}
if (output.length > 1) {
output.sort((a, b) => stringCompare(a.name || "", b.name || ""));
output.sort((a, b) =>
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
);
}
output.push({

View File

@ -28,7 +28,7 @@ class StateInfo extends LitElement {
const name = computeStateName(this.stateObj);
return html` <state-badge
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}

View File

@ -16,7 +16,11 @@ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
${item.icon
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</mwc-list-item>`;
@ -80,7 +84,9 @@ class HaAddonPicker extends LitElement {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
.sort((a, b) => stringCompare(a.name, b.name));
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(

View File

@ -73,6 +73,14 @@ export class HaAreaPicker extends LitElement {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of areas to be excluded.
* @type {Array}
* @attr exclude-areas
*/
@property({ type: Array, attribute: "exclude-areas" })
public excludeAreas?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@ -109,7 +117,8 @@ export class HaAreaPicker extends LitElement {
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
noAdd: this["noAdd"]
noAdd: this["noAdd"],
excludeAreas: this["excludeAreas"]
): AreaRegistryEntry[] => {
if (!areas.length) {
return [
@ -235,6 +244,12 @@ export class HaAreaPicker extends LitElement {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
if (!outputAreas.length) {
outputAreas = [
{
@ -264,7 +279,7 @@ export class HaAreaPicker extends LitElement {
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this.comboBox as any).items = this._getAreas(
const areas = this._getAreas(
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),
@ -273,8 +288,11 @@ export class HaAreaPicker extends LitElement {
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
this.noAdd,
this.excludeAreas
);
(this.comboBox as any).items = areas;
(this.comboBox as any).filteredItems = areas;
}
}
@ -384,7 +402,8 @@ export class HaAreaPicker extends LitElement {
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
this.noAdd,
this.excludeAreas
);
await this.updateComplete;
await this.comboBox.updateComplete;

View File

@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement {
seconds: this.seconds,
milliseconds: this.milliseconds,
};
if (this.enableDay) {
value.days = this.days;
}
if (this.format === 12) {
value.amPm = this.amPm;
}

View File

@ -46,7 +46,9 @@ class HaBluePrintPicker extends LitElement {
...(blueprint as Blueprint).metadata,
path,
}));
return result.sort((a, b) => stringCompare(a.name, b.name));
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass!.locale.language)
);
});
protected render(): TemplateResult {

View File

@ -4,6 +4,7 @@ import type {
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
@ -72,9 +73,9 @@ export class HaCodeEditor extends ReactiveElement {
if (!this.codemirror || !this._loadedCodeMirror) {
return false;
}
const className = this._loadedCodeMirror.HighlightStyle.get(
const className = this._loadedCodeMirror.highlightingFor(
this.codemirror.state,
this._loadedCodeMirror.tags.comment
[this._loadedCodeMirror.tags.comment]
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
}
@ -136,7 +137,7 @@ export class HaCodeEditor extends ReactiveElement {
private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
const extensions = [
const extensions: Extension[] = [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
@ -152,10 +153,8 @@ export class HaCodeEditor extends ReactiveElement {
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting,
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
@ -227,7 +226,7 @@ export class HaCodeEditor extends ReactiveElement {
return {
from: Number(entityWord.from),
options: states,
span: /^[a-z_]{3,}\.\w*$/,
validFor: /^[a-z_]{3,}\.\w*$/,
};
}
@ -268,7 +267,7 @@ export class HaCodeEditor extends ReactiveElement {
return {
from: Number(match.from),
options: iconItems,
span: /^mdi:\S*$/,
validFor: /^mdi:\S*$/,
};
}

View File

@ -59,6 +59,7 @@ class HaConfigEntryPicker extends LitElement {
>
<span slot="secondary">${item.localized_domain_name}</span>
<img
alt=""
slot="graphic"
src=${brandsUrl({
domain: item.domain,
@ -121,7 +122,8 @@ class HaConfigEntryPicker extends LitElement {
.sort((conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
conf2.localized_domain_name + conf2.title,
this.hass.locale.language
)
);
});

View File

@ -3,10 +3,12 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css";
import { mdiClose } from "@mdi/js";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
export const createCloseHeading = (
hass: HomeAssistant,
title: string | TemplateResult
@ -32,6 +34,14 @@ export class HaDialog extends DialogBase {
return html`<slot name="heading"> ${super.renderHeading()} </slot>`;
}
protected firstUpdated(): void {
super.firstUpdated();
this.suppressDefaultPressSelector = [
this.suppressDefaultPressSelector,
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
}
static override styles = [
styles,
css`

View File

@ -67,6 +67,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@change=${this._valueChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
`;
}

View File

@ -1,8 +1,4 @@
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@ -17,8 +13,7 @@ import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
} from "../../data/device_registry";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
@ -28,12 +23,11 @@ import {
filterSelectorEntities,
TargetSelector,
} from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../ha-target-picker";
@customElement("ha-selector-target")
export class HaTargetSelector extends SubscribeMixin(LitElement) {
export class HaTargetSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TargetSelector;
@ -48,18 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@state() private _entitySources?: EntitySources;
@state() private _entities?: EntityRegistryEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
@ -88,12 +72,19 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.entityFilter=${this._filterStates}
.entityRegFilter=${this._filterRegEntities}
.includeDeviceClasses=${this.selector.target?.entity?.device_class
? [this.selector.target?.entity.device_class]
: undefined}
.includeDomains=${this.selector.target?.entity?.domain
? [this.selector.target?.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterEntities = (entity: HassEntity): boolean => {
private _filterStates = (entity: HassEntity): boolean => {
if (!this.selector.target?.entity) {
return true;
}
@ -105,15 +96,26 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
);
};
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.target?.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
};
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if (!this.selector.target?.device) {
return true;
}
const deviceIntegrations =
this._entitySources && this._entities
? this._deviceIntegrationLookup(this._entitySources, this._entities)
: undefined;
const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup(
this._entitySources,
Object.values(this.hass.entities)
)
: undefined;
return filterSelectorDevices(
this.selector.target.device,

View File

@ -87,7 +87,8 @@ const panelSorter = (
reverseSort: string[],
defaultPanel: string,
a: PanelInfo,
b: PanelInfo
b: PanelInfo,
language: string
) => {
const indexA = reverseSort.indexOf(a.url_path);
const indexB = reverseSort.indexOf(b.url_path);
@ -97,13 +98,14 @@ const panelSorter = (
}
return -1;
}
return defaultPanelSorter(defaultPanel, a, b);
return defaultPanelSorter(defaultPanel, a, b, language);
};
const defaultPanelSorter = (
defaultPanel: string,
a: PanelInfo,
b: PanelInfo
b: PanelInfo,
language: string
) => {
// Put all the Lovelace at the top.
const aLovelace = a.component_name === "lovelace";
@ -117,7 +119,7 @@ const defaultPanelSorter = (
}
if (aLovelace && bLovelace) {
return stringCompare(a.title!, b.title!);
return stringCompare(a.title!, b.title!, language);
}
if (aLovelace && !bLovelace) {
return -1;
@ -139,7 +141,7 @@ const defaultPanelSorter = (
return 1;
}
// both not built in, sort by title
return stringCompare(a.title!, b.title!);
return stringCompare(a.title!, b.title!, language);
};
const computePanels = memoizeOne(
@ -147,7 +149,8 @@ const computePanels = memoizeOne(
panels: HomeAssistant["panels"],
defaultPanel: HomeAssistant["defaultPanel"],
panelsOrder: string[],
hiddenPanels: string[]
hiddenPanels: string[],
locale: HomeAssistant["locale"]
): [PanelInfo[], PanelInfo[]] => {
if (!panels) {
return [[], []];
@ -171,8 +174,12 @@ const computePanels = memoizeOne(
const reverseSort = [...panelsOrder].reverse();
beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b));
beforeSpacer.sort((a, b) =>
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
);
afterSpacer.sort((a, b) =>
panelSorter(reverseSort, defaultPanel, a, b, locale.language)
);
return [beforeSpacer, afterSpacer];
}
@ -374,7 +381,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
this._hiddenPanels,
this.hass.locale
);
// Show the supervisor as beeing part of configuration

View File

@ -345,6 +345,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeAreas=${ensureArray(this.value?.area_id)}
@value-changed=${this._targetPicked}
></ha-area-picker>
`;
@ -358,9 +359,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
"ui.components.target-picker.add_device_id"
)}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityRegFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeDevices=${ensureArray(this.value?.device_id)}
@value-changed=${this._targetPicked}
></ha-device-picker>
`;
@ -376,6 +377,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityFilter}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)}
@value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>
@ -393,6 +395,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
const target = ev.currentTarget;
target.value = "";
this._addMode = undefined;
if (
this.value &&
this.value[target.type] &&
ensureArray(this.value[target.type]).includes(value)
) {
return;
}
fireEvent(this, "value-changed", {
value: this.value
? {

View File

@ -1,14 +1,19 @@
import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property() public imageUrl?: string;
@property() public imageAlt?: string;
protected render(): TemplateResult {
return html`
<div class="image">
${this.imageUrl ? html`<img src=${this.imageUrl} />` : null}
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: null}
</div>
`;
}

View File

@ -30,7 +30,9 @@ class HaUserPicker extends LitElement {
return users
.filter((user) => !user.system_generated)
.sort((a, b) => stringCompare(a.name, b.name));
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass!.locale.language)
);
});
protected render(): TemplateResult {

View File

@ -50,6 +50,7 @@ export enum RecurrenceRange {
export const enum CalendarEntityFeature {
CREATE_EVENT = 1,
DELETE_EVENT = 2,
UPDATE_EVENT = 4,
}
export const fetchCalendarEvents = async (
@ -161,12 +162,18 @@ export const createCalendarEvent = (
export const updateCalendarEvent = (
hass: HomeAssistant,
entityId: string,
event: CalendarEventMutableParams
uid: string,
event: CalendarEventMutableParams,
recurrence_id?: string,
recurrence_range?: RecurrenceRange
) =>
hass.callWS<void>({
type: "calendar/event/update",
entity_id: entityId,
event: event,
uid,
recurrence_id,
recurrence_range,
event,
});
export const deleteCalendarEvent = (

View File

@ -61,12 +61,14 @@ export const processConversationInput = (
hass: HomeAssistant,
text: string,
// eslint-disable-next-line: variable-name
conversation_id: string
conversation_id: string | null,
language: string
): Promise<ConversationResult> =>
hass.callWS({
type: "conversation/process",
text,
conversation_id,
language,
});
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>

View File

@ -123,9 +123,12 @@ export const subscribeDeviceRegistry = (
onChange
);
export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
export const sortDeviceRegistryByName = (
entries: DeviceRegistryEntry[],
language: string
) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const getDeviceEntityLookup = (

View File

@ -29,6 +29,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
original_icon?: string;
device_class?: string;
original_device_class?: string;
aliases: string[];
}
export interface UpdateEntityRegistryEntryResult {
@ -63,6 +64,7 @@ export interface EntityRegistryEntryUpdateParams {
new_entity_id?: string;
options_domain?: string;
options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions;
aliases?: string[];
}
export const findBatteryEntity = (
@ -109,6 +111,15 @@ export const getExtendedEntityRegistryEntry = (
entity_id: entityId,
});
export const getExtendedEntityRegistryEntries = (
hass: HomeAssistant,
entityIds: string[]
): Promise<Record<string, ExtEntityRegistryEntry>> =>
hass.callWS({
type: "config/entity_registry/get_entries",
entity_ids: entityIds,
});
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,
@ -162,9 +173,12 @@ export const subscribeEntityRegistry = (
onChange
);
export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
export const sortEntityRegistryByName = (
entries: EntityRegistryEntry[],
language: string
) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language)
);
export const entityRegistryById = memoizeOne(

View File

@ -68,7 +68,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
entity_id: entityId,
});
export const filterUpdateEntities = (entities: HassEntities) =>
export const filterUpdateEntities = (
entities: HassEntities,
language?: string
) =>
(
Object.values(entities).filter(
(entity) => computeStateDomain(entity) === "update"
@ -94,7 +97,8 @@ export const filterUpdateEntities = (entities: HassEntities) =>
}
return caseInsensitiveStringCompare(
a.attributes.title || a.attributes.friendly_name || "",
b.attributes.title || b.attributes.friendly_name || ""
b.attributes.title || b.attributes.friendly_name || "",
language
);
});
@ -110,7 +114,7 @@ export const checkForEntityUpdates = async (
element: HTMLElement,
hass: HomeAssistant
) => {
const entities = filterUpdateEntities(hass.states).map(
const entities = filterUpdateEntities(hass.states, hass.locale.language).map(
(entity) => entity.entity_id
);

View File

@ -74,7 +74,7 @@ export class HaImagecropperDialog extends LitElement {
round: Boolean(this._params?.options.round),
})}"
>
<img />
<img alt=${this.hass.localize("ui.dialogs.image_cropper.crop_image")} />
</div>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}

View File

@ -484,7 +484,11 @@ export class QuickBar extends LitElement {
};
})
.sort((a, b) =>
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
caseInsensitiveStringCompare(
a.primaryText,
b.primaryText,
this.hass.locale.language
)
);
}
@ -494,7 +498,11 @@ export class QuickBar extends LitElement {
...this._generateServerControlCommands(),
...(await this._generateNavigationCommands()),
].sort((a, b) =>
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
caseInsensitiveStringCompare(
a.strings.join(" "),
b.strings.join(" "),
this.hass.locale.language
)
);
}

View File

@ -13,7 +13,6 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { SpeechRecognition } from "../../common/dom/speech-recognition";
import { uid } from "../../common/util/uid";
import "../../components/ha-dialog";
import type { HaDialog } from "../../components/ha-dialog";
import "../../components/ha-icon-button";
@ -60,7 +59,7 @@ export class HaVoiceCommandDialog extends LitElement {
private recognition!: SpeechRecognition;
private _conversationId?: string;
private _conversationId: string | null = null;
public async showDialog(): Promise<void> {
this._opened = true;
@ -175,7 +174,6 @@ export class HaVoiceCommandDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
this._conversationId = uid();
this._conversation = [
{
who: "hass",
@ -211,18 +209,29 @@ export class HaVoiceCommandDialog extends LitElement {
private _initRecognition() {
this.recognition = new SpeechRecognition();
this.recognition.interimResults = true;
this.recognition.lang = "en-US";
this.recognition.lang = this.hass.language;
this.recognition.onstart = () => {
this.recognition.addEventListener("start", () => {
this.results = {
final: false,
transcript: "",
};
};
this.recognition.onerror = (event) => {
});
this.recognition.addEventListener("nomatch", () => {
this._addMessage({
who: "user",
text: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_understand"
)}>`,
error: true,
});
});
this.recognition.addEventListener("error", (event) => {
// eslint-disable-next-line
console.error("Error recognizing text", event);
this.recognition!.abort();
// @ts-ignore
if (event.error !== "aborted") {
if (event.error !== "aborted" && event.error !== "no-speech") {
const text =
this.results && this.results.transcript
? this.results.transcript
@ -232,8 +241,8 @@ export class HaVoiceCommandDialog extends LitElement {
this._addMessage({ who: "user", text, error: true });
}
this.results = null;
};
this.recognition.onend = () => {
});
this.recognition.addEventListener("end", () => {
// Already handled by onerror
if (this.results == null) {
return;
@ -251,15 +260,14 @@ export class HaVoiceCommandDialog extends LitElement {
error: true,
});
}
};
this.recognition.onresult = (event) => {
});
this.recognition.addEventListener("result", (event) => {
const result = event.results[0];
this.results = {
transcript: result[0].transcript,
final: result.isFinal,
};
};
});
}
private async _processText(text: string) {
@ -277,8 +285,10 @@ export class HaVoiceCommandDialog extends LitElement {
const response = await processConversationInput(
this.hass,
text,
this._conversationId!
this._conversationId,
this.hass.language
);
this._conversationId = response.conversation_id;
const plain = response.response.speech?.plain;
if (plain) {
message.text = plain.speech;

View File

@ -19,6 +19,7 @@ class IntegrationBadge extends LitElement {
return html`
<div class="icon">
<img
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",

View File

@ -271,7 +271,9 @@ class OnboardingCoreConfig extends LitElement {
"[name=currency]"
) as HaTextField;
curInput.updateComplete.then(() => {
curInput.shadowRoot!.appendChild(createCurrencyListEl());
curInput.shadowRoot!.appendChild(
createCurrencyListEl(this.hass.locale.language)
);
curInput.formElement.setAttribute("list", "currencies");
});
@ -279,7 +281,9 @@ class OnboardingCoreConfig extends LitElement {
"[name=country]"
) as HaTextField;
countryInput.updateComplete.then(() => {
countryInput.shadowRoot!.appendChild(createCountryListEl());
countryInput.shadowRoot!.appendChild(
createCountryListEl(this.hass.locale.language)
);
countryInput.formElement.setAttribute("list", "countries");
});

View File

@ -117,7 +117,7 @@ class OnboardingIntegrations extends LitElement {
}
);
const content = [...entries, ...discovered]
.sort((a, b) => stringCompare(a[0], b[0]))
.sort((a, b) => stringCompare(a[0], b[0], this.hass.locale.language))
.map((item) => item[1]);
return html`

View File

@ -4,15 +4,11 @@ import { addDays, isSameDay } from "date-fns/esm";
import { toDate } from "date-fns-tz";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
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";
@ -23,10 +19,10 @@ import {
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import { renderRRuleAsText } from "./recurrence";
class DialogCalendarEventDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -137,54 +133,16 @@ class DialogCalendarEventDetail extends LitElement {
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>`;
const ruleText = renderRRuleAsText(this.hass, value);
if (ruleText !== undefined) {
return html`<div id="text">${ruleText}</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() {
// Parse a dates in the browser timezone
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

View File

@ -25,6 +25,8 @@ import {
CalendarEventMutableParams,
createCalendarEvent,
deleteCalendarEvent,
updateCalendarEvent,
RecurrenceRange,
} from "../../data/calendar";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@ -49,7 +51,7 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _summary = "";
@state() private _description = "";
@state() private _description? = "";
@state() private _rrule?: string;
@ -85,12 +87,13 @@ class DialogCalendarEventEditor extends LitElement {
const entry = params.entry!;
this._allDay = isDate(entry.dtstart);
this._summary = entry.summary;
this._description = entry.description;
this._rrule = entry.rrule;
if (this._allDay) {
this._dtstart = new Date(entry.dtstart);
this._dtstart = new Date(entry.dtstart + "T00:00:00");
// Calendar event end dates are exclusive, but not shown that way in the UI. The
// reverse happens when persisting the event.
this._dtend = addDays(new Date(entry.dtend), -1);
this._dtend = addDays(new Date(entry.dtend + "T00:00:00"), -1);
} else {
this._dtstart = new Date(entry.dtstart);
this._dtend = new Date(entry.dtend);
@ -168,6 +171,7 @@ class DialogCalendarEventEditor extends LitElement {
class="summary"
name="summary"
.label=${this.hass.localize("ui.components.calendar.event.summary")}
.value=${this._summary}
required
@change=${this._handleSummaryChanged}
error-message=${this.hass.localize("ui.common.error_required")}
@ -179,6 +183,7 @@ class DialogCalendarEventEditor extends LitElement {
.label=${this.hass.localize(
"ui.components.calendar.event.description"
)}
.value=${this._description}
@change=${this._handleDescriptionChanged}
autogrow
></ha-textarea>
@ -244,6 +249,9 @@ class DialogCalendarEventEditor extends LitElement {
</div>
</div>
<ha-recurrence-rule-editor
.hass=${this.hass}
.dtstart=${this._dtstart}
.allDay=${this._allDay}
.locale=${this.hass.locale}
.timezone=${this.hass.config.time_zone}
.value=${this._rrule || ""}
@ -412,6 +420,13 @@ class DialogCalendarEventEditor extends LitElement {
this._calendarId = ev.detail.value;
}
private _isValidStartEnd(): boolean {
if (this._allDay) {
return this._dtend! >= this._dtstart!;
}
return this._dtend! > this._dtstart!;
}
private async _createEvent() {
if (!this._summary || !this._calendarId) {
this._error = this.hass.localize(
@ -420,7 +435,7 @@ class DialogCalendarEventEditor extends LitElement {
return;
}
if (this._dtend! <= this._dtstart!) {
if (!this._isValidStartEnd()) {
this._error = this.hass.localize(
"ui.components.calendar.event.invalid_duration"
);
@ -445,7 +460,61 @@ class DialogCalendarEventEditor extends LitElement {
}
private async _saveEvent() {
// to be implemented
if (!this._summary || !this._calendarId) {
this._error = this.hass.localize(
"ui.components.calendar.event.not_all_required_fields"
);
return;
}
if (!this._isValidStartEnd()) {
this._error = this.hass.localize(
"ui.components.calendar.event.invalid_duration"
);
return;
}
this._submitting = true;
const entry = this._params!.entry!;
let range: RecurrenceRange | undefined = RecurrenceRange.THISEVENT;
if (entry.recurrence_id) {
range = await showConfirmEventDialog(this, {
title: this.hass.localize(
"ui.components.calendar.event.confirm_update.update"
),
text: this.hass.localize(
"ui.components.calendar.event.confirm_update.recurring_prompt"
),
confirmText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_this"
),
confirmFutureText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_future"
),
});
}
if (range === undefined) {
// Cancel
this._submitting = false;
return;
}
try {
await updateCalendarEvent(
this.hass!,
this._calendarId!,
entry.uid!,
this._calculateData(),
entry.recurrence_id || "",
range!
);
} catch (err: any) {
this._error = err ? err.message : "Unknown error";
return;
} finally {
this._submitting = false;
}
await this._params!.updated();
this.closeDialog();
}
private async _deleteEvent() {

View File

@ -302,6 +302,9 @@ export class HAFullCalendar extends LitElement {
private _handleEventClick(info): void {
const entityStateObj = this.hass.states[info.event.extendedProps.calendar];
const canEdit =
entityStateObj &&
supportsFeature(entityStateObj, CalendarEntityFeature.UPDATE_EVENT);
const canDelete =
entityStateObj &&
supportsFeature(entityStateObj, CalendarEntityFeature.DELETE_EVENT);
@ -312,6 +315,7 @@ export class HAFullCalendar extends LitElement {
updated: () => {
this._fireViewChanged();
},
canEdit: canEdit,
canDelete: canDelete,
});
}

View File

@ -1,11 +1,13 @@
import type { SelectedDetail } from "@material/mwc-list";
import { formatInTimeZone, toDate } from "date-fns-tz";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { Options, WeekdayStr } from "rrule";
import { ByWeekday, RRule, Weekday } from "rrule";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-chip";
import "../../components/ha-list-item";
import "../../components/ha-select";
@ -16,22 +18,31 @@ import {
convertFrequency,
convertRepeatFrequency,
DEFAULT_COUNT,
getWeekday,
getWeekdays,
intervalSuffix,
getMonthlyRepeatItems,
RepeatEnd,
RepeatFrequency,
ruleByWeekDay,
untilValue,
WEEKDAY_NAME,
MonthlyRepeatItem,
getMonthlyRepeatWeekdayFromRule,
getMonthdayRepeatFromRule,
} from "./recurrence";
import "../../components/ha-date-input";
@customElement("ha-recurrence-rule-editor")
export class RecurrenceRuleEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public disabled = false;
@property() public value = "";
@property() public dtstart?: Date;
@property() public allDay?: boolean;
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property() public timezone?: string;
@ -44,14 +55,24 @@ export class RecurrenceRuleEditor extends LitElement {
@state() private _weekday: Set<WeekdayStr> = new Set<WeekdayStr>();
@state() private _monthlyRepeat?: string;
@state() private _monthlyRepeatWeekday?: Weekday;
@state() private _monthday?: number;
@state() private _end: RepeatEnd = "never";
@state() private _count?: number;
@state() private _until?: Date;
@state() private _untilDay?: Date;
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
private _allWeekdays?: WeekdayStr[];
private _monthlyRepeatItems: MonthlyRepeatItem[] = [];
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
@ -61,15 +82,50 @@ export class RecurrenceRuleEditor extends LitElement {
);
}
if (!changedProps.has("value") || this._computedRRule === this.value) {
if (changedProps.has("dtstart") || changedProps.has("_interval")) {
this._monthlyRepeatItems = this.dtstart
? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart)
: [];
this._computeWeekday();
const selectElement = this._monthlyRepeatSelect;
if (selectElement) {
const oldSelected = selectElement.index;
selectElement.select(-1);
this.updateComplete.then(() => {
selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected);
});
}
}
if (
!changedProps.has("value") &&
(changedProps.has("dtstart") ||
changedProps.has("timezone") ||
changedProps.has("_freq") ||
changedProps.has("_interval") ||
changedProps.has("_weekday") ||
changedProps.has("_monthlyRepeatWeekday") ||
changedProps.has("_monthday") ||
changedProps.has("_end") ||
changedProps.has("_count") ||
changedProps.has("_untilDay"))
) {
this._updateRule();
return;
}
if (this._computedRRule === this.value) {
return;
}
this._interval = 1;
this._weekday.clear();
this._monthlyRepeat = undefined;
this._monthday = undefined;
this._monthlyRepeatWeekday = undefined;
this._end = "never";
this._count = undefined;
this._until = undefined;
this._untilDay = undefined;
this._computedRRule = this.value;
if (this.value === "") {
@ -88,6 +144,14 @@ export class RecurrenceRuleEditor extends LitElement {
if (rrule.interval) {
this._interval = rrule.interval;
}
this._monthlyRepeatWeekday = getMonthlyRepeatWeekdayFromRule(rrule);
if (this._monthlyRepeatWeekday) {
this._monthlyRepeat = `BYDAY=${this._monthlyRepeatWeekday.toString()}`;
}
this._monthday = getMonthdayRepeatFromRule(rrule);
if (this._monthday) {
this._monthlyRepeat = `BYMONTHDAY=${this._monthday}`;
}
if (
this._freq === "weekly" &&
rrule.byweekday &&
@ -101,7 +165,7 @@ export class RecurrenceRuleEditor extends LitElement {
}
if (rrule.until) {
this._end = "on";
this._until = rrule.until;
this._untilDay = toDate(rrule.until, { timeZone: this.timezone });
} else if (rrule.count) {
this._end = "after";
this._count = rrule.count;
@ -112,24 +176,65 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="freq"
label="Repeat"
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
@selected=${this._onRepeatSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._freq}
>
<ha-list-item value="none">None</ha-list-item>
<ha-list-item value="yearly">Yearly</ha-list-item>
<ha-list-item value="monthly">Monthly</ha-list-item>
<ha-list-item value="weekly">Weekly</ha-list-item>
<ha-list-item value="daily">Daily</ha-list-item>
<ha-list-item value="none">
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
</ha-list-item>
<ha-list-item value="yearly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.yearly"
)}
</ha-list-item>
<ha-list-item value="monthly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.monthly"
)}
</ha-list-item>
<ha-list-item value="weekly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.weekly"
)}
</ha-list-item>
<ha-list-item value="daily">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.daily"
)}
</ha-list-item>
</ha-select>
`;
}
renderMonthly() {
return this.renderInterval();
return html`
${this.renderInterval()}
${this._monthlyRepeatItems.length > 0
? html`<ha-select
id="monthly"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.monthly.label"
)}
@selected=${this._onMonthlyDetailSelected}
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._monthlyRepeatItems!.map(
(item) => html`
<ha-list-item .value=${item.value} .item=${item}>
${item.label}
</ha-list-item>
`
)}
</ha-select>`
: html``}
`;
}
renderWeekly() {
@ -142,7 +247,11 @@ export class RecurrenceRuleEditor extends LitElement {
.value=${item}
class=${classMap({ active: this._weekday.has(item) })}
@click=${this._onWeekdayToggle}
>${WEEKDAY_NAME[item]}</ha-chip
>${this.hass.localize(
`ui.components.calendar.event.repeat.weekly.weekday.${
item.toLowerCase() as Lowercase<WeekdayStr>
}`
)}</ha-chip
>
`
)}
@ -158,11 +267,16 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-textfield
id="interval"
label="Repeat interval"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.interval.label"
)}
type="number"
min="1"
.value=${this._interval}
.suffix=${intervalSuffix(this._freq!)}
.suffix=${this.hass.localize(
`ui.components.calendar.event.repeat.interval.${this
._freq!}` as LocalizeKeys
)}
@change=${this._onIntervalChange}
></ha-textfield>
`;
@ -172,26 +286,38 @@ export class RecurrenceRuleEditor extends LitElement {
return html`
<ha-select
id="end"
label="Ends"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end.label"
)}
.value=${this._end}
@selected=${this._onEndSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item value="never">Never</ha-list-item>
<ha-list-item value="after">After</ha-list-item>
<ha-list-item value="on">On</ha-list-item>
<ha-list-item value="never">
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
</ha-list-item>
<ha-list-item value="after">
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
</ha-list-item>
</ha-select>
${this._end === "after"
? html`
<ha-textfield
id="after"
label="End after"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.label"
)}
type="number"
min="1"
.value=${this._count!}
suffix="ocurrences"
suffix=${this.hass.localize(
"ui.components.calendar.event.repeat.end_after.ocurrences"
)}
@change=${this._onCountChange}
></ha-textfield>
`
@ -200,9 +326,11 @@ export class RecurrenceRuleEditor extends LitElement {
? html`
<ha-date-input
id="on"
label="End on"
label=${this.hass.localize(
"ui.components.calendar.event.repeat.end_on.label"
)}
.locale=${this.locale}
.value=${this._until!.toISOString()}
.value=${this._formatDate(this._untilDay!)}
@value-changed=${this._onUntilChange}
></ha-date-input>
`
@ -222,7 +350,6 @@ export class RecurrenceRuleEditor extends LitElement {
private _onIntervalChange(e: Event) {
this._interval = (e.target! as any).value;
this._updateRule();
}
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
@ -233,9 +360,20 @@ export class RecurrenceRuleEditor extends LitElement {
}
if (this._freq !== "weekly") {
this._weekday.clear();
this._computeWeekday();
}
e.stopPropagation();
this._updateRule();
}
private _onMonthlyDetailSelected(e: CustomEvent<SelectedDetail<number>>) {
e.stopPropagation();
const selectedItem = this._monthlyRepeatItems[e.detail.index];
if (!selectedItem) {
return;
}
this._monthlyRepeat = selectedItem.value;
this._monthlyRepeatWeekday = selectedItem.byday;
this._monthday = selectedItem.bymonthday;
}
private _onWeekdayToggle(e: MouseEvent) {
@ -246,7 +384,7 @@ export class RecurrenceRuleEditor extends LitElement {
} else {
this._weekday.delete(value);
}
this._updateRule();
this.requestUpdate("_weekday");
}
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
@ -259,44 +397,78 @@ export class RecurrenceRuleEditor extends LitElement {
switch (this._end) {
case "after":
this._count = DEFAULT_COUNT[this._freq!];
this._until = undefined;
this._untilDay = undefined;
break;
case "on":
this._count = undefined;
this._until = untilValue(this._freq!);
this._untilDay = untilValue(this._freq!);
break;
default:
this._count = undefined;
this._until = undefined;
this._untilDay = undefined;
}
e.stopPropagation();
this._updateRule();
}
private _onCountChange(e: Event) {
this._count = (e.target! as any).value;
this._updateRule();
}
private _onUntilChange(e: CustomEvent) {
e.stopPropagation();
this._until = new Date(e.detail.value);
this._updateRule();
this._untilDay = toDate(e.detail.value + "T00:00:00", {
timeZone: this.timezone,
});
}
// Reset the weekday selected when there is only a single value
private _computeWeekday() {
if (this.dtstart && this._weekday.size <= 1) {
const weekdayNum = getWeekday(this.dtstart);
this._weekday.clear();
this._weekday.add(new Weekday(weekdayNum).toString() as WeekdayStr);
}
}
private _computeRRule() {
if (this._freq === undefined || this._freq === "none") {
return "";
}
const options = {
let byweekday: Weekday[] | undefined;
let bymonthday: number | undefined;
if (this._freq === "monthly" && this._monthlyRepeatWeekday !== undefined) {
byweekday = [this._monthlyRepeatWeekday];
} else if (this._freq === "monthly" && this._monthday !== undefined) {
bymonthday = this._monthday;
} else if (this._freq === "weekly") {
byweekday = ruleByWeekDay(this._weekday);
}
const options: Partial<Options> = {
freq: convertRepeatFrequency(this._freq!)!,
interval: this._interval > 1 ? this._interval : undefined,
byweekday: ruleByWeekDay(this._weekday),
count: this._count,
until: this._until,
tzid: this.timezone,
byweekday: byweekday,
bymonthday: bymonthday,
};
const contentline = RRule.optionsToString(options);
let contentline = RRule.optionsToString(options);
if (this._untilDay) {
// The UNTIL value should be inclusive of the last event instance
const until = toDate(
this._formatDate(this._untilDay!) +
"T" +
this._formatTime(this.dtstart!),
{ timeZone: this.timezone }
);
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
// in the same format as dtstart.
const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss";
const newUntilValue = formatInTimeZone(
until,
this.hass.config.time_zone,
format
);
contentline += `;UNTIL=${newUntilValue}`;
}
return contentline.slice(6); // Strip "RRULE:" prefix
}
@ -315,6 +487,16 @@ export class RecurrenceRuleEditor extends LitElement {
);
}
// Formats a date in browser display timezone
private _formatDate(date: Date): string {
return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd");
}
// Formats a time in browser display timezone
private _formatTime(date: Date): string {
return formatInTimeZone(date, this.timezone!, "HH:mm:ss");
}
static styles = css`
ha-textfield,
ha-select {

View File

@ -1,8 +1,22 @@
// Library for converting back and forth from values use by this webcomponent
// and the values defined by rrule.js.
import { RRule, Frequency, Weekday } from "rrule";
import type { WeekdayStr } from "rrule";
import { addDays, addMonths, addWeeks, addYears } from "date-fns";
import {
addDays,
addMonths,
addWeeks,
addYears,
getDate,
getDay,
isLastDayOfMonth,
isSameMonth,
} from "date-fns";
import type { Options, WeekdayStr } from "rrule";
import { Frequency, RRule, Weekday } from "rrule";
import { formatDate } from "../../common/datetime/format_date";
import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter";
import { dayNames } from "../../common/translations/day_names";
import { monthNames } from "../../common/translations/month_names";
import { HomeAssistant } from "../../types";
export type RepeatFrequency =
| "none"
@ -21,14 +35,11 @@ export const DEFAULT_COUNT = {
daily: 30,
};
export function intervalSuffix(freq: RepeatFrequency) {
if (freq === "monthly") {
return "months";
}
if (freq === "weekly") {
return "weeks";
}
return "days";
export interface MonthlyRepeatItem {
value: string;
byday?: Weekday;
bymonthday?: number;
label: string;
}
export function untilValue(freq: RepeatFrequency): Date {
@ -81,16 +92,6 @@ export const convertRepeatFrequency = (
}
};
export const WEEKDAY_NAME = {
SU: "Sun",
MO: "Mon",
TU: "Tue",
WE: "Wed",
TH: "Thu",
FR: "Fri",
SA: "Sat",
};
export const WEEKDAYS = [
RRule.SU,
RRule.MO,
@ -101,7 +102,16 @@ export const WEEKDAYS = [
RRule.SA,
];
export function getWeekdays(firstDay?: number) {
/** Return a weekday number compatible with rrule.js weekdays */
export function getWeekday(dtstart: Date): number {
let weekDay = getDay(dtstart) - 1;
if (weekDay < 0) {
weekDay += 7;
}
return weekDay;
}
export function getWeekdays(firstDay?: number): Weekday[] {
if (firstDay === undefined || firstDay === 0) {
return WEEKDAYS;
}
@ -114,9 +124,7 @@ export function getWeekdays(firstDay?: number) {
return weekDays;
}
export function ruleByWeekDay(
weekdays: Set<WeekdayStr>
): Weekday[] | undefined {
export function ruleByWeekDay(weekdays: Set<WeekdayStr>): Weekday[] {
return Array.from(weekdays).map((value: string) => {
switch (value) {
case "MO":
@ -138,3 +146,127 @@ export function ruleByWeekDay(
}
});
}
/**
* Determine the recurrence options based on the day of the month. The
* return values are a Weekday object that represent a BYDAY for a
* particular week of the month like "first Saturday" or "last Friday".
*/
function getWeekydaysForMonth(dtstart: Date): Weekday[] {
const weekDay = getWeekday(dtstart);
const dayOfMonth = getDate(dtstart);
const nthWeekdayOfMonth = Math.floor((dayOfMonth - 1) / 7) + 1;
const isLastWeekday = !isSameMonth(dtstart, addDays(dtstart, 7));
const byweekdays: Weekday[] = [];
if (!isLastWeekday || dayOfMonth <= 28) {
byweekdays.push(new Weekday(weekDay, nthWeekdayOfMonth));
}
if (isLastWeekday) {
byweekdays.push(new Weekday(weekDay, -1));
}
return byweekdays;
}
/**
* Returns the list of repeat values available for the specified date.
*/
export function getMonthlyRepeatItems(
hass: HomeAssistant,
interval: number,
dtstart: Date
): MonthlyRepeatItem[] {
const getLabel = (repeatValue: string) =>
renderRRuleAsText(hass, `FREQ=MONTHLY;INTERVAL=${interval};${repeatValue}`);
const result: MonthlyRepeatItem[] = [
// The default repeat rule is on day of month e.g. 3rd day of month
{
value: `BYMONTHDAY=${getDate(dtstart)}`,
label: getLabel(`BYMONTHDAY=${getDate(dtstart)}`)!,
},
// Additional optional rules based on the week of month e.g. 2nd sunday of month
...getWeekydaysForMonth(dtstart).map((item) => ({
value: `BYDAY=${item.toString()}`,
byday: item,
label: getLabel(`BYDAY=${item.toString()}`)!,
})),
];
if (isLastDayOfMonth(dtstart)) {
result.push({
value: "BYMONTHDAY=-1",
bymonthday: -1,
label: getLabel(`BYMONTHDAY=-1`)!,
});
}
return result;
}
export function getMonthlyRepeatWeekdayFromRule(
rrule: Partial<Options>
): Weekday | undefined {
if (rrule.freq !== Frequency.MONTHLY) {
return undefined;
}
if (
rrule.byweekday &&
Array.isArray(rrule.byweekday) &&
rrule.byweekday.length === 1 &&
rrule.byweekday[0] instanceof Weekday
) {
return rrule.byweekday[0];
}
return undefined;
}
export function getMonthdayRepeatFromRule(
rrule: Partial<Options>
): number | undefined {
if (rrule.freq !== Frequency.MONTHLY || !rrule.bymonthday) {
return undefined;
}
if (Array.isArray(rrule.bymonthday)) {
return rrule.bymonthday[0];
}
return rrule.bymonthday;
}
/**
* A wrapper around RRule.toText that assists with translation.
*/
export function renderRRuleAsText(hass: HomeAssistant, value: string) {
const rule = RRule.fromString(`RRULE:${value}`);
if (!rule.isFullyConvertibleToText()) {
return undefined;
}
return capitalizeFirstLetter(
rule.toText(
(id: string | number | Weekday): string => {
if (typeof id === "string") {
return hass.localize(`ui.components.calendar.event.rrule.${id}`);
}
return "";
},
{
dayNames: dayNames(hass.locale),
monthNames: monthNames(hass.locale),
tokens: {},
},
// Format the date
(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(hass.locale).indexOf(month));
date.setDate(day);
return formatDate(date, hass.locale);
}
)
);
}

View File

@ -192,13 +192,13 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
devices.forEach((entry) => {
entry.name = computeDeviceName(entry, this.hass);
});
sortDeviceRegistryByName(devices);
sortDeviceRegistryByName(devices, this.hass.locale.language);
}
if (entities) {
entities.forEach((entry) => {
entry.name = computeEntityRegistryName(this.hass, entry);
});
sortEntityRegistryByName(entities);
sortEntityRegistryByName(entities, this.hass.locale.language);
}
// Group entities by domain
@ -258,7 +258,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
<div class="column">
${area.picture
? html`<div class="img-container">
<img src=${area.picture} /><ha-icon-button
<img alt=${area.name} src=${area.picture} />
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
@ -507,7 +508,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
});
groupedEntities.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
caseInsensitiveStringCompare(
entry1.name!,
entry2.name!,
this.hass.locale.language
)
);
}
if (relatedEntityIds?.length) {
@ -521,7 +526,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
});
relatedEntities.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
caseInsensitiveStringCompare(
entry1.name!,
entry2.name!,
this.hass.locale.language
)
);
}

View File

@ -299,7 +299,7 @@ export default class HaAutomationAction extends LitElement {
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1]))
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {

View File

@ -66,7 +66,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1]))
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
private _conditionChanged(ev: CustomEvent) {

View File

@ -158,6 +158,7 @@ export class HaDeviceAction extends LitElement {
}
ha-form {
display: block;
margin-top: 24px;
}
`;

View File

@ -328,7 +328,7 @@ export default class HaAutomationCondition extends LitElement {
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1]))
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {

View File

@ -541,15 +541,10 @@ export class HaAutomationTrace extends LitElement {
justify-content: center;
display: flex;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}

View File

@ -302,7 +302,7 @@ export default class HaAutomationTrigger extends LitElement {
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1]))
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {

View File

@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
],
],
},
{ name: "offset", selector: { duration: { enable_day: true } } },
{ name: "offset", selector: { duration: {} } },
{
name: "offset_type",
type: "select",

View File

@ -54,7 +54,11 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
private async _fetchTags() {
this._tags = (await fetchTags(this.hass)).sort((a, b) =>
caseInsensitiveStringCompare(a.name || a.id, b.name || b.id)
caseInsensitiveStringCompare(
a.name || a.id,
b.name || b.id,
this.hass.locale.language
)
);
}

View File

@ -9,7 +9,6 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@ -41,22 +40,17 @@ import {
updateCloudAlexaEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@customElement("cloud-alexa")
class CloudAlexa extends SubscribeMixin(LitElement) {
class CloudAlexa extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property()
@ -168,13 +162,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
${entity.interfaces
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
.join(", ")}
</state-info>
${!emptyFilter
? html`${iconButton}`
@ -323,23 +312,18 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
}
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
this._entityCategories = categories;
}
}
private async _fetchData() {
@ -349,7 +333,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
const stateB = this.hass.states[b.entity_id];
return stringCompare(
stateA ? computeStateName(stateA) : a.entity_id,
stateB ? computeStateName(stateB) : b.entity_id
stateB ? computeStateName(stateB) : b.entity_id,
this.hass.locale.language
);
});
this._entities = entities;
@ -541,6 +526,7 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
}
state-info {
cursor: pointer;
height: 40px;
}
ha-switch {
padding: 8px 0;

View File

@ -9,7 +9,6 @@ import {
mdiFormatListChecks,
mdiSync,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@ -41,7 +40,9 @@ import {
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
ExtEntityRegistryEntry,
getExtendedEntityRegistryEntries,
updateEntityRegistryEntry,
} from "../../../../data/entity_registry";
import {
fetchCloudGoogleEntities,
@ -51,15 +52,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases";
const DEFAULT_CONFIG_EXPOSE = true;
@customElement("cloud-google-assistant")
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
class CloudGoogleAssistant extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus!: CloudStatusLoggedIn;
@ -68,6 +69,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
@state() private _entities?: GoogleEntity[];
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
@state() private _syncing = false;
@state()
@ -164,6 +167,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
: mdiCloseBoxMultiple}
></ha-icon-button>`;
const aliases = this._entries?.[entity.entity_id]?.aliases;
target.push(html`
<ha-card outlined>
<div class="card-content">
@ -174,15 +179,57 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
secondary-line
@click=${this._showMoreInfo}
>
${entity.traits
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")}
${aliases
? html`
<span>
${aliases.length > 0
? [...aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")
: this.hass.localize(
"ui.panel.config.cloud.google.no_aliases"
)}
</span>
<br />
<button
class="link"
.entityId=${entity.entity_id}
@click=${this._openAliasesSettings}
>
${this.hass.localize(
`ui.panel.config.cloud.google.${
aliases.length > 0
? "manage_aliases"
: "add_aliases"
}`
)}
</button>
`
: html`
<span>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available"
)}
</span>
<br />
<button
class="link"
.stateObj=${stateObj}
@click=${this._showMoreInfoSettings}
>
${this.hass.localize(
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
)}
</button>
`}
</state-info>
${!emptyFilter
? html`${iconButton}`
: html`<ha-button-menu
corner="BOTTOM_START"
.entityId=${stateObj.entity_id}
.entityId=${entity.entity_id}
@action=${this._exposeChanged}
>
${iconButton}
@ -308,7 +355,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.alexa.exposed",
"ui.panel.config.cloud.google.exposed",
"selected",
selected
)
@ -329,7 +376,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.alexa.not_exposed",
"ui.panel.config.cloud.google.not_exposed",
"selected",
this._entities.length - selected
)
@ -354,23 +401,38 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
if (changedProps.has("cloudStatus")) {
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
}
if (
changedProps.has("hass") &&
changedProps.get("hass")?.entities !== this.hass.entities
) {
const categories = {};
for (const entry of Object.values(this.hass.entities)) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}
}
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
private async _openAliasesSettings(ev) {
ev.stopPropagation();
const entityId = ev.target.entityId;
const entry = this._entries![entityId];
if (!entry) {
return;
}
showEntityAliasesDialog(this, {
entity: entry,
updateEntry: async (updates) => {
const { entity_entry } = await updateEntityRegistryEntry(
this.hass,
entry.entity_id,
updates
);
this._entries![entity_entry.entity_id] = entity_entry;
},
});
}
private _configIsDomainExposed(
@ -397,12 +459,20 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
private async _fetchData() {
const entities = await fetchCloudGoogleEntities(this.hass);
this._entries = await getExtendedEntityRegistryEntries(
this.hass,
entities
.filter((ent) => this.hass.entities[ent.entity_id])
.map((e) => e.entity_id)
);
entities.sort((a, b) => {
const stateA = this.hass.states[a.entity_id];
const stateB = this.hass.states[b.entity_id];
return stringCompare(
stateA ? computeStateName(stateA) : a.entity_id,
stateB ? computeStateName(stateB) : b.entity_id
stateB ? computeStateName(stateB) : b.entity_id,
this.hass.locale.language
);
});
this._entities = entities;
@ -410,7 +480,14 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
private _showMoreInfo(ev) {
const entityId = ev.currentTarget.stateObj.entity_id;
fireEvent(this, "hass-more-info", { entityId });
const moreInfoTab = ev.currentTarget.moreInfoTab;
fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab });
}
private _showMoreInfoSettings(ev) {
ev.stopPropagation();
const entityId = ev.currentTarget.stateObj.entity_id;
fireEvent(this, "hass-more-info", { entityId, tab: "settings" });
}
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
@ -582,6 +659,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return [
haStyle,
buttonLinkStyle,
css`
mwc-list-item > [slot="meta"] {
margin-left: 4px;

View File

@ -304,7 +304,11 @@ class HaConfigSectionGeneral extends LitElement {
}
this._languages = Object.entries(this.hass.translationMetadata.translations)
.sort((a, b) =>
caseInsensitiveStringCompare(a[1].nativeName, b[1].nativeName)
caseInsensitiveStringCompare(
a[1].nativeName,
b[1].nativeName,
this.hass.locale.language
)
)
.map(([value, metaData]) => ({
value,

View File

@ -31,7 +31,8 @@ export class HaDeviceViaDevicesCard extends LitElement {
.sort((d1, d2) =>
caseInsensitiveStringCompare(
computeDeviceName(d1, this.hass),
computeDeviceName(d2, this.hass)
computeDeviceName(d2, this.hass),
this.hass.locale.language
)
)
);

View File

@ -157,7 +157,8 @@ export class HaConfigDevicePage extends LitElement {
.sort((ent1, ent2) =>
stringCompare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`
ent2.stateName || `zzz${ent2.entity_id}`,
this.hass.locale.language
)
)
);
@ -658,6 +659,10 @@ export class HaConfigDevicePage extends LitElement {
integrations.length
? html`
<img
alt=${domainToName(
this.hass.localize,
integrations[0].domain
)}
src=${brandsUrl({
domain: integrations[0].domain,
type: "logo",

View File

@ -220,6 +220,7 @@ export class EnergyGridSettings extends LitElement {
${this._co2ConfigEntry
? html`<div class="row" .entry=${this._co2ConfigEntry}>
<img
alt=""
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: "co2signal",
@ -244,6 +245,7 @@ export class EnergyGridSettings extends LitElement {
: html`
<div class="row border-bottom">
<img
alt=""
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: "co2signal",

View File

@ -130,6 +130,7 @@ export class DialogEnergySolarSettings
style="display: flex; align-items: center;"
>
<img
alt=""
referrerpolicy="no-referrer"
style="height: 24px; margin-right: 16px;"
src=${brandsUrl({

View File

@ -57,7 +57,13 @@ export class EntityRegistrySettingsHelper extends LitElement {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
this._item = undefined;
if (
this.entry.unique_id !==
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
) {
this._item = undefined;
}
this._getItem();
}
}

View File

@ -0,0 +1,212 @@
import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { EntityAliasesDialogParams } from "./show-dialog-entity-aliases";
@customElement("dialog-entity-aliases")
class DialogEntityAliases extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@state() private _params?: EntityAliasesDialogParams;
@state() private _aliases!: string[];
@state() private _submitting = false;
public async showDialog(params: EntityAliasesDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._aliases =
this._params.entity.aliases?.length > 0
? [...this._params.entity.aliases].sort()
: [""];
await this.updateComplete;
}
public closeDialog(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
const entityId = this._params.entity.entity_id;
const stateObj = entityId ? this.hass.states[entityId] : undefined;
const name = (stateObj && computeStateName(stateObj)) || entityId;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.heading",
{ name }
)}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: ""}
<div class="form">
${this._aliases.map(
(alias, index) => html`
<div class="layout horizontal center-center row">
<ha-textfield
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.input_label",
{ number: index + 1 }
)}
.value=${alias}
?data-last=${index === this._aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.remove_alias",
{ number: index + 1 }
)}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
></ha-icon-button>
</div>
`
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.add_alias"
)}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
</div>
</div>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.save"
)}
</mwc-button>
</ha-dialog>
`;
}
private async _addAlias() {
this._aliases = [...this._aliases, ""];
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
| HaTextField
| undefined;
field?.focus();
}
private async _editAlias(ev: Event) {
const index = (ev.target as any).index;
this._aliases[index] = (ev.target as any).value;
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this._aliases];
aliases.splice(index, 1);
this._aliases = aliases;
}
private async _updateEntry(): Promise<void> {
this._submitting = true;
const noEmptyAliases = this._aliases
.map((alias) => alias.trim())
.filter((alias) => alias);
try {
await this._params!.updateEntry({
aliases: noEmptyAliases,
});
this.closeDialog();
} catch (err: any) {
this._error =
err.message ||
this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.unknown_error"
);
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.row {
margin-bottom: 8px;
}
ha-textfield {
display: block;
}
ha-icon-button {
display: block;
}
mwc-button {
margin-left: 8px;
}
#alias_input {
margin-top: 8px;
}
.alias {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
--mdc-icon-button-size: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-entity-aliases": DialogEntityAliases;
}
}

View File

@ -0,0 +1,25 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import {
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
} from "../../../../data/entity_registry";
export interface EntityAliasesDialogParams {
entity: ExtEntityRegistryEntry;
updateEntry: (
updates: Partial<EntityRegistryEntryUpdateParams>
) => Promise<unknown>;
}
export const loadEntityAliasesDialog = () => import("./dialog-entity-aliases");
export const showEntityAliasesDialog = (
element: HTMLElement,
entityAliasesParams: EntityAliasesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-entity-aliases",
dialogImport: loadEntityAliasesDialog,
dialogParams: entityAliasesParams,
});
};

View File

@ -1,8 +1,13 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPencil } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-radio";
@ -21,6 +26,7 @@ import {
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@ -44,6 +50,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _submitting = false;
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
});
}
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
@ -247,6 +268,37 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
</div>
`
: ""}
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
"ui.dialogs.entity_registry.editor.configured_aliases",
{ count: this.entry.aliases.length }
)
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
)}
</div>
</ha-expansion-panel>
`;
}
@ -300,6 +352,13 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
.label {
margin-top: 16px;
}
.aliases {
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
--mdc-icon-button-size: 24px;
overflow: hidden;
}
`;
}
}

View File

@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { mdiPencil } from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@ -26,6 +27,7 @@ import {
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon";
import "../../../components/ha-icon-picker";
import "../../../components/ha-radio";
import "../../../components/ha-select";
@ -75,6 +77,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases";
const OVERRIDE_DEVICE_CLASSES = {
cover: [
@ -115,6 +118,43 @@ const OVERRIDE_NUMBER_UNITS = {
};
const OVERRIDE_SENSOR_UNITS = {
current: ["A", "mA"],
data_rate: [
"bit/s",
"kbit/s",
"Mbit/s",
"Gbit/s",
"B/s",
"kB/s",
"MB/s",
"GB/s",
"KiB/s",
"MiB/s",
"GiB/s",
],
data_size: [
"bit",
"kbit",
"Mbit",
"Gbit",
"B",
"kB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
],
distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"],
gas: ["CCF", "ft³", "m³"],
precipitation: ["cm", "in", "mm"],
@ -122,6 +162,7 @@ const OVERRIDE_SENSOR_UNITS = {
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"],
temperature: ["°C", "°F", "K"],
voltage: ["V", "mV"],
volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"],
water: ["CCF", "ft³", "gal", "L", "m³"],
weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"],
@ -673,7 +714,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_status"
)}:
)}
</div>
<div class="secondary">
${this._disabledBy &&
@ -760,12 +801,45 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</div>
`
: ""}
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
"ui.dialogs.entity_registry.editor.configured_aliases",
{ count: this.entry.aliases.length }
)
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.description"
)}
</div>
${this.entry.device_id
? html`
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_area"
)}:
)}
</div>
<ha-area-picker
.hass=${this.hass}
@ -943,6 +1017,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
});
}
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
showEntityAliasesDialog(this, {
entity: this.entry!,
updateEntry: async (updates) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
updates
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
});
}
private async _enableEntry() {
this._error = undefined;
this._submitting = true;
@ -1143,7 +1232,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
domain: entry,
label: domainToName(localize, entry),
}))
.sort((a, b) => stringCompare(a.label, b.label))
.sort((a, b) =>
stringCompare(a.label, b.label, this.hass.locale.language)
)
);
private _deviceClassesSorted = memoizeOne(
@ -1155,7 +1246,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
),
}))
.sort((a, b) => stringCompare(a.label, b.label))
.sort((a, b) =>
stringCompare(a.label, b.label, this.hass.locale.language)
)
);
static get styles(): CSSResultGroup {
@ -1212,7 +1305,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
.secondary {
margin: 8px 0;
width: 340px;
}
li[divider] {
border-bottom-color: var(--divider-color);
@ -1220,6 +1312,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
ha-alert mwc-button {
width: max-content;
}
.aliases {
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
--mdc-icon-button-size: 24px;
overflow: hidden;
}
`,
];
}

View File

@ -20,7 +20,12 @@ import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
(
showAdvanced: boolean,
hardware: HassioHardwareInfo,
filter: string,
language: string
) =>
hardware.devices
.filter(
(device) =>
@ -33,7 +38,7 @@ const _filterDevices = memoizeOne(
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name))
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@customElement("ha-dialog-hardware-available")
@ -70,7 +75,8 @@ class DialogHardwareAvailable extends LitElement implements HassDialog {
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._hardware,
(this._filter || "").toLowerCase()
(this._filter || "").toLowerCase(),
this.hass.locale.language
);
return html`

View File

@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
.twoline=${Boolean(boardId)}
>
${imageURL
? html`<img slot="graphic" src=${imageURL} />`
? html`<img alt="" slot="graphic" src=${imageURL} />`
: ""}
<span class="primary-text">
${boardName ||

View File

@ -147,7 +147,13 @@ class AddIntegrationDialog extends LitElement {
is_built_in: true,
is_add: true,
}))
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
.sort((a, b) =>
caseInsensitiveStringCompare(
a.name,
b.name,
this.hass.locale.language
)
);
const integrations: IntegrationListItem[] = [];
const yamlIntegrations: IntegrationListItem[] = [];
@ -242,7 +248,11 @@ class AddIntegrationDialog extends LitElement {
return [
...addDeviceRows,
...integrations.sort((a, b) =>
caseInsensitiveStringCompare(a.name || "", b.name || "")
caseInsensitiveStringCompare(
a.name || "",
b.name || "",
this.hass.locale.language
)
),
];
}

View File

@ -230,7 +230,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
(conf1, conf2) =>
caseInsensitiveStringCompare(
conf1.localized_domain_name + conf1.title,
conf2.localized_domain_name + conf2.title
conf2.localized_domain_name + conf2.title,
this.hass.locale.language
)
);
},

View File

@ -48,6 +48,7 @@ class HaDomainIntegrations extends LitElement {
hasMeta
>
<img
alt=""
slot="graphic"
loading="lazy"
src=${brandsUrl({
@ -121,7 +122,8 @@ class HaDomainIntegrations extends LitElement {
}
return caseInsensitiveStringCompare(
a[1].name || domainToName(this.hass.localize, a[0]),
b[1].name || domainToName(this.hass.localize, b[0])
b[1].name || domainToName(this.hass.localize, b[0]),
this.hass.locale.language
);
})
.map(

View File

@ -92,6 +92,7 @@ export class HaIntegrationHeader extends LitElement {
<slot name="above-header"></slot>
<div class="header">
<img
alt=""
src=${brandsUrl({
domain: this.domain,
type: "icon",

View File

@ -47,6 +47,7 @@ export class HaIntegrationListItem extends ListItemBase {
)}"
>
<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: this.integration.domain,

View File

@ -22,25 +22,32 @@ class HaPanelDevMqtt extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@LocalStorage("panel-dev-mqtt-topic-ls", true, false)
private topic = "";
private _topic = "";
@LocalStorage("panel-dev-mqtt-payload-ls", true, false)
private payload = "";
private _payload = "";
@LocalStorage("panel-dev-mqtt-qos-ls", true, false)
private qos = "0";
private _qos = "0";
@LocalStorage("panel-dev-mqtt-retain-ls", true, false)
private retain = false;
private _retain = false;
@LocalStorage("panel-dev-mqtt-allow-template-ls", true, false)
private _allowTemplate = false;
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card header="MQTT settings">
<ha-card
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
>
<div class="card-actions">
<mwc-button @click=${this._openOptionFlow}
>Re-configure MQTT</mwc-button
>${this.hass.localize(
"ui.panel.config.mqtt.reconfigure"
)}</mwc-button
>
</div>
</ha-card>
@ -53,12 +60,12 @@ class HaPanelDevMqtt extends LitElement {
<div class="panel-dev-mqtt-fields">
<ha-textfield
.label=${this.hass.localize("ui.panel.config.mqtt.topic")}
.value=${this.topic}
.value=${this._topic}
@change=${this._handleTopic}
></ha-textfield>
<ha-select
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
.value=${this.qos}
.value=${this._qos}
@selected=${this._handleQos}
>${qosLevel.map(
(qos) =>
@ -70,17 +77,35 @@ class HaPanelDevMqtt extends LitElement {
>
<ha-switch
@change=${this._handleRetain}
.checked=${this.retain}
.checked=${this._retain}
></ha-switch>
</ha-formfield>
</div>
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
<p>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.mqtt.allow_template"
)}
>
<ha-switch
@change=${this._handleAllowTemplate}
.checked=${this._allowTemplate}
></ha-switch>
</ha-formfield>
</p>
<p>
${this._allowTemplate
? this.hass.localize("ui.panel.config.mqtt.payload")
: this.hass.localize(
"ui.panel.config.mqtt.payload_no_template"
)}
</p>
<ha-code-editor
mode="jinja2"
autocomplete-entities
autocomplete-icons
.hass=${this.hass}
.value=${this.payload}
.value=${this._payload}
@value-changed=${this._handlePayload}
dir="ltr"
></ha-code-editor>
@ -101,22 +126,26 @@ class HaPanelDevMqtt extends LitElement {
}
private _handleTopic(ev: CustomEvent) {
this.topic = (ev.target! as any).value;
this._topic = (ev.target! as any).value;
}
private _handlePayload(ev: CustomEvent) {
this.payload = ev.detail.value;
this._payload = ev.detail.value;
}
private _handleQos(ev: CustomEvent) {
const newValue = (ev.target! as any).value;
if (newValue >= 0 && newValue !== this.qos) {
this.qos = newValue;
if (newValue >= 0 && newValue !== this._qos) {
this._qos = newValue;
}
}
private _handleRetain(ev: CustomEvent) {
this.retain = (ev.target! as any).checked;
this._retain = (ev.target! as any).checked;
}
private _handleAllowTemplate(ev: CustomEvent) {
this._allowTemplate = (ev.target! as any).checked;
}
private _publish(): void {
@ -124,10 +153,11 @@ class HaPanelDevMqtt extends LitElement {
return;
}
this.hass.callService("mqtt", "publish", {
topic: this.topic,
payload_template: this.payload,
qos: parseInt(this.qos),
retain: this.retain,
topic: this._topic,
payload: !this._allowTemplate ? this._payload : undefined,
payload_template: this._allowTemplate ? this._payload : undefined,
qos: parseInt(this._qos),
retain: this._retain,
});
}

View File

@ -9,6 +9,8 @@ import { MQTTMessage, subscribeMQTTTopic } from "../../../../../data/mqtt";
import { HomeAssistant } from "../../../../../types";
import "@material/mwc-list/mwc-list-item";
import { LocalStorage } from "../../../../../common/decorators/local-storage";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-switch";
const qosLevel = ["0", "1", "2"];
@ -22,6 +24,9 @@ class MqttSubscribeCard extends LitElement {
@LocalStorage("panel-dev-mqtt-qos-subscribe", true, false)
private _qos = "0";
@LocalStorage("panel-dev-mqtt-json-format", true, false)
private _json_format = false;
@state() private _subscribed?: () => void;
@state() private _messages: Array<{
@ -47,6 +52,18 @@ class MqttSubscribeCard extends LitElement {
header=${this.hass.localize("ui.panel.config.mqtt.description_listen")}
>
<form>
<p>
<ha-formfield
label=${this.hass!.localize(
"ui.panel.config.mqtt.json_formatting"
)}
>
<ha-switch
@change=${this._handleJSONFormat}
.checked=${this._json_format}
></ha-switch>
</ha-formfield>
</p>
<div class="panel-dev-mqtt-subscribe-fields">
<ha-textfield
.label=${this._subscribed
@ -114,6 +131,10 @@ class MqttSubscribeCard extends LitElement {
}
}
private _handleJSONFormat(ev: CustomEvent) {
this._json_format = (ev.target! as any).checked;
}
private async _handleSubmit(): Promise<void> {
if (this._subscribed) {
this._subscribed();
@ -132,9 +153,13 @@ class MqttSubscribeCard extends LitElement {
const tail =
this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages;
let payload: string;
try {
payload = JSON.stringify(JSON.parse(message.payload), null, 4);
} catch (err: any) {
if (this._json_format) {
try {
payload = JSON.stringify(JSON.parse(message.payload), null, 4);
} catch (err: any) {
payload = message.payload;
}
} else {
payload = message.payload;
}
this._messages = [

View File

@ -50,7 +50,8 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
.sort((ent1, ent2) =>
stringCompare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`
ent2.stateName || `zzz${ent2.entity_id}`,
this.hass.locale.language
)
)
);

View File

@ -230,7 +230,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
result.push(
...dashboards
.sort((a, b) => stringCompare(a.title, b.title))
.sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
)
.map((dashboard) => ({
filename: "",
...dashboard,
@ -342,7 +344,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
createDashboard: async (values: LovelaceDashboardCreateParams) => {
const created = await createDashboard(this.hass!, values);
this._dashboards = this._dashboards!.concat(created).sort(
(res1, res2) => stringCompare(res1.url_path, res2.url_path)
(res1, res2) =>
stringCompare(
res1.url_path,
res2.url_path,
this.hass.locale.language
)
);
},
updateDashboard: async (values) => {

View File

@ -143,7 +143,7 @@ export class HaConfigLovelaceRescources extends LitElement {
createResource: async (values) => {
const created = await createResource(this.hass!, values);
this._resources = this._resources!.concat(created).sort((res1, res2) =>
stringCompare(res1.url, res2.url)
stringCompare(res1.url, res2.url, this.hass!.locale.language)
);
loadLovelaceResources([created], this.hass!.auth.data.hassUrl);
},

View File

@ -156,10 +156,10 @@ class HaConfigPerson extends LitElement {
const personData = await fetchPersons(this.hass!);
this._storageItems = personData.storage.sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
);
this._configItems = personData.config.sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
);
this._openDialogIfPersonSpecifiedInRoute();
}
@ -221,7 +221,8 @@ class HaConfigPerson extends LitElement {
createEntry: async (values) => {
const created = await createPerson(this.hass!, values);
this._storageItems = this._storageItems!.concat(created).sort(
(ent1, ent2) => stringCompare(ent1.name, ent2.name)
(ent1, ent2) =>
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
);
},
updateEntry: async (values) => {

View File

@ -52,6 +52,7 @@ class HaConfigRepairs extends LitElement {
@click=${this._openShowMoreDialog}
>
<img
alt=${domainToName(this.hass.localize, issue.domain)}
loading="lazy"
src=${brandsUrl({
domain: issue.issue_domain || issue.domain,

View File

@ -65,6 +65,7 @@ class IntegrationsStartupTime extends LitElement {
href=${docLink}
>
<img
alt=""
loading="lazy"
src=${brandsUrl({
domain: setup.domain,

View File

@ -527,15 +527,10 @@ export class HaScriptTrace extends LitElement {
:host([narrow]) .graph {
max-width: 100%;
}
.info {
flex: 1;
background-color: var(--card-background-color);
}
.linkButton {
color: var(--primary-text-color);
}
.trace-link {
text-decoration: none;
}

View File

@ -244,7 +244,14 @@ class DialogTagDetail
canvas.height / 3
);
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
this._qrCode = html`<img
alt=${this.hass.localize(
"ui.panel.config.tag.qr_code_image",
"name",
this._name
)}
src=${canvas.toDataURL()}
></img>`;
}
static get styles(): CSSResultGroup {

View File

@ -296,7 +296,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
private async _fetchData() {
this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
);
this._getStates();
}
@ -411,7 +411,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
private async _createEntry(values: ZoneMutableParams) {
const created = await createZone(this.hass!, values);
this._storageItems = this._storageItems!.concat(created).sort(
(ent1, ent2) => stringCompare(ent1.name, ent2.name)
(ent1, ent2) =>
stringCompare(ent1.name, ent2.name, this.hass!.locale.language)
);
if (this.narrow) {
return;

View File

@ -58,7 +58,9 @@ class EventsList extends EventsMixin(LocalizeMixin(PolymerElement)) {
connectedCallback() {
super.connectedCallback();
this.hass.callApi("GET", "events").then((events) => {
this.events = events.sort((e1, e2) => stringCompare(e1.event, e2.event));
this.events = events.sort((e1, e2) =>
stringCompare(e1.event, e2.event, this.hass.locale.language)
);
});
}

View File

@ -79,7 +79,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
throw new Error("Parameter geo_location_sources needs to be an array");
}
this._config = config;
@ -102,6 +102,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
: "100";
return 1 + Math.floor(Number(ar) / 25) || 3;
}
@ -185,10 +186,21 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return false;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const root = this.shadowRoot!.getElementById("root");
protected updated(changedProps: PropertyValues): void {
if (this._config?.hours_to_show && this._configEntities?.length) {
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
this._getHistory();
}
}
if (changedProps.has("_config")) {
this._computePadding();
}
}
private _computePadding(): void {
const root = this.shadowRoot!.getElementById("root");
if (!this._config || this.isPanel || !root) {
return;
}
@ -206,16 +218,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
: (root.style.paddingBottom = "100%");
}
protected updated(changedProps: PropertyValues): void {
if (this._config?.hours_to_show && this._configEntities?.length) {
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
this._getHistory();
}
}
}
private _fitMap() {
this._map?.fitMap();
}

View File

@ -101,7 +101,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
),
})}
>
<img src=${this.hass.hassUrl(this._config.image)} />
<img
alt=${this._config.alt_text}
src=${this.hass.hassUrl(this._config.image)}
/>
</ha-card>
`;
}

View File

@ -332,6 +332,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
theme?: string;
alt_text?: string;
}
export interface PictureElementsCardConfig extends LovelaceCardConfig {

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