diff --git a/.github/workflows/cast_deployment.yaml b/.github/workflows/cast_deployment.yaml index 4634ae0136..8b8b0b98f7 100644 --- a/.github/workflows/cast_deployment.yaml +++ b/.github/workflows/cast_deployment.yaml @@ -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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a6151af742..b43acab616 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a15a50c233..7717444454 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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. diff --git a/.github/workflows/demo_deployment.yaml b/.github/workflows/demo_deployment.yaml index dc59829095..cc394b646f 100644 --- a/.github/workflows/demo_deployment.yaml +++ b/.github/workflows/demo_deployment.yaml @@ -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 diff --git a/.github/workflows/design_deployment.yaml b/.github/workflows/design_deployment.yaml index 0743c1d800..0574dd8676 100644 --- a/.github/workflows/design_deployment.yaml +++ b/.github/workflows/design_deployment.yaml @@ -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 diff --git a/.github/workflows/design_preview.yaml b/.github/workflows/design_preview.yaml index e53f0c83a3..084f69ad93 100644 --- a/.github/workflows/design_preview.yaml +++ b/.github/workflows/design_preview.yaml @@ -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 diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index ba0b88f672..bd54ca3026 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e0bddf81d2..662c7adcc6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8ddf8091ce..bd5f019615 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -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 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 6cc03e0883..4d593874d7 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -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: | diff --git a/package.json b/package.json index 51f4fca91d..531d6e4246 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/entity/compute_attribute_display.ts b/src/common/entity/compute_attribute_display.ts new file mode 100644 index 0000000000..f969646265 --- /dev/null +++ b/src/common/entity/compute_attribute_display.ts @@ -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 + ); +}; diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index 52bd0b6316..a1f11551c4 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -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`
${this._computeTarget()}
` @@ -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; } diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index ce5208ec67..4e9c74da46 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -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, diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index 4029846998..ac2ba9d63b 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -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` @@ -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} > `; } @@ -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` !item.disabled)} + .items=${optionItems} .value=${this.value} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} + @opened-changed=${this._openedChanged} > `; } @@ -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()); }); diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts index 30fa4956f9..2e4bf825f4 100644 --- a/src/data/cached-history.ts +++ b/src/data/cached-history.ts @@ -23,63 +23,8 @@ interface CachedResults { data: HistoryResult; } -// This is a different interface, a different cache :( -interface RecentCacheResults { - created: number; - language: string; - data: Promise; -} - -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, diff --git a/src/data/climate.ts b/src/data/climate.ts index 064fb32c98..6ed6fa7523 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -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, diff --git a/src/data/energy.ts b/src/data/energy.ts index 3e412bd127..4110bcde82 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -451,7 +451,7 @@ const getEnergyData = async ( ...(await fetchStatistics( hass!, compareStartMinHour, - end, + endCompare, waterStatIds, period, waterUnits, diff --git a/src/data/history.ts b/src/data/history.ts index 9940292b72..73688d4057 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -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({ ...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, diff --git a/src/data/translation.ts b/src/data/translation.ts index 56764f922a..1d3f5c2d71 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -44,6 +44,7 @@ declare global { export type TranslationCategory = | "title" | "state" + | "state_attributes" | "entity" | "config" | "config_panel" diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 03b94c3827..559b9bfbef 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -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`
- ${hass.localize("ui.card.climate.target_temperature")} + ${computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + "temperature" + )}
` : ""} @@ -145,7 +155,14 @@ class MoreInfoClimate extends LitElement { ${supportTargetHumidity ? html`
-
${hass.localize("ui.card.climate.target_humidity")}
+
+ ${computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + "humidity" + )} +
${stateObj.attributes.humidity} % @@ -182,7 +199,13 @@ class MoreInfoClimate extends LitElement { .map( (mode) => html` - ${hass.localize(`component.climate.state._.${mode}`)} + ${computeStateDisplay( + hass.localize, + stateObj, + hass.locale, + hass.entities, + mode + )} ` )} @@ -194,7 +217,12 @@ class MoreInfoClimate extends LitElement { ? html`
html` - ${hass.localize( - `state_attributes.climate.preset_mode.${mode}` - ) || mode} + ${computeAttributeValueDisplay( + hass.localize, + stateObj, + hass.entities, + "preset_mode", + mode + )} ` )} @@ -218,7 +250,12 @@ class MoreInfoClimate extends LitElement { ? html`
html` - ${hass.localize( - `state_attributes.climate.fan_mode.${mode}` - ) || mode} + ${computeAttributeValueDisplay( + hass.localize, + stateObj, + hass.entities, + "fan_mode", + mode + )} ` )} @@ -242,7 +283,12 @@ class MoreInfoClimate extends LitElement { ? html`
${stateObj.attributes.swing_modes!.map( (mode) => html` - ${mode} + + ${computeAttributeValueDisplay( + hass.localize, + stateObj, + hass.entities, + "swing_mode", + mode + )} + ` )} @@ -263,7 +317,12 @@ class MoreInfoClimate extends LitElement {
- ${hass.localize("ui.card.climate.aux_heat")} + ${computeAttributeNameDisplay( + hass.localize, + stateObj, + hass.entities, + "aux_heat" + )}
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; } diff --git a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts index 852df1ebe1..77fa19ca74 100644 --- a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts @@ -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; + 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` diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index a60764a184..5ab4a00ab2 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts @@ -50,28 +50,30 @@ class HaPanelDevMqtt extends LitElement { )} >
- - ${qosLevel.map( - (qos) => - html`${qos}` - )} - - - - +
+ + ${qosLevel.map( + (qos) => + html`${qos}` + )} + + + + +

${this.hass.localize("ui.panel.config.mqtt.payload")}

- - ${qosLevel.map( - (qos) => html`${qos}` - )} - - - ${this._subscribed - ? this.hass.localize("ui.panel.config.mqtt.stop_listening") - : this.hass.localize("ui.panel.config.mqtt.start_listening")} - +
+ + ${qosLevel.map( + (qos) => + html`${qos}` + )} + + + ${this._subscribed + ? this.hass.localize("ui.panel.config.mqtt.stop_listening") + : this.hass.localize("ui.panel.config.mqtt.start_listening")} + +
${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%; + } + } `; } } diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index d44bd49f5d..9a05d71990 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -279,7 +279,9 @@ export class HuiEnergySourcesTableCard ? html` - Solar total + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_sources_table.solar_total" + )} ${compare ? html` config.entity), this._cacheConfig!, this.hass!.localize, this.hass!.language diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 6d6dbc7dde..9ce9f8fa62 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -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" + )} ` : "" } diff --git a/src/panels/lovelace/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts index 555bca93ad..573210f86d 100644 --- a/src/panels/lovelace/components/hui-image.ts +++ b/src/panels/lovelace/components/hui-image.ts @@ -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" diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 9637884550..37208ba078 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -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")) + ); } } diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index c5e60b5540..7960d31dba 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -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", }, diff --git a/src/translations/en.json b/src/translations/en.json index 5cc1fd033d..f8415ad180 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 40ccc226bd..c81d28f903 100644 --- a/yarn.lock +++ b/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