mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-03 22:47:47 +00:00
Merge dev and pin action versions
This commit is contained in:
commit
555c43caeb
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@ -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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
20
.github/workflows/ci.yaml
vendored
20
.github/workflows/ci.yaml
vendored
@ -32,13 +32,13 @@ jobs:
|
||||
sha: ${{ steps.get-sha.outputs.sha }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.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
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: ${{ needs.dedupe.outputs.sha }}
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: ${{ needs.dedupe.outputs.sha }}
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: ${{ needs.dedupe.outputs.sha }}
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: ${{ needs.dedupe.outputs.sha }}
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@ -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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@ -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
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@ -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
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.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
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.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
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v6.0.1
|
||||
uses: actions/stale@v7.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@ -106,6 +106,7 @@
|
||||
"core-js": "^3.15.2",
|
||||
"cropperjs": "^1.5.12",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
|
52
src/common/entity/compute_attribute_display.ts
Normal file
52
src/common/entity/compute_attribute_display.ts
Normal 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
|
||||
);
|
||||
};
|
@ -86,7 +86,7 @@ export const protocolIntegrationPicked = async (
|
||||
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
||||
{
|
||||
integration: "Zigbee",
|
||||
brand: options?.brand || options?.domain || "Z-Wave",
|
||||
brand: options?.brand || options?.domain || "Zigbee",
|
||||
supported_hardware_link: html`<a
|
||||
href=${documentationUrl(
|
||||
hass,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
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 { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import { isUnavailableState } from "../data/entity";
|
||||
@ -21,9 +23,12 @@ class HaClimateState extends LitElement {
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this.hass.localize(
|
||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||
) || this.stateObj.attributes.preset_mode}`
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"preset_mode"
|
||||
)}`
|
||||
: ""}
|
||||
</span>
|
||||
<div class="unit">${this._computeTarget()}</div>`
|
||||
@ -112,13 +117,19 @@ class HaClimateState extends LitElement {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = this.hass.localize(
|
||||
`component.climate.state._.${this.stateObj.state}`
|
||||
const stateString = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return this.stateObj.attributes.hvac_action
|
||||
? `${this.hass.localize(
|
||||
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
|
||||
? `${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"hvac_action"
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.disabled=${item.disabled || this.disabled}
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
@ -95,7 +95,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item, this.data),
|
||||
helper: this._computeHelper(item),
|
||||
disabled: this.disabled || item.disabled,
|
||||
disabled: this.disabled || item.disabled || false,
|
||||
hass: this.hass,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
|
@ -88,6 +88,10 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||
|
||||
const optionItems = options.filter(
|
||||
(option) => !option.disabled && !value?.includes(option.value)
|
||||
);
|
||||
|
||||
return html`
|
||||
${value?.length
|
||||
? html`<ha-chip-set>
|
||||
@ -118,11 +122,11 @@ export class HaSelectSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._filter}
|
||||
.filteredItems=${options.filter(
|
||||
(option) => !option.disabled && !value?.includes(option.value)
|
||||
)}
|
||||
.items=${optionItems}
|
||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
@ -130,11 +134,14 @@ export class HaSelectSelector extends LitElement {
|
||||
if (this.selector.select?.custom_value) {
|
||||
if (
|
||||
this.value !== undefined &&
|
||||
!Array.isArray(this.value) &&
|
||||
!options.find((option) => option.value === this.value)
|
||||
) {
|
||||
options.unshift({ value: this.value, label: this.value });
|
||||
}
|
||||
|
||||
const optionItems = options.filter((option) => !option.disabled);
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
@ -144,10 +151,11 @@ export class HaSelectSelector extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.items=${options.filter((item) => !item.disabled)}
|
||||
.items=${optionItems}
|
||||
.value=${this.value}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
@ -190,7 +198,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail?.value || ev.target.value;
|
||||
if (this.disabled || !value) {
|
||||
if (this.disabled || value === undefined) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
this._filter = ev?.detail.value || "";
|
||||
|
||||
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;
|
||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||
});
|
||||
|
@ -23,63 +23,8 @@ interface CachedResults {
|
||||
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 } = {};
|
||||
|
||||
// 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
|
||||
function getEmptyCache(
|
||||
language: string,
|
||||
@ -97,7 +42,7 @@ function getEmptyCache(
|
||||
|
||||
export const getRecentWithCache = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
entityIds: string[],
|
||||
cacheConfig: CacheConfig,
|
||||
localize: LocalizeFunc,
|
||||
language: string
|
||||
@ -132,7 +77,9 @@ export const getRecentWithCache = (
|
||||
}
|
||||
|
||||
const curCacheProm = cache.prom;
|
||||
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
|
||||
const noAttributes = !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
);
|
||||
|
||||
const genProm = async () => {
|
||||
let fetchedHistory: HistoryStates;
|
||||
@ -142,7 +89,7 @@ export const getRecentWithCache = (
|
||||
curCacheProm,
|
||||
fetchRecentWS(
|
||||
hass,
|
||||
entityId,
|
||||
entityIds,
|
||||
toFetchStartTime,
|
||||
endTime,
|
||||
appendingToCache,
|
||||
|
@ -2,7 +2,6 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { TranslationDict } from "../types";
|
||||
|
||||
export type HvacMode =
|
||||
| "off"
|
||||
@ -15,12 +14,13 @@ export type HvacMode =
|
||||
|
||||
export const CLIMATE_PRESET_NONE = "none";
|
||||
|
||||
type ClimateAttributes = TranslationDict["state_attributes"]["climate"];
|
||||
export type HvacAction = keyof ClimateAttributes["hvac_action"];
|
||||
export type FanMode = keyof ClimateAttributes["fan_mode"];
|
||||
export type PresetMode =
|
||||
| keyof ClimateAttributes["preset_mode"]
|
||||
| typeof CLIMATE_PRESET_NONE;
|
||||
export type HvacAction =
|
||||
| "off"
|
||||
| "heating"
|
||||
| "cooling"
|
||||
| "drying"
|
||||
| "idle"
|
||||
| "fan";
|
||||
|
||||
export type ClimateEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
@ -40,23 +40,25 @@ export type ClimateEntity = HassEntityBase & {
|
||||
target_humidity_high?: number;
|
||||
min_humidity?: number;
|
||||
max_humidity?: number;
|
||||
fan_mode?: FanMode;
|
||||
fan_modes?: FanMode[];
|
||||
preset_mode?: PresetMode;
|
||||
preset_modes?: PresetMode[];
|
||||
fan_mode?: string;
|
||||
fan_modes?: string[];
|
||||
preset_mode?: string;
|
||||
preset_modes?: string[];
|
||||
swing_mode?: string;
|
||||
swing_modes?: string[];
|
||||
aux_heat?: "on" | "off";
|
||||
};
|
||||
};
|
||||
|
||||
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1;
|
||||
export const CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE = 2;
|
||||
export const CLIMATE_SUPPORT_TARGET_HUMIDITY = 4;
|
||||
export const CLIMATE_SUPPORT_FAN_MODE = 8;
|
||||
export const CLIMATE_SUPPORT_PRESET_MODE = 16;
|
||||
export const CLIMATE_SUPPORT_SWING_MODE = 32;
|
||||
export const CLIMATE_SUPPORT_AUX_HEAT = 64;
|
||||
export const enum ClimateEntityFeature {
|
||||
TARGET_TEMPERATURE = 1,
|
||||
TARGET_TEMPERATURE_RANGE = 2,
|
||||
TARGET_HUMIDITY = 4,
|
||||
FAN_MODE = 8,
|
||||
PRESET_MODE = 16,
|
||||
SWING_MODE = 32,
|
||||
AUX_HEAT = 64,
|
||||
}
|
||||
|
||||
const hvacModeOrdering: { [key in HvacMode]: number } = {
|
||||
auto: 1,
|
||||
|
@ -451,7 +451,7 @@ const getEnergyData = async (
|
||||
...(await fetchStatistics(
|
||||
hass!,
|
||||
compareStartMinHour,
|
||||
end,
|
||||
endCompare,
|
||||
waterStatIds,
|
||||
period,
|
||||
waterUnits,
|
||||
|
@ -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 { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
|
||||
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
|
||||
@ -117,7 +121,7 @@ export const fetchRecent = (
|
||||
|
||||
export const fetchRecentWS = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string, // This may be CSV
|
||||
entityIds: string[],
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
skipInitialState = false,
|
||||
@ -133,7 +137,7 @@ export const fetchRecentWS = (
|
||||
include_start_time_state: !skipInitialState,
|
||||
minimal_response: minimalResponse,
|
||||
no_attributes: noAttributes || false,
|
||||
entity_ids: entityId.split(","),
|
||||
entity_ids: entityIds,
|
||||
});
|
||||
|
||||
export const fetchDate = (
|
||||
@ -160,9 +164,9 @@ export const fetchDateWS = (
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
minimal_response: true,
|
||||
no_attributes: !entityIds
|
||||
.map((entityId) => entityIdHistoryNeedsAttributes(hass, entityId))
|
||||
.reduce((cur, next) => cur || next, false),
|
||||
no_attributes: !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
),
|
||||
};
|
||||
if (entityIds.length !== 0) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentAttributes: HassEntityAttributeBase = {};
|
||||
if (current_state?.attributes.device_class) {
|
||||
currentAttributes.device_class = current_state?.attributes.device_class;
|
||||
}
|
||||
|
||||
data.push({
|
||||
state_localize: computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
language,
|
||||
entities,
|
||||
entityId,
|
||||
state.a || first.a,
|
||||
{
|
||||
...(state.a || first.a),
|
||||
...currentAttributes,
|
||||
},
|
||||
state.s
|
||||
),
|
||||
state: state.s,
|
||||
|
@ -44,6 +44,7 @@ declare global {
|
||||
export type TranslationCategory =
|
||||
| "title"
|
||||
| "state"
|
||||
| "state_attributes"
|
||||
| "entity"
|
||||
| "config"
|
||||
| "config_panel"
|
||||
|
@ -11,6 +11,11 @@ import { property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
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 { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-climate-control";
|
||||
@ -19,13 +24,7 @@ import "../../../components/ha-slider";
|
||||
import "../../../components/ha-switch";
|
||||
import {
|
||||
ClimateEntity,
|
||||
CLIMATE_SUPPORT_AUX_HEAT,
|
||||
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,
|
||||
ClimateEntityFeature,
|
||||
compareClimateHvacModes,
|
||||
} from "../../../data/climate";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -47,26 +46,32 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
const supportTargetTemperature = supportsFeature(
|
||||
stateObj,
|
||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
);
|
||||
const supportTargetTemperatureRange = supportsFeature(
|
||||
stateObj,
|
||||
CLIMATE_SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
);
|
||||
const supportTargetHumidity = supportsFeature(
|
||||
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(
|
||||
stateObj,
|
||||
CLIMATE_SUPPORT_PRESET_MODE
|
||||
ClimateEntityFeature.PRESET_MODE
|
||||
);
|
||||
const supportSwingMode = supportsFeature(
|
||||
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 =
|
||||
stateObj.attributes.target_temp_step ||
|
||||
@ -94,7 +99,12 @@ class MoreInfoClimate extends LitElement {
|
||||
${supportTargetTemperature || supportTargetTemperatureRange
|
||||
? html`
|
||||
<div>
|
||||
${hass.localize("ui.card.climate.target_temperature")}
|
||||
${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"temperature"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@ -145,7 +155,14 @@ class MoreInfoClimate extends LitElement {
|
||||
${supportTargetHumidity
|
||||
? html`
|
||||
<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="target-humidity">
|
||||
${stateObj.attributes.humidity} %
|
||||
@ -182,7 +199,13 @@ class MoreInfoClimate extends LitElement {
|
||||
.map(
|
||||
(mode) => html`
|
||||
<mwc-list-item .value=${mode}>
|
||||
${hass.localize(`component.climate.state._.${mode}`)}
|
||||
${computeStateDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.locale,
|
||||
hass.entities,
|
||||
mode
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
@ -194,7 +217,12 @@ class MoreInfoClimate extends LitElement {
|
||||
? html`
|
||||
<div class="container-preset_modes">
|
||||
<ha-select
|
||||
.label=${hass.localize("ui.card.climate.preset_mode")}
|
||||
.label=${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"preset_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.preset_mode}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@ -204,9 +232,13 @@ class MoreInfoClimate extends LitElement {
|
||||
${stateObj.attributes.preset_modes!.map(
|
||||
(mode) => html`
|
||||
<mwc-list-item .value=${mode}>
|
||||
${hass.localize(
|
||||
`state_attributes.climate.preset_mode.${mode}`
|
||||
) || mode}
|
||||
${computeAttributeValueDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"preset_mode",
|
||||
mode
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
@ -218,7 +250,12 @@ class MoreInfoClimate extends LitElement {
|
||||
? html`
|
||||
<div class="container-fan_list">
|
||||
<ha-select
|
||||
.label=${hass.localize("ui.card.climate.fan_mode")}
|
||||
.label=${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"fan_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.fan_mode}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@ -228,9 +265,13 @@ class MoreInfoClimate extends LitElement {
|
||||
${stateObj.attributes.fan_modes!.map(
|
||||
(mode) => html`
|
||||
<mwc-list-item .value=${mode}>
|
||||
${hass.localize(
|
||||
`state_attributes.climate.fan_mode.${mode}`
|
||||
) || mode}
|
||||
${computeAttributeValueDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"fan_mode",
|
||||
mode
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
@ -242,7 +283,12 @@ class MoreInfoClimate extends LitElement {
|
||||
? html`
|
||||
<div class="container-swing_list">
|
||||
<ha-select
|
||||
.label=${hass.localize("ui.card.climate.swing_mode")}
|
||||
.label=${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"swing_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.swing_mode}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@ -251,7 +297,15 @@ class MoreInfoClimate extends LitElement {
|
||||
>
|
||||
${stateObj.attributes.swing_modes!.map(
|
||||
(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>
|
||||
@ -263,7 +317,12 @@ class MoreInfoClimate extends LitElement {
|
||||
<div class="container-aux_heat">
|
||||
<div class="center horizontal layout single-row">
|
||||
<div class="flex">
|
||||
${hass.localize("ui.card.climate.aux_heat")}
|
||||
${computeAttributeNameDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.entities,
|
||||
"aux_heat"
|
||||
)}
|
||||
</div>
|
||||
<ha-switch
|
||||
.checked=${stateObj.attributes.aux_heat === "on"}
|
||||
|
@ -139,7 +139,7 @@ export class MoreInfoHistory extends LitElement {
|
||||
}
|
||||
this._stateHistory = await getRecentWithCache(
|
||||
this.hass!,
|
||||
this.entityId,
|
||||
[this.entityId],
|
||||
{
|
||||
cacheKey: `more_info.${this.entityId}`,
|
||||
hoursToShow: 24,
|
||||
|
@ -138,6 +138,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state");
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state_attributes");
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "entity");
|
||||
|
||||
document.addEventListener(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCalendarClock, mdiClose } from "@mdi/js";
|
||||
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";
|
||||
@ -185,11 +186,12 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
};
|
||||
|
||||
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
|
||||
const end = isDate(this._data.dtend)
|
||||
? addDays(new Date(this._data!.dtend), -1)
|
||||
: new Date(this._data!.dtend);
|
||||
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
|
||||
// The range can be shortened when the start and end are on the same day.
|
||||
if (isSameDay(start, end)) {
|
||||
if (isDate(this._data.dtstart)) {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
differenceInMilliseconds,
|
||||
startOfHour,
|
||||
} from "date-fns/esm";
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@ -60,6 +61,12 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@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 {
|
||||
this._error = undefined;
|
||||
this._info = undefined;
|
||||
@ -71,6 +78,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
computeStateDomain(stateObj) === "calendar" &&
|
||||
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
|
||||
)?.entity_id;
|
||||
this._timeZone =
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone ||
|
||||
this.hass.config.time_zone;
|
||||
if (params.entry) {
|
||||
const entry = params.entry!;
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
@ -281,20 +291,30 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
private _isEditableCalendar = (entityStateObj: HassEntity) =>
|
||||
supportsFeature(entityStateObj, CalendarEntityFeature.CREATE_EVENT);
|
||||
|
||||
private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) =>
|
||||
// en-CA locale used for date format YYYY-MM-DD
|
||||
// en-GB locale used for 24h time format HH:MM:SS
|
||||
{
|
||||
const timeZone = this.hass.config.time_zone;
|
||||
return {
|
||||
startDate: startDate?.toLocaleDateString("en-CA", { timeZone }),
|
||||
startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }),
|
||||
endDate: endDate?.toLocaleDateString("en-CA", { timeZone }),
|
||||
endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }),
|
||||
};
|
||||
}
|
||||
private _getLocaleStrings = memoizeOne(
|
||||
(startDate?: Date, endDate?: Date) => ({
|
||||
startDate: this._formatDate(startDate!),
|
||||
startTime: this._formatTime(startDate!),
|
||||
endDate: this._formatDate(endDate!),
|
||||
endTime: this._formatTime(endDate!),
|
||||
})
|
||||
);
|
||||
|
||||
// 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() {
|
||||
this._info = undefined;
|
||||
}
|
||||
@ -319,27 +339,14 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = new Date(
|
||||
ev.detail.value +
|
||||
"T" +
|
||||
this._dtstart!.toLocaleTimeString("en-GB", {
|
||||
timeZone: this.hass.config.time_zone,
|
||||
})
|
||||
this._dtstart = this._parseDate(
|
||||
`${ev.detail.value}T${this._formatTime(this._dtstart!)}`
|
||||
);
|
||||
|
||||
// Prevent that the end time can be before the start time. Try to keep the
|
||||
// duration the same.
|
||||
if (this._dtend! <= this._dtstart!) {
|
||||
const newEnd = 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._dtend = addMilliseconds(this._dtstart, duration);
|
||||
this._info = this.hass.localize(
|
||||
"ui.components.calendar.event.end_auto_adjusted"
|
||||
);
|
||||
@ -347,12 +354,8 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _endDateChanged(ev: CustomEvent) {
|
||||
this._dtend = new Date(
|
||||
ev.detail.value +
|
||||
"T" +
|
||||
this._dtend!.toLocaleTimeString("en-GB", {
|
||||
timeZone: this.hass.config.time_zone,
|
||||
})
|
||||
this._dtend = this._parseDate(
|
||||
`${ev.detail.value}T${this._formatTime(this._dtend!)}`
|
||||
);
|
||||
}
|
||||
|
||||
@ -360,25 +363,14 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// Store previous event duration
|
||||
const duration = differenceInMilliseconds(this._dtend!, this._dtstart!);
|
||||
|
||||
this._dtstart = new Date(
|
||||
this._dtstart!.toLocaleDateString("en-CA", {
|
||||
timeZone: this.hass.config.time_zone,
|
||||
}) +
|
||||
"T" +
|
||||
ev.detail.value
|
||||
this._dtstart = this._parseDate(
|
||||
`${this._formatDate(this._dtstart!)}T${ev.detail.value}`
|
||||
);
|
||||
|
||||
// Prevent that the end time can be before the start time. Try to keep the
|
||||
// duration the same.
|
||||
if (this._dtend! <= this._dtstart!) {
|
||||
const newEnd = 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._dtend = addMilliseconds(new Date(this._dtstart), duration);
|
||||
this._info = this.hass.localize(
|
||||
"ui.components.calendar.event.end_auto_adjusted"
|
||||
);
|
||||
@ -386,36 +378,32 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _endTimeChanged(ev: CustomEvent) {
|
||||
this._dtend = new Date(
|
||||
this._dtend!.toLocaleDateString("en-CA", {
|
||||
timeZone: this.hass.config.time_zone,
|
||||
}) +
|
||||
"T" +
|
||||
ev.detail.value
|
||||
this._dtend = this._parseDate(
|
||||
`${this._formatDate(this._dtend!)}T${ev.detail.value}`
|
||||
);
|
||||
}
|
||||
|
||||
private _calculateData() {
|
||||
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
|
||||
this._dtstart,
|
||||
this._dtend
|
||||
);
|
||||
const data: CalendarEventMutableParams = {
|
||||
summary: this._summary,
|
||||
description: this._description,
|
||||
rrule: this._rrule,
|
||||
rrule: this._rrule || undefined,
|
||||
dtstart: "",
|
||||
dtend: "",
|
||||
};
|
||||
if (this._allDay) {
|
||||
data.dtstart = startDate!;
|
||||
data.dtstart = this._formatDate(this._dtstart!);
|
||||
// End date/time is exclusive when persisted
|
||||
data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString(
|
||||
"en-CA"
|
||||
);
|
||||
data.dtend = this._formatDate(addDays(this._dtend!, 1));
|
||||
} else {
|
||||
data.dtstart = `${startDate}T${startTime}`;
|
||||
data.dtend = `${endDate}T${endTime}`;
|
||||
data.dtstart = `${this._formatDate(
|
||||
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;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 "../../../../../components/ha-card";
|
||||
import {
|
||||
@ -13,6 +13,7 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-alert";
|
||||
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
|
||||
@customElement("matter-config-panel")
|
||||
export class MatterConfigPanel extends LitElement {
|
||||
@ -22,6 +23,8 @@ export class MatterConfigPanel extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _curMatterDevices?: Set<string>;
|
||||
|
||||
private get _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() {
|
||||
this._redirectOnNewDevice();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "matter/commission",
|
||||
});
|
||||
@ -112,6 +138,7 @@ export class MatterConfigPanel extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._redirectOnNewDevice();
|
||||
try {
|
||||
await commissionMatterDevice(this.hass, code);
|
||||
} catch (err: any) {
|
||||
@ -130,6 +157,7 @@ export class MatterConfigPanel extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._redirectOnNewDevice();
|
||||
try {
|
||||
await acceptSharedMatterDevice(this.hass, Number(code));
|
||||
} 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 = [
|
||||
haStyle,
|
||||
css`
|
||||
|
@ -50,28 +50,30 @@ class HaPanelDevMqtt extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.topic")}
|
||||
.value=${this.topic}
|
||||
@change=${this._handleTopic}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
||||
.value=${this.qos}
|
||||
@selected=${this._handleQos}
|
||||
>${qosLevel.map(
|
||||
(qos) =>
|
||||
html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-formfield
|
||||
label=${this.hass!.localize("ui.panel.config.mqtt.retain")}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._handleRetain}
|
||||
.checked=${this.retain}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<div class="panel-dev-mqtt-fields">
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.topic")}
|
||||
.value=${this.topic}
|
||||
@change=${this._handleTopic}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
||||
.value=${this.qos}
|
||||
@selected=${this._handleQos}
|
||||
>${qosLevel.map(
|
||||
(qos) =>
|
||||
html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-formfield
|
||||
label=${this.hass!.localize("ui.panel.config.mqtt.retain")}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._handleRetain}
|
||||
.checked=${this.retain}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
@ -160,6 +162,28 @@ class HaPanelDevMqtt extends LitElement {
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
@ -47,32 +47,35 @@ class MqttSubscribeCard extends LitElement {
|
||||
header=${this.hass.localize("ui.panel.config.mqtt.description_listen")}
|
||||
>
|
||||
<form>
|
||||
<ha-textfield
|
||||
.label=${this._subscribed
|
||||
? this.hass.localize("ui.panel.config.mqtt.listening_to")
|
||||
: this.hass.localize("ui.panel.config.mqtt.subscribe_to")}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
.value=${this._topic}
|
||||
@change=${this._handleTopic}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
.value=${this._qos}
|
||||
@selected=${this._handleQos}
|
||||
>${qosLevel.map(
|
||||
(qos) => html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<mwc-button
|
||||
.disabled=${this._topic === ""}
|
||||
@click=${this._handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
${this._subscribed
|
||||
? this.hass.localize("ui.panel.config.mqtt.stop_listening")
|
||||
: this.hass.localize("ui.panel.config.mqtt.start_listening")}
|
||||
</mwc-button>
|
||||
<div class="panel-dev-mqtt-subscribe-fields">
|
||||
<ha-textfield
|
||||
.label=${this._subscribed
|
||||
? this.hass.localize("ui.panel.config.mqtt.listening_to")
|
||||
: this.hass.localize("ui.panel.config.mqtt.subscribe_to")}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
.value=${this._topic}
|
||||
@change=${this._handleTopic}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.config.mqtt.qos")}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
.value=${this._qos}
|
||||
@selected=${this._handleQos}
|
||||
>${qosLevel.map(
|
||||
(qos) =>
|
||||
html`<mwc-list-item .value=${qos}>${qos}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<mwc-button
|
||||
.disabled=${this._topic === ""}
|
||||
@click=${this._handleSubmit}
|
||||
type="submit"
|
||||
>
|
||||
${this._subscribed
|
||||
? this.hass.localize("ui.panel.config.mqtt.stop_listening")
|
||||
: this.hass.localize("ui.panel.config.mqtt.start_listening")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="events">
|
||||
${this._messages.map(
|
||||
@ -170,6 +173,28 @@ class MqttSubscribeCard extends LitElement {
|
||||
pre {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -279,7 +279,9 @@ export class HuiEnergySourcesTableCard
|
||||
? html`<tr class="mdc-data-table__row total">
|
||||
<td class="mdc-data-table__cell"></td>
|
||||
<th class="mdc-data-table__cell" scope="row">
|
||||
Solar total
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_sources_table.solar_total"
|
||||
)}
|
||||
</th>
|
||||
${compare
|
||||
? html`<td
|
||||
|
@ -295,7 +295,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
@ -162,7 +162,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
this._stateHistory = {
|
||||
...(await getRecentWithCache(
|
||||
this.hass!,
|
||||
this._cacheConfig!.cacheKey,
|
||||
this._configEntities!.map((config) => config.entity),
|
||||
this._cacheConfig!,
|
||||
this.hass!.localize,
|
||||
this.hass!.language
|
||||
|
@ -24,6 +24,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { UNIT_F } from "../../../common/const";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
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 { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-card";
|
||||
@ -213,11 +215,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
${
|
||||
stateObj.attributes.hvac_action
|
||||
? this.hass!.localize(
|
||||
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
|
||||
? computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
"hvac_action"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
`component.climate.state._.${stateObj.state}`
|
||||
: computeStateDisplay(
|
||||
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
|
||||
? html`
|
||||
-
|
||||
${this.hass!.localize(
|
||||
`state_attributes.climate.preset_mode.${stateObj.attributes.preset_mode}`
|
||||
) || stateObj.attributes.preset_mode}
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
"preset_mode"
|
||||
)}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ export class HuiImage extends LitElement {
|
||||
style=${styleMap({
|
||||
paddingBottom: useRatio
|
||||
? `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`
|
||||
: !this._lastImageHeight
|
||||
: this._lastImageHeight === undefined
|
||||
? "56.25%"
|
||||
: undefined,
|
||||
backgroundImage:
|
||||
@ -206,7 +206,7 @@ export class HuiImage extends LitElement {
|
||||
: undefined,
|
||||
})}
|
||||
class="container ${classMap({
|
||||
ratio: useRatio || !this._lastImageHeight,
|
||||
ratio: useRatio || this._lastImageHeight === undefined,
|
||||
})}"
|
||||
>
|
||||
${this.cameraImage && this.cameraView === "live"
|
||||
|
@ -38,9 +38,11 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import scrollToTarget from "../../common/dom/scroll-to-target";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
addSearchParam,
|
||||
extractSearchParam,
|
||||
extractSearchParamsObject,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@ -556,8 +558,16 @@ class HUIRoot extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
// Check for requested edit mode
|
||||
if (extractSearchParam("edit") === "1") {
|
||||
const searchParams = extractSearchParamsObject();
|
||||
if (searchParams.edit === "1") {
|
||||
this.lovelace!.setEditMode(true);
|
||||
} else if (searchParams.conversation === "1") {
|
||||
showVoiceCommandDialog(this);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam("conversation"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,10 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
server_controls: {
|
||||
redirect: "/developer-tools/yaml",
|
||||
},
|
||||
calendar: {
|
||||
component: "calendar",
|
||||
redirect: "/calendar",
|
||||
},
|
||||
config: {
|
||||
redirect: "/config/dashboard",
|
||||
},
|
||||
|
@ -22,31 +22,6 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"mode": {
|
||||
"normal": "Normal",
|
||||
@ -140,7 +115,6 @@
|
||||
"climate": {
|
||||
"currently": "Currently",
|
||||
"on_off": "On / off",
|
||||
"target_temperature": "Target temperature",
|
||||
"target_temperature_entity": "{name} target temperature",
|
||||
"target_temperature_mode": "{name} target temperature {mode}",
|
||||
"current_temperature": "{name} current temperature",
|
||||
@ -148,13 +122,8 @@
|
||||
"cooling": "{name} cooling",
|
||||
"high": "high",
|
||||
"low": "low",
|
||||
"target_humidity": "Target humidity",
|
||||
"operation": "Operation",
|
||||
"fan_mode": "Fan mode",
|
||||
"swing_mode": "Swing mode",
|
||||
"preset_mode": "Preset",
|
||||
"away_mode": "Away mode",
|
||||
"aux_heat": "Aux heat"
|
||||
"away_mode": "Away mode"
|
||||
},
|
||||
"counter": {
|
||||
"actions": {
|
||||
@ -3225,7 +3194,7 @@
|
||||
"mqtt": {
|
||||
"title": "MQTT",
|
||||
"description_publish": "Publish a packet",
|
||||
"topic": "topic",
|
||||
"topic": "Topic",
|
||||
"payload": "Payload (template allowed)",
|
||||
"publish": "Publish",
|
||||
"description_listen": "Listen to a topic",
|
||||
@ -3808,6 +3777,7 @@
|
||||
"energy_sources_table": {
|
||||
"grid_total": "Grid total",
|
||||
"gas_total": "Gas total",
|
||||
"solar_total": "Solar total",
|
||||
"water_total": "Water total",
|
||||
"source": "Source",
|
||||
"energy": "Energy",
|
||||
|
10
yarn.lock
10
yarn.lock
@ -6993,6 +6993,15 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.23.0
|
||||
resolution: "date-fns@npm:2.23.0"
|
||||
@ -9427,6 +9436,7 @@ fsevents@^1.2.7:
|
||||
core-js: ^3.15.2
|
||||
cropperjs: ^1.5.12
|
||||
date-fns: ^2.23.0
|
||||
date-fns-tz: ^1.3.7
|
||||
deep-clone-simple: ^1.1.1
|
||||
deep-freeze: ^0.0.1
|
||||
del: ^4.0.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user