Merge dev and pin action versions

This commit is contained in:
Steve Repsher 2022-12-27 20:34:31 +00:00
commit 555c43caeb
No known key found for this signature in database
GPG Key ID: 776C4F2DACF6131B
38 changed files with 511 additions and 325 deletions

View File

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

View File

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

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3.2.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # 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 }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v3.2.0
with: with:
ref: dev ref: dev
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3.5.1
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -61,12 +61,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v3.2.0
with: with:
ref: master ref: master
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3.5.1
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -17,10 +17,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3.5.1
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn 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') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3 uses: actions/checkout@v3.2.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3.5.1
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

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

View File

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

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 90 days stale policy - name: 90 days stale policy
uses: actions/stale@v6.0.1 uses: actions/stale@v7.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90

View File

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

View File

@ -106,6 +106,7 @@
"core-js": "^3.15.2", "core-js": "^3.15.2",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.3.7",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",

View File

@ -0,0 +1,52 @@
import { HassEntity } from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
export const computeAttributeValueDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
entities: HomeAssistant["entities"],
attribute: string,
value?: any
): string => {
const entityId = stateObj.entity_id;
const attributeValue =
value !== undefined ? value : stateObj.attributes[attribute];
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (
(translationKey &&
localize(
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}`
)) ||
localize(
`component.${domain}.state_attributes._.${attribute}.state.${attributeValue}`
) ||
attributeValue
);
};
export const computeAttributeNameDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
entities: HomeAssistant["entities"],
attribute: string
): string => {
const entityId = stateObj.entity_id;
const domain = computeDomain(entityId);
const entity = entities[entityId] as EntityRegistryEntry | undefined;
const translationKey = entity?.translation_key;
return (
(translationKey &&
localize(
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.name`
)) ||
localize(`component.${domain}.state_attributes._.${attribute}.name`) ||
attribute
);
};

View File

@ -86,7 +86,7 @@ export const protocolIntegrationPicked = async (
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee", "ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{ {
integration: "Zigbee", integration: "Zigbee",
brand: options?.brand || options?.domain || "Z-Wave", brand: options?.brand || options?.domain || "Zigbee",
supported_hardware_link: html`<a supported_hardware_link: html`<a
href=${documentationUrl( href=${documentationUrl(
hass, hass,

View File

@ -1,5 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate"; import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState } from "../data/entity"; import { isUnavailableState } from "../data/entity";
@ -21,9 +23,12 @@ class HaClimateState extends LitElement {
${this.stateObj.attributes.preset_mode && ${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`- ? html`-
${this.hass.localize( ${computeAttributeValueDisplay(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` this.hass.localize,
) || this.stateObj.attributes.preset_mode}` this.stateObj,
this.hass.entities,
"preset_mode"
)}`
: ""} : ""}
</span> </span>
<div class="unit">${this._computeTarget()}</div>` <div class="unit">${this._computeTarget()}</div>`
@ -112,13 +117,19 @@ class HaClimateState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`); return this.hass.localize(`state.default.${this.stateObj.state}`);
} }
const stateString = this.hass.localize( const stateString = computeStateDisplay(
`component.climate.state._.${this.stateObj.state}` this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.entities
); );
return this.stateObj.attributes.hvac_action return this.stateObj.attributes.hvac_action
? `${this.hass.localize( ? `${computeAttributeValueDisplay(
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}` this.hass.localize,
this.stateObj,
this.hass.entities,
"hvac_action"
)} (${stateString})` )} (${stateString})`
: stateString; : stateString;
} }

View File

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

View File

@ -88,6 +88,10 @@ export class HaSelectSelector extends LitElement {
const value = const value =
!this.value || this.value === "" ? [] : (this.value as string[]); !this.value || this.value === "" ? [] : (this.value as string[]);
const optionItems = options.filter(
(option) => !option.disabled && !value?.includes(option.value)
);
return html` return html`
${value?.length ${value?.length
? html`<ha-chip-set> ? html`<ha-chip-set>
@ -118,11 +122,11 @@ export class HaSelectSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required && !value.length} .required=${this.required && !value.length}
.value=${this._filter} .value=${this._filter}
.filteredItems=${options.filter( .items=${optionItems}
(option) => !option.disabled && !value?.includes(option.value) .allowCustomValue=${this.selector.select.custom_value ?? false}
)}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged} @value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box> ></ha-combo-box>
`; `;
} }
@ -130,11 +134,14 @@ export class HaSelectSelector extends LitElement {
if (this.selector.select?.custom_value) { if (this.selector.select?.custom_value) {
if ( if (
this.value !== undefined && this.value !== undefined &&
!Array.isArray(this.value) &&
!options.find((option) => option.value === this.value) !options.find((option) => option.value === this.value)
) { ) {
options.unshift({ value: this.value, label: this.value }); options.unshift({ value: this.value, label: this.value });
} }
const optionItems = options.filter((option) => !option.disabled);
return html` return html`
<ha-combo-box <ha-combo-box
item-value-path="value" item-value-path="value"
@ -144,10 +151,11 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
.items=${options.filter((item) => !item.disabled)} .items=${optionItems}
.value=${this.value} .value=${this.value}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged} @value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box> ></ha-combo-box>
`; `;
} }
@ -190,7 +198,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail?.value || ev.target.value; const value = ev.detail?.value || ev.target.value;
if (this.disabled || !value) { if (this.disabled || value === undefined) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@ -271,13 +279,16 @@ export class HaSelectSelector extends LitElement {
}); });
} }
private _openedChanged(ev?: CustomEvent): void {
if (ev?.detail.value) {
this._filterChanged();
}
}
private _filterChanged(ev?: CustomEvent): void { private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || ""; this._filter = ev?.detail.value || "";
const filteredItems = this.comboBox.items?.filter((item) => { const filteredItems = this.comboBox.items?.filter((item) => {
if (this.selector.select?.multiple && this.value?.includes(item.value)) {
return false;
}
const label = item.label || item.value; const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase()); return label.toLowerCase().includes(this._filter?.toLowerCase());
}); });

View File

@ -23,63 +23,8 @@ interface CachedResults {
data: HistoryResult; data: HistoryResult;
} }
// This is a different interface, a different cache :(
interface RecentCacheResults {
created: number;
language: string;
data: Promise<HistoryResult>;
}
const RECENT_THRESHOLD = 60000; // 1 minute
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {}; const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cached type 1 function. Without cache config.
export const getRecent = (
hass: HomeAssistant,
entityId: string,
startTime: Date,
endTime: Date,
localize: LocalizeFunc,
language: string
) => {
const cacheKey = entityId;
const cache = RECENT_CACHE[cacheKey];
if (
cache &&
Date.now() - cache.created < RECENT_THRESHOLD &&
cache.language === language
) {
return cache.data;
}
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecentWS(
hass,
entityId,
startTime,
endTime,
false,
undefined,
true,
noAttributes
).then(
(stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => {
delete RECENT_CACHE[entityId];
throw err;
}
);
RECENT_CACHE[cacheKey] = {
created: Date.now(),
language,
data: prom,
};
return prom;
};
// Cache type 2 functionality // Cache type 2 functionality
function getEmptyCache( function getEmptyCache(
language: string, language: string,
@ -97,7 +42,7 @@ function getEmptyCache(
export const getRecentWithCache = ( export const getRecentWithCache = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityIds: string[],
cacheConfig: CacheConfig, cacheConfig: CacheConfig,
localize: LocalizeFunc, localize: LocalizeFunc,
language: string language: string
@ -132,7 +77,9 @@ export const getRecentWithCache = (
} }
const curCacheProm = cache.prom; const curCacheProm = cache.prom;
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const noAttributes = !entityIds.some((entityId) =>
entityIdHistoryNeedsAttributes(hass, entityId)
);
const genProm = async () => { const genProm = async () => {
let fetchedHistory: HistoryStates; let fetchedHistory: HistoryStates;
@ -142,7 +89,7 @@ export const getRecentWithCache = (
curCacheProm, curCacheProm,
fetchRecentWS( fetchRecentWS(
hass, hass,
entityId, entityIds,
toFetchStartTime, toFetchStartTime,
endTime, endTime,
appendingToCache, appendingToCache,

View File

@ -2,7 +2,6 @@ import {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { TranslationDict } from "../types";
export type HvacMode = export type HvacMode =
| "off" | "off"
@ -15,12 +14,13 @@ export type HvacMode =
export const CLIMATE_PRESET_NONE = "none"; export const CLIMATE_PRESET_NONE = "none";
type ClimateAttributes = TranslationDict["state_attributes"]["climate"]; export type HvacAction =
export type HvacAction = keyof ClimateAttributes["hvac_action"]; | "off"
export type FanMode = keyof ClimateAttributes["fan_mode"]; | "heating"
export type PresetMode = | "cooling"
| keyof ClimateAttributes["preset_mode"] | "drying"
| typeof CLIMATE_PRESET_NONE; | "idle"
| "fan";
export type ClimateEntity = HassEntityBase & { export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
@ -40,23 +40,25 @@ export type ClimateEntity = HassEntityBase & {
target_humidity_high?: number; target_humidity_high?: number;
min_humidity?: number; min_humidity?: number;
max_humidity?: number; max_humidity?: number;
fan_mode?: FanMode; fan_mode?: string;
fan_modes?: FanMode[]; fan_modes?: string[];
preset_mode?: PresetMode; preset_mode?: string;
preset_modes?: PresetMode[]; preset_modes?: string[];
swing_mode?: string; swing_mode?: string;
swing_modes?: string[]; swing_modes?: string[];
aux_heat?: "on" | "off"; aux_heat?: "on" | "off";
}; };
}; };
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1; export const enum ClimateEntityFeature {
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2; TARGET_TEMPERATURE = 1,
export const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4; TARGET_TEMPERATURE_RANGE = 2,
export const CLIMATE_SUPPORT_FAN_MODE = 8; TARGET_HUMIDITY = 4,
export const CLIMATE_SUPPORT_PRESET_MODE = 16; FAN_MODE = 8,
export const CLIMATE_SUPPORT_SWING_MODE = 32; PRESET_MODE = 16,
export const CLIMATE_SUPPORT_AUX_HEAT = 64; SWING_MODE = 32,
AUX_HEAT = 64,
}
const hvacModeOrdering: { [key in HvacMode]: number } = { const hvacModeOrdering: { [key in HvacMode]: number } = {
auto: 1, auto: 1,

View File

@ -451,7 +451,7 @@ const getEnergyData = async (
...(await fetchStatistics( ...(await fetchStatistics(
hass!, hass!,
compareStartMinHour, compareStartMinHour,
end, endCompare,
waterStatIds, waterStatIds,
period, period,
waterUnits, waterUnits,

View File

@ -1,4 +1,8 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import {
HassEntities,
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
@ -117,7 +121,7 @@ export const fetchRecent = (
export const fetchRecentWS = ( export const fetchRecentWS = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, // This may be CSV entityIds: string[],
startTime: Date, startTime: Date,
endTime: Date, endTime: Date,
skipInitialState = false, skipInitialState = false,
@ -133,7 +137,7 @@ export const fetchRecentWS = (
include_start_time_state: !skipInitialState, include_start_time_state: !skipInitialState,
minimal_response: minimalResponse, minimal_response: minimalResponse,
no_attributes: noAttributes || false, no_attributes: noAttributes || false,
entity_ids: entityId.split(","), entity_ids: entityIds,
}); });
export const fetchDate = ( export const fetchDate = (
@ -160,9 +164,9 @@ export const fetchDateWS = (
start_time: startTime.toISOString(), start_time: startTime.toISOString(),
end_time: endTime.toISOString(), end_time: endTime.toISOString(),
minimal_response: true, minimal_response: true,
no_attributes: !entityIds no_attributes: !entityIds.some((entityId) =>
.map((entityId) => entityIdHistoryNeedsAttributes(hass, entityId)) entityIdHistoryNeedsAttributes(hass, entityId)
.reduce((cur, next) => cur || next, false), ),
}; };
if (entityIds.length !== 0) { if (entityIds.length !== 0) {
return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds }); return hass.callWS<HistoryStates>({ ...params, entity_ids: entityIds });
@ -195,13 +199,22 @@ const processTimelineEntity = (
if (data.length > 0 && state.s === data[data.length - 1].state) { if (data.length > 0 && state.s === data[data.length - 1].state) {
continue; continue;
} }
const currentAttributes: HassEntityAttributeBase = {};
if (current_state?.attributes.device_class) {
currentAttributes.device_class = current_state?.attributes.device_class;
}
data.push({ data.push({
state_localize: computeStateDisplayFromEntityAttributes( state_localize: computeStateDisplayFromEntityAttributes(
localize, localize,
language, language,
entities, entities,
entityId, entityId,
state.a || first.a, {
...(state.a || first.a),
...currentAttributes,
},
state.s state.s
), ),
state: state.s, state: state.s,

View File

@ -44,6 +44,7 @@ declare global {
export type TranslationCategory = export type TranslationCategory =
| "title" | "title"
| "state" | "state"
| "state_attributes"
| "entity" | "entity"
| "config" | "config"
| "config_panel" | "config_panel"

View File

@ -11,6 +11,11 @@ import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-climate-control"; import "../../../components/ha-climate-control";
@ -19,13 +24,7 @@ import "../../../components/ha-slider";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import { import {
ClimateEntity, ClimateEntity,
CLIMATE_SUPPORT_AUX_HEAT, ClimateEntityFeature,
CLIMATE_SUPPORT_FAN_MODE,
CLIMATE_SUPPORT_PRESET_MODE,
CLIMATE_SUPPORT_SWING_MODE,
CLIMATE_SUPPORT_TARGET_HUMIDITY,
CLIMATE_SUPPORT_TARGET_TEMPERATURE,
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE,
compareClimateHvacModes, compareClimateHvacModes,
} from "../../../data/climate"; } from "../../../data/climate";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -47,26 +46,32 @@ class MoreInfoClimate extends LitElement {
const supportTargetTemperature = supportsFeature( const supportTargetTemperature = supportsFeature(
stateObj, stateObj,
CLIMATE_SUPPORT_TARGET_TEMPERATURE ClimateEntityFeature.TARGET_TEMPERATURE
); );
const supportTargetTemperatureRange = supportsFeature( const supportTargetTemperatureRange = supportsFeature(
stateObj, stateObj,
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
); );
const supportTargetHumidity = supportsFeature( const supportTargetHumidity = supportsFeature(
stateObj, stateObj,
CLIMATE_SUPPORT_TARGET_HUMIDITY ClimateEntityFeature.TARGET_HUMIDITY
);
const supportFanMode = supportsFeature(
stateObj,
ClimateEntityFeature.FAN_MODE
); );
const supportFanMode = supportsFeature(stateObj, CLIMATE_SUPPORT_FAN_MODE);
const supportPresetMode = supportsFeature( const supportPresetMode = supportsFeature(
stateObj, stateObj,
CLIMATE_SUPPORT_PRESET_MODE ClimateEntityFeature.PRESET_MODE
); );
const supportSwingMode = supportsFeature( const supportSwingMode = supportsFeature(
stateObj, stateObj,
CLIMATE_SUPPORT_SWING_MODE ClimateEntityFeature.SWING_MODE
);
const supportAuxHeat = supportsFeature(
stateObj,
ClimateEntityFeature.AUX_HEAT
); );
const supportAuxHeat = supportsFeature(stateObj, CLIMATE_SUPPORT_AUX_HEAT);
const temperatureStepSize = const temperatureStepSize =
stateObj.attributes.target_temp_step || stateObj.attributes.target_temp_step ||
@ -94,7 +99,12 @@ class MoreInfoClimate extends LitElement {
${supportTargetTemperature || supportTargetTemperatureRange ${supportTargetTemperature || supportTargetTemperatureRange
? html` ? html`
<div> <div>
${hass.localize("ui.card.climate.target_temperature")} ${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"temperature"
)}
</div> </div>
` `
: ""} : ""}
@ -145,7 +155,14 @@ class MoreInfoClimate extends LitElement {
${supportTargetHumidity ${supportTargetHumidity
? html` ? html`
<div class="container-humidity"> <div class="container-humidity">
<div>${hass.localize("ui.card.climate.target_humidity")}</div> <div>
${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"humidity"
)}
</div>
<div class="single-row"> <div class="single-row">
<div class="target-humidity"> <div class="target-humidity">
${stateObj.attributes.humidity} % ${stateObj.attributes.humidity} %
@ -182,7 +199,13 @@ class MoreInfoClimate extends LitElement {
.map( .map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode}> <mwc-list-item .value=${mode}>
${hass.localize(`component.climate.state._.${mode}`)} ${computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.entities,
mode
)}
</mwc-list-item> </mwc-list-item>
` `
)} )}
@ -194,7 +217,12 @@ class MoreInfoClimate extends LitElement {
? html` ? html`
<div class="container-preset_modes"> <div class="container-preset_modes">
<ha-select <ha-select
.label=${hass.localize("ui.card.climate.preset_mode")} .label=${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode} .value=${stateObj.attributes.preset_mode}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@ -204,9 +232,13 @@ class MoreInfoClimate extends LitElement {
${stateObj.attributes.preset_modes!.map( ${stateObj.attributes.preset_modes!.map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode}> <mwc-list-item .value=${mode}>
${hass.localize( ${computeAttributeValueDisplay(
`state_attributes.climate.preset_mode.${mode}` hass.localize,
) || mode} stateObj,
hass.entities,
"preset_mode",
mode
)}
</mwc-list-item> </mwc-list-item>
` `
)} )}
@ -218,7 +250,12 @@ class MoreInfoClimate extends LitElement {
? html` ? html`
<div class="container-fan_list"> <div class="container-fan_list">
<ha-select <ha-select
.label=${hass.localize("ui.card.climate.fan_mode")} .label=${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode} .value=${stateObj.attributes.fan_mode}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@ -228,9 +265,13 @@ class MoreInfoClimate extends LitElement {
${stateObj.attributes.fan_modes!.map( ${stateObj.attributes.fan_modes!.map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode}> <mwc-list-item .value=${mode}>
${hass.localize( ${computeAttributeValueDisplay(
`state_attributes.climate.fan_mode.${mode}` hass.localize,
) || mode} stateObj,
hass.entities,
"fan_mode",
mode
)}
</mwc-list-item> </mwc-list-item>
` `
)} )}
@ -242,7 +283,12 @@ class MoreInfoClimate extends LitElement {
? html` ? html`
<div class="container-swing_list"> <div class="container-swing_list">
<ha-select <ha-select
.label=${hass.localize("ui.card.climate.swing_mode")} .label=${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode} .value=${stateObj.attributes.swing_mode}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@ -251,7 +297,15 @@ class MoreInfoClimate extends LitElement {
> >
${stateObj.attributes.swing_modes!.map( ${stateObj.attributes.swing_modes!.map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode}>${mode}</mwc-list-item> <mwc-list-item .value=${mode}>
${computeAttributeValueDisplay(
hass.localize,
stateObj,
hass.entities,
"swing_mode",
mode
)}
</mwc-list-item>
` `
)} )}
</ha-select> </ha-select>
@ -263,7 +317,12 @@ class MoreInfoClimate extends LitElement {
<div class="container-aux_heat"> <div class="container-aux_heat">
<div class="center horizontal layout single-row"> <div class="center horizontal layout single-row">
<div class="flex"> <div class="flex">
${hass.localize("ui.card.climate.aux_heat")} ${computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
"aux_heat"
)}
</div> </div>
<ha-switch <ha-switch
.checked=${stateObj.attributes.aux_heat === "on"} .checked=${stateObj.attributes.aux_heat === "on"}

View File

@ -139,7 +139,7 @@ export class MoreInfoHistory extends LitElement {
} }
this._stateHistory = await getRecentWithCache( this._stateHistory = await getRecentWithCache(
this.hass!, this.hass!,
this.entityId, [this.entityId],
{ {
cacheKey: `more_info.${this.entityId}`, cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24, hoursToShow: 24,

View File

@ -138,6 +138,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
// @ts-ignore // @ts-ignore
this._loadHassTranslations(this.hass!.language, "state"); this._loadHassTranslations(this.hass!.language, "state");
// @ts-ignore // @ts-ignore
this._loadHassTranslations(this.hass!.language, "state_attributes");
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "entity"); this._loadHassTranslations(this.hass!.language, "entity");
document.addEventListener( document.addEventListener(

View File

@ -1,6 +1,7 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCalendarClock, mdiClose } from "@mdi/js"; import { mdiCalendarClock, mdiClose } from "@mdi/js";
import { addDays, isSameDay } from "date-fns/esm"; import { addDays, isSameDay } from "date-fns/esm";
import { toDate } from "date-fns-tz";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { RRule, Weekday } from "rrule"; import { RRule, Weekday } from "rrule";
@ -185,11 +186,12 @@ class DialogCalendarEventDetail extends LitElement {
}; };
private _formatDateRange() { private _formatDateRange() {
const start = new Date(this._data!.dtstart); // Parse a dates in the browser timezone
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const start = toDate(this._data!.dtstart, { timeZone: timeZone });
const endValue = toDate(this._data!.dtend, { timeZone: timeZone });
// All day events should be displayed as a day earlier // All day events should be displayed as a day earlier
const end = isDate(this._data.dtend) const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
? addDays(new Date(this._data!.dtend), -1)
: new Date(this._data!.dtend);
// The range can be shortened when the start and end are on the same day. // The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) { if (isSameDay(start, end)) {
if (isDate(this._data.dtstart)) { if (isDate(this._data.dtstart)) {

View File

@ -7,6 +7,7 @@ import {
differenceInMilliseconds, differenceInMilliseconds,
startOfHour, startOfHour,
} from "date-fns/esm"; } from "date-fns/esm";
import { formatInTimeZone, toDate } from "date-fns-tz";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -60,6 +61,12 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
// Dates are manipulated and displayed in the browser timezone
// which may be different from the Home Assistant timezone. When
// events are persisted, they are relative to the Home Assistant
// timezone, but floating without a timezone.
private _timeZone?: string;
public showDialog(params: CalendarEventEditDialogParams): void { public showDialog(params: CalendarEventEditDialogParams): void {
this._error = undefined; this._error = undefined;
this._info = undefined; this._info = undefined;
@ -71,6 +78,9 @@ class DialogCalendarEventEditor extends LitElement {
computeStateDomain(stateObj) === "calendar" && computeStateDomain(stateObj) === "calendar" &&
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT) supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
)?.entity_id; )?.entity_id;
this._timeZone =
Intl.DateTimeFormat().resolvedOptions().timeZone ||
this.hass.config.time_zone;
if (params.entry) { if (params.entry) {
const entry = params.entry!; const entry = params.entry!;
this._allDay = isDate(entry.dtstart); this._allDay = isDate(entry.dtstart);
@ -281,20 +291,30 @@ class DialogCalendarEventEditor extends LitElement {
private _isEditableCalendar = (entityStateObj: HassEntity) => private _isEditableCalendar = (entityStateObj: HassEntity) =>
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT); supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT);
private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) => private _getLocaleStrings = memoizeOne(
// en-CA locale used for date format YYYY-MM-DD (startDate?: Date, endDate?: Date) => ({
// en-GB locale used for 24h time format HH:MM:SS startDate: this._formatDate(startDate!),
{ startTime: this._formatTime(startDate!),
const timeZone = this.hass.config.time_zone; endDate: this._formatDate(endDate!),
return { endTime: this._formatTime(endDate!),
startDate: startDate?.toLocaleDateString("en-CA", { timeZone }), })
startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }),
endDate: endDate?.toLocaleDateString("en-CA", { timeZone }),
endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }),
};
}
); );
// Formats a date in specified timezone, or defaulting to browser display timezone
private _formatDate(date: Date, timeZone: string = this._timeZone!): string {
return formatInTimeZone(date, timeZone, "yyyy-MM-dd");
}
// Formats a time in specified timezone, or defaulting to browser display timezone
private _formatTime(date: Date, timeZone: string = this._timeZone!): string {
return formatInTimeZone(date, timeZone, "HH:mm:ss"); // 24 hr
}
// Parse a date in the browser timezone
private _parseDate(dateStr: string): Date {
return toDate(dateStr, { timeZone: this._timeZone! });
}
private _clearInfo() { private _clearInfo() {
this._info = undefined; this._info = undefined;
} }
@ -319,27 +339,14 @@ class DialogCalendarEventEditor extends LitElement {
// Store previous event duration // Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = this._parseDate(
ev.detail.value + `${ev.detail.value}T${this._formatTime(this._dtstart!)}`
"T" +
this._dtstart!.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})
); );
// Prevent that the end time can be before the start time. Try to keep the // Prevent that the end time can be before the start time. Try to keep the
// duration the same. // duration the same.
if (this._dtend! <= this._dtstart!) { if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(this._dtstart, duration); this._dtend = addMilliseconds(this._dtstart, duration);
// en-CA locale used for date format YYYY-MM-DD
// en-GB locale used for 24h time format HH:MM:SS
this._dtend = new Date(
`${newEnd.toLocaleDateString("en-CA", {
timeZone: this.hass.config.time_zone,
})}T${newEnd.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})}`
);
this._info = this.hass.localize( this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted" "ui.components.calendar.event.end_auto_adjusted"
); );
@ -347,12 +354,8 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _endDateChanged(ev: CustomEvent) { private _endDateChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = this._parseDate(
ev.detail.value + `${ev.detail.value}T${this._formatTime(this._dtend!)}`
"T" +
this._dtend!.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})
); );
} }
@ -360,25 +363,14 @@ class DialogCalendarEventEditor extends LitElement {
// Store previous event duration // Store previous event duration
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!); const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
this._dtstart = new Date( this._dtstart = this._parseDate(
this._dtstart!.toLocaleDateString("en-CA", { `${this._formatDate(this._dtstart!)}T${ev.detail.value}`
timeZone: this.hass.config.time_zone,
}) +
"T" +
ev.detail.value
); );
// Prevent that the end time can be before the start time. Try to keep the // Prevent that the end time can be before the start time. Try to keep the
// duration the same. // duration the same.
if (this._dtend! <= this._dtstart!) { if (this._dtend! <= this._dtstart!) {
const newEnd = addMilliseconds(new Date(this._dtstart), duration); this._dtend = addMilliseconds(new Date(this._dtstart), duration);
this._dtend = new Date(
`${newEnd.toLocaleDateString("en-CA", {
timeZone: this.hass.config.time_zone,
})}T${newEnd.toLocaleTimeString("en-GB", {
timeZone: this.hass.config.time_zone,
})}`
);
this._info = this.hass.localize( this._info = this.hass.localize(
"ui.components.calendar.event.end_auto_adjusted" "ui.components.calendar.event.end_auto_adjusted"
); );
@ -386,36 +378,32 @@ class DialogCalendarEventEditor extends LitElement {
} }
private _endTimeChanged(ev: CustomEvent) { private _endTimeChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = this._parseDate(
this._dtend!.toLocaleDateString("en-CA", { `${this._formatDate(this._dtend!)}T${ev.detail.value}`
timeZone: this.hass.config.time_zone,
}) +
"T" +
ev.detail.value
); );
} }
private _calculateData() { private _calculateData() {
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
this._dtstart,
this._dtend
);
const data: CalendarEventMutableParams = { const data: CalendarEventMutableParams = {
summary: this._summary, summary: this._summary,
description: this._description, description: this._description,
rrule: this._rrule, rrule: this._rrule || undefined,
dtstart: "", dtstart: "",
dtend: "", dtend: "",
}; };
if (this._allDay) { if (this._allDay) {
data.dtstart = startDate!; data.dtstart = this._formatDate(this._dtstart!);
// End date/time is exclusive when persisted // End date/time is exclusive when persisted
data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString( data.dtend = this._formatDate(addDays(this._dtend!, 1));
"en-CA"
);
} else { } else {
data.dtstart = `${startDate}T${startTime}`; data.dtstart = `${this._formatDate(
data.dtend = `${endDate}T${endTime}`; this._dtstart!,
this.hass.config.time_zone
)}T${this._formatTime(this._dtstart!, this.hass.config.time_zone)}`;
data.dtend = `${this._formatDate(
this._dtend!,
this.hass.config.time_zone
)}T${this._formatTime(this._dtend!, this.hass.config.time_zone)}`;
} }
return data; return data;
} }

View File

@ -1,5 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import { import {
@ -13,6 +13,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-alert"; import "../../../../../components/ha-alert";
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { navigate } from "../../../../../common/navigate";
@customElement("matter-config-panel") @customElement("matter-config-panel")
export class MatterConfigPanel extends LitElement { export class MatterConfigPanel extends LitElement {
@ -22,6 +23,8 @@ export class MatterConfigPanel extends LitElement {
@state() private _error?: string; @state() private _error?: string;
private _curMatterDevices?: Set<string>;
private get _canCommissionMatter() { private get _canCommissionMatter() {
return this.hass.auth.external?.config.canCommissionMatter; return this.hass.auth.external?.config.canCommissionMatter;
} }
@ -68,7 +71,30 @@ export class MatterConfigPanel extends LitElement {
`; `;
} }
protected override updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._curMatterDevices || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.devices === this.hass.devices) {
return;
}
const newMatterDevices = Object.values(this.hass.devices).filter(
(device) =>
device.identifiers.find((identifier) => identifier[0] === "matter") &&
!this._curMatterDevices!.has(device.id)
);
if (newMatterDevices.length) {
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
}
}
private _startMobileCommissioning() { private _startMobileCommissioning() {
this._redirectOnNewDevice();
this.hass.auth.external!.fireMessage({ this.hass.auth.external!.fireMessage({
type: "matter/commission", type: "matter/commission",
}); });
@ -112,6 +138,7 @@ export class MatterConfigPanel extends LitElement {
return; return;
} }
this._error = undefined; this._error = undefined;
this._redirectOnNewDevice();
try { try {
await commissionMatterDevice(this.hass, code); await commissionMatterDevice(this.hass, code);
} catch (err: any) { } catch (err: any) {
@ -130,6 +157,7 @@ export class MatterConfigPanel extends LitElement {
return; return;
} }
this._error = undefined; this._error = undefined;
this._redirectOnNewDevice();
try { try {
await acceptSharedMatterDevice(this.hass, Number(code)); await acceptSharedMatterDevice(this.hass, Number(code));
} catch (err: any) { } catch (err: any) {
@ -155,6 +183,19 @@ export class MatterConfigPanel extends LitElement {
} }
} }
private _redirectOnNewDevice() {
if (this._curMatterDevices) {
return;
}
this._curMatterDevices = new Set(
Object.values(this.hass.devices)
.filter((device) =>
device.identifiers.find((identifier) => identifier[0] === "matter")
)
.map((device) => device.id)
);
}
static styles = [ static styles = [
haStyle, haStyle,
css` css`

View File

@ -50,6 +50,7 @@ class HaPanelDevMqtt extends LitElement {
)} )}
> >
<div class="card-content"> <div class="card-content">
<div class="panel-dev-mqtt-fields">
<ha-textfield <ha-textfield
.label=${this.hass.localize("ui.panel.config.mqtt.topic")} .label=${this.hass.localize("ui.panel.config.mqtt.topic")}
.value=${this.topic} .value=${this.topic}
@ -72,6 +73,7 @@ class HaPanelDevMqtt extends LitElement {
.checked=${this.retain} .checked=${this.retain}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
</div>
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p> <p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
<ha-code-editor <ha-code-editor
mode="jinja2" mode="jinja2"
@ -160,6 +162,28 @@ class HaPanelDevMqtt extends LitElement {
margin: 0 auto; margin: 0 auto;
direction: ltr; direction: ltr;
} }
.panel-dev-mqtt-fields {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
ha-select {
width: 96px;
margin: 0 8px;
}
ha-textfield {
flex: 1;
}
@media screen and (max-width: 600px) {
ha-select {
margin-left: 0px;
margin-top: 8px;
}
ha-textfield {
flex: auto;
width: 100%;
}
}
ha-card:first-child { ha-card:first-child {
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@ -47,6 +47,7 @@ class MqttSubscribeCard extends LitElement {
header=${this.hass.localize("ui.panel.config.mqtt.description_listen")} header=${this.hass.localize("ui.panel.config.mqtt.description_listen")}
> >
<form> <form>
<div class="panel-dev-mqtt-subscribe-fields">
<ha-textfield <ha-textfield
.label=${this._subscribed .label=${this._subscribed
? this.hass.localize("ui.panel.config.mqtt.listening_to") ? this.hass.localize("ui.panel.config.mqtt.listening_to")
@ -61,7 +62,8 @@ class MqttSubscribeCard extends LitElement {
.value=${this._qos} .value=${this._qos}
@selected=${this._handleQos} @selected=${this._handleQos}
>${qosLevel.map( >${qosLevel.map(
(qos) => html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>` (qos) =>
html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>`
)} )}
</ha-select> </ha-select>
<mwc-button <mwc-button
@ -73,6 +75,7 @@ class MqttSubscribeCard extends LitElement {
? this.hass.localize("ui.panel.config.mqtt.stop_listening") ? this.hass.localize("ui.panel.config.mqtt.stop_listening")
: this.hass.localize("ui.panel.config.mqtt.start_listening")} : this.hass.localize("ui.panel.config.mqtt.start_listening")}
</mwc-button> </mwc-button>
</div>
</form> </form>
<div class="events"> <div class="events">
${this._messages.map( ${this._messages.map(
@ -170,6 +173,28 @@ class MqttSubscribeCard extends LitElement {
pre { pre {
font-family: var(--code-font-family, monospace); font-family: var(--code-font-family, monospace);
} }
.panel-dev-mqtt-subscribe-fields {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
ha-select {
width: 96px;
margin: 0 8px;
}
ha-textfield {
flex: 1;
}
@media screen and (max-width: 600px) {
ha-select {
margin-left: 0px;
margin-top: 8px;
}
ha-textfield {
flex: auto;
width: 100%;
}
}
`; `;
} }
} }

View File

@ -279,7 +279,9 @@ export class HuiEnergySourcesTableCard
? html`<tr class="mdc-data-table__row total"> ? html`<tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td> <td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row"> <th class="mdc-data-table__cell" scope="row">
Solar total ${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_sources_table.solar_total"
)}
</th> </th>
${compare ${compare
? html`<td ? html`<td

View File

@ -295,7 +295,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
} }
.triggered { .triggered {
--alarm-state-color: rgb(var(--rgb-state-alarm-trigger-color)); --alarm-state-color: rgb(var(--rgb-state-alarm-triggered-color));
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }

View File

@ -162,7 +162,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
this._stateHistory = { this._stateHistory = {
...(await getRecentWithCache( ...(await getRecentWithCache(
this.hass!, this.hass!,
this._cacheConfig!.cacheKey, this._configEntities!.map((config) => config.entity),
this._cacheConfig!, this._cacheConfig!,
this.hass!.localize, this.hass!.localize,
this.hass!.language this.hass!.language

View File

@ -24,6 +24,8 @@ import { classMap } from "lit/directives/class-map";
import { UNIT_F } from "../../../common/const"; import { UNIT_F } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card"; import "../../../components/ha-card";
@ -213,11 +215,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
> >
${ ${
stateObj.attributes.hvac_action stateObj.attributes.hvac_action
? this.hass!.localize( ? computeAttributeValueDisplay(
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}` this.hass.localize,
stateObj,
this.hass.entities,
"hvac_action"
) )
: this.hass!.localize( : computeStateDisplay(
`component.climate.state._.${stateObj.state}` this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
) )
} }
${ ${
@ -225,9 +233,12 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html` ? html`
- -
${this.hass!.localize( ${computeAttributeValueDisplay(
`state_attributes.climate.preset_mode.${stateObj.attributes.preset_mode}` this.hass.localize,
) || stateObj.attributes.preset_mode} stateObj,
this.hass.entities,
"preset_mode"
)}
` `
: "" : ""
} }

View File

@ -193,7 +193,7 @@ export class HuiImage extends LitElement {
style=${styleMap({ style=${styleMap({
paddingBottom: useRatio paddingBottom: useRatio
? `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%` ? `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`
: !this._lastImageHeight : this._lastImageHeight === undefined
? "56.25%" ? "56.25%"
: undefined, : undefined,
backgroundImage: backgroundImage:
@ -206,7 +206,7 @@ export class HuiImage extends LitElement {
: undefined, : undefined,
})} })}
class="container ${classMap({ class="container ${classMap({
ratio: useRatio || !this._lastImageHeight, ratio: useRatio || this._lastImageHeight === undefined,
})}" })}"
> >
${this.cameraImage && this.cameraView === "live" ${this.cameraImage && this.cameraView === "live"

View File

@ -38,9 +38,11 @@ import { fireEvent } from "../../common/dom/fire_event";
import scrollToTarget from "../../common/dom/scroll-to-target"; import scrollToTarget from "../../common/dom/scroll-to-target";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import { import {
addSearchParam, addSearchParam,
extractSearchParam, extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params"; } from "../../common/url/search-params";
import { computeRTLDirection } from "../../common/util/compute_rtl"; import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
@ -556,8 +558,16 @@ class HUIRoot extends LitElement {
protected firstUpdated() { protected firstUpdated() {
// Check for requested edit mode // Check for requested edit mode
if (extractSearchParam("edit") === "1") { const searchParams = extractSearchParamsObject();
if (searchParams.edit === "1") {
this.lovelace!.setEditMode(true); this.lovelace!.setEditMode(true);
} else if (searchParams.conversation === "1") {
showVoiceCommandDialog(this);
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam("conversation"))
);
} }
} }

View File

@ -41,6 +41,10 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
server_controls: { server_controls: {
redirect: "/developer-tools/yaml", redirect: "/developer-tools/yaml",
}, },
calendar: {
component: "calendar",
redirect: "/calendar",
},
config: { config: {
redirect: "/config/dashboard", redirect: "/config/dashboard",
}, },

View File

@ -22,31 +22,6 @@
} }
}, },
"state_attributes": { "state_attributes": {
"climate": {
"fan_mode": {
"off": "Off",
"on": "On",
"auto": "Auto"
},
"preset_mode": {
"none": "None",
"eco": "Eco",
"away": "Away",
"boost": "Boost",
"comfort": "Comfort",
"home": "Home",
"sleep": "Sleep",
"activity": "Activity"
},
"hvac_action": {
"off": "Off",
"heating": "Heating",
"cooling": "Cooling",
"drying": "Drying",
"idle": "Idle",
"fan": "Fan"
}
},
"humidifier": { "humidifier": {
"mode": { "mode": {
"normal": "Normal", "normal": "Normal",
@ -140,7 +115,6 @@
"climate": { "climate": {
"currently": "Currently", "currently": "Currently",
"on_off": "On / off", "on_off": "On / off",
"target_temperature": "Target temperature",
"target_temperature_entity": "{name} target temperature", "target_temperature_entity": "{name} target temperature",
"target_temperature_mode": "{name} target temperature {mode}", "target_temperature_mode": "{name} target temperature {mode}",
"current_temperature": "{name} current temperature", "current_temperature": "{name} current temperature",
@ -148,13 +122,8 @@
"cooling": "{name} cooling", "cooling": "{name} cooling",
"high": "high", "high": "high",
"low": "low", "low": "low",
"target_humidity": "Target humidity",
"operation": "Operation", "operation": "Operation",
"fan_mode": "Fan mode", "away_mode": "Away mode"
"swing_mode": "Swing mode",
"preset_mode": "Preset",
"away_mode": "Away mode",
"aux_heat": "Aux heat"
}, },
"counter": { "counter": {
"actions": { "actions": {
@ -3225,7 +3194,7 @@
"mqtt": { "mqtt": {
"title": "MQTT", "title": "MQTT",
"description_publish": "Publish a packet", "description_publish": "Publish a packet",
"topic": "topic", "topic": "Topic",
"payload": "Payload (template allowed)", "payload": "Payload (template allowed)",
"publish": "Publish", "publish": "Publish",
"description_listen": "Listen to a topic", "description_listen": "Listen to a topic",
@ -3808,6 +3777,7 @@
"energy_sources_table": { "energy_sources_table": {
"grid_total": "Grid total", "grid_total": "Grid total",
"gas_total": "Gas total", "gas_total": "Gas total",
"solar_total": "Solar total",
"water_total": "Water total", "water_total": "Water total",
"source": "Source", "source": "Source",
"energy": "Energy", "energy": "Energy",

View File

@ -6993,6 +6993,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"date-fns-tz@npm:^1.3.7":
version: 1.3.7
resolution: "date-fns-tz@npm:1.3.7"
peerDependencies:
date-fns: ">=2.0.0"
checksum: b749613669223056d5e6d715114c94bec57234b676d0cea0c72ca710626c81e9ea04df6441852a5fec74b42c5f27b2f076e13697ec43da360b67806a3042a10e
languageName: node
linkType: hard
"date-fns@npm:^2.23.0": "date-fns@npm:^2.23.0":
version: 2.23.0 version: 2.23.0
resolution: "date-fns@npm:2.23.0" resolution: "date-fns@npm:2.23.0"
@ -9427,6 +9436,7 @@ fsevents@^1.2.7:
core-js: ^3.15.2 core-js: ^3.15.2
cropperjs: ^1.5.12 cropperjs: ^1.5.12
date-fns: ^2.23.0 date-fns: ^2.23.0
date-fns-tz: ^1.3.7
deep-clone-simple: ^1.1.1 deep-clone-simple: ^1.1.1
deep-freeze: ^0.0.1 deep-freeze: ^0.0.1
del: ^4.0.0 del: ^4.0.0