diff --git a/.eslintrc.json b/.eslintrc.json index 985f1da4fd..54c0a23818 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -60,7 +60,6 @@ "no-restricted-globals": [2, "event"], "prefer-promise-reject-errors": "off", "no-unsafe-optional-chaining": "warn", - "prefer-regex-literals": ["warn"], "import/prefer-default-export": "off", "import/no-default-export": "off", "import/no-unresolved": "off", diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index 298487bd0f..de31b9bce1 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -252,6 +252,22 @@ export class HcMain extends HassElement { msg.urlPath = null; } this._lovelacePath = msg.viewPath; + if (msg.urlPath === "energy") { + this._lovelaceConfig = { + views: [ + { + strategy: { + type: "energy", + options: { show_date_selection: true }, + }, + }, + ], + }; + this._urlPath = "energy"; + this._lovelacePath = 0; + this._sendStatus(); + return; + } if (!this._unsubLovelace || this._urlPath !== msg.urlPath) { this._urlPath = msg.urlPath; this._lovelaceConfig = undefined; diff --git a/demo/src/stubs/history.ts b/demo/src/stubs/history.ts index 5b96b77ddf..0daccb4d80 100644 --- a/demo/src/stubs/history.ts +++ b/demo/src/stubs/history.ts @@ -66,7 +66,7 @@ const incrementalUnits = ["clients", "queries", "ads"]; export const mockHistory = (mockHass: MockHomeAssistant) => { mockHass.mockAPI( - new RegExp("history/period/.+"), + /history\/period\/.+/, (hass, _method, path, _parameters) => { const params = parseQuery(path.split("?")[1]); const entities = params.filter_entity_id.split(","); diff --git a/gallery/src/pages/automation/describe-action.ts b/gallery/src/pages/automation/describe-action.ts index 1a32ac0829..6465e9fc1e 100644 --- a/gallery/src/pages/automation/describe-action.ts +++ b/gallery/src/pages/automation/describe-action.ts @@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${this._action - ? describeAction(this.hass, this._action) + ? describeAction(this.hass, [], this._action) : ""} html`
- ${describeAction(this.hass, conf as any)} + ${describeAction(this.hass, [], conf as any)}
${dump(conf)}
` diff --git a/package.json b/package.json index 998f5eda7b..1d10ea6580 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@material/mwc-textfield": "^0.27.0", "@material/mwc-top-app-bar-fixed": "^0.27.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", - "@material/web": "=1.0.0-pre.2", + "@material/web": "=1.0.0-pre.3", "@mdi/js": "7.1.96", "@mdi/svg": "7.1.96", "@polymer/app-layout": "^3.1.0", @@ -216,7 +216,7 @@ "lint-staged": "^13.1.2", "lit-analyzer": "^1.2.1", "lodash.template": "^4.5.0", - "magic-string": "^0.29.0", + "magic-string": "^0.30.0", "map-stream": "^0.0.7", "merge-stream": "^2.0.0", "mocha": "^10.2.0", diff --git a/pyproject.toml b/pyproject.toml index 01692958c7..518a24ff18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20230222.0" +version = "20230223.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/entity/compute_attribute_display.ts b/src/common/entity/compute_attribute_display.ts index f969646265..7cba3466f7 100644 --- a/src/common/entity/compute_attribute_display.ts +++ b/src/common/entity/compute_attribute_display.ts @@ -1,5 +1,5 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { EntityRegistryEntry } from "../../data/entity_registry"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { HomeAssistant } from "../../types"; import { LocalizeFunc } from "../translations/localize"; import { computeDomain } from "./compute_domain"; @@ -15,7 +15,7 @@ export const computeAttributeValueDisplay = ( const attributeValue = value !== undefined ? value : stateObj.attributes[attribute]; const domain = computeDomain(entityId); - const entity = entities[entityId] as EntityRegistryEntry | undefined; + const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined; const translationKey = entity?.translation_key; return ( @@ -38,7 +38,7 @@ export const computeAttributeNameDisplay = ( ): string => { const entityId = stateObj.entity_id; const domain = computeDomain(entityId); - const entity = entities[entityId] as EntityRegistryEntry | undefined; + const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined; const translationKey = entity?.translation_key; return ( diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index c469585a9d..cb327710c2 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,6 +1,6 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; -import { EntityRegistryEntry } from "../../data/entity_registry"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { FrontendLocaleData } from "../../data/translation"; import { updateIsInstallingFromAttributes, @@ -49,7 +49,7 @@ export const computeStateDisplayFromEntityAttributes = ( return localize(`state.default.${state}`); } - const entity = entities[entityId] as EntityRegistryEntry | undefined; + const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined; // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` if (isNumericFromAttributes(attributes)) { diff --git a/src/common/number/format_number.ts b/src/common/number/format_number.ts index 2e461ab822..3834ca1a65 100644 --- a/src/common/number/format_number.ts +++ b/src/common/number/format_number.ts @@ -2,7 +2,7 @@ import { HassEntity, HassEntityAttributeBase, } from "home-assistant-js-websocket"; -import { EntityRegistryEntry } from "../../data/entity_registry"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { round } from "./round"; @@ -92,11 +92,9 @@ export const formatNumber = ( */ export const getNumberFormatOptions = ( entityState: HassEntity, - entity?: EntityRegistryEntry + entity?: EntityRegistryDisplayEntry ): Intl.NumberFormatOptions | undefined => { - const precision = - entity?.options?.sensor?.display_precision ?? - entity?.options?.sensor?.suggested_display_precision; + const precision = entity?.display_precision; if (precision != null) { return { maximumFractionDigits: precision, diff --git a/src/common/string/has-template.ts b/src/common/string/has-template.ts index c86d7f6e8e..189f10f635 100644 --- a/src/common/string/has-template.ts +++ b/src/common/string/has-template.ts @@ -1,4 +1,4 @@ -const isTemplateRegex = new RegExp("{%|{{"); +const isTemplateRegex = /{%|{{/; export const isTemplate = (value: string): boolean => isTemplateRegex.test(value); diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 03079a331f..3f9262e781 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -22,7 +22,7 @@ import { isNumericState, } from "../../common/number/format_number"; import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity"; -import { EntityRegistryEntry } from "../../data/entity_registry"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { timerTimeRemaining } from "../../data/timer"; import { HomeAssistant } from "../../types"; import "../ha-label-badge"; @@ -160,7 +160,7 @@ export class HaStateLabelBadge extends LitElement { private _computeValue( domain: string, entityState: HassEntity, - entry?: EntityRegistryEntry + entry?: EntityRegistryDisplayEntry ) { switch (domain) { case "alarm_control_panel": @@ -200,7 +200,7 @@ export class HaStateLabelBadge extends LitElement { private _computeShowIcon( domain: string, entityState: HassEntity, - entry?: EntityRegistryEntry + entry?: EntityRegistryDisplayEntry ): boolean { if (entityState.state === UNAVAILABLE) { return false; diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 6f016cc1f7..71ce5abe9b 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -12,10 +12,11 @@ import { createAreaRegistryEntry, } from "../data/area_registry"; import { - DeviceEntityLookup, + DeviceEntityDisplayLookup, DeviceRegistryEntry, + getDeviceEntityDisplayLookup, } from "../data/device_registry"; -import { EntityRegistryEntry } from "../data/entity_registry"; +import { EntityRegistryDisplayEntry } from "../data/entity_registry"; import { showAlertDialog, showPromptDialog, @@ -113,7 +114,7 @@ export class HaAreaPicker extends LitElement { ( areas: AreaRegistryEntry[], devices: DeviceRegistryEntry[], - entities: EntityRegistryEntry[], + entities: EntityRegistryDisplayEntry[], includeDomains: this["includeDomains"], excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], @@ -133,111 +134,107 @@ export class HaAreaPicker extends LitElement { ]; } - const deviceEntityLookup: DeviceEntityLookup = {}; + let deviceEntityLookup: DeviceEntityDisplayLookup = {}; let inputDevices: DeviceRegistryEntry[] | undefined; - let inputEntities: EntityRegistryEntry[] | undefined; + let inputEntities: EntityRegistryDisplayEntry[] | undefined; if ( includeDomains || excludeDomains || includeDeviceClasses || + deviceFilter || entityFilter ) { - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - } - inputDevices = devices; - inputEntities = entities.filter((entity) => entity.area_id); + deviceEntityLookup = getDeviceEntityDisplayLookup(entities); + inputDevices = devices; + inputEntities = entities.filter((entity) => entity.area_id); - if (includeDomains) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => + if (includeDomains) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return deviceEntityLookup[device.id].some((entity) => + includeDomains.includes(computeDomain(entity.entity_id)) + ); + }); + inputEntities = inputEntities!.filter((entity) => includeDomains.includes(computeDomain(entity.entity_id)) ); - }); - inputEntities = inputEntities!.filter((entity) => - includeDomains.includes(computeDomain(entity.entity_id)) - ); - } + } - if (excludeDomains) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return true; - } - return entities.every( + if (excludeDomains) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return true; + } + return entities.every( + (entity) => + !excludeDomains.includes(computeDomain(entity.entity_id)) + ); + }); + inputEntities = inputEntities!.filter( (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) ); - }); - inputEntities = inputEntities!.filter( - (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) - ); - } + } - if (includeDeviceClasses) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { + if (includeDeviceClasses) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { return false; } + return deviceEntityLookup[device.id].some((entity) => { + const stateObj = this.hass.states[entity.entity_id]; + if (!stateObj) { + return false; + } + return ( + stateObj.attributes.device_class && + includeDeviceClasses.includes(stateObj.attributes.device_class) + ); + }); + }); + inputEntities = inputEntities!.filter((entity) => { + const stateObj = this.hass.states[entity.entity_id]; return ( stateObj.attributes.device_class && includeDeviceClasses.includes(stateObj.attributes.device_class) ); }); - }); - inputEntities = inputEntities!.filter((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - return ( - stateObj.attributes.device_class && - includeDeviceClasses.includes(stateObj.attributes.device_class) + } + + if (deviceFilter) { + inputDevices = inputDevices!.filter((device) => + deviceFilter!(device) ); - }); - } + } - if (deviceFilter) { - inputDevices = inputDevices!.filter((device) => deviceFilter!(device)); - } - - if (entityFilter) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => { + if (entityFilter) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return deviceEntityLookup[device.id].some((entity) => { + const stateObj = this.hass.states[entity.entity_id]; + if (!stateObj) { + return false; + } + return entityFilter(stateObj); + }); + }); + inputEntities = inputEntities!.filter((entity) => { const stateObj = this.hass.states[entity.entity_id]; if (!stateObj) { return false; } - return entityFilter(stateObj); + return entityFilter!(stateObj); }); - }); - inputEntities = inputEntities!.filter((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { - return false; - } - return entityFilter!(stateObj); - }); + } } let outputAreas = areas; diff --git a/src/components/ha-control-switch.ts b/src/components/ha-control-switch.ts index b2a05073ce..ae15ce98b7 100644 --- a/src/components/ha-control-switch.ts +++ b/src/components/ha-control-switch.ts @@ -1,3 +1,10 @@ +import { + DIRECTION_HORIZONTAL, + DIRECTION_VERTICAL, + Manager, + Swipe, + Tap, +} from "@egjs/hammerjs"; import { css, CSSResultGroup, @@ -6,7 +13,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import "./ha-svg-icon"; @@ -30,8 +37,11 @@ export class HaControlSwitch extends LitElement { // SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an in) @property({ type: String }) pathOff?: string; + private _mc?: HammerManager; + protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); + this.setupListeners(); this.setAttribute("role", "switch"); if (!this.hasAttribute("tabindex")) { this.setAttribute("tabindex", "0"); @@ -53,14 +63,70 @@ export class HaControlSwitch extends LitElement { connectedCallback(): void { super.connectedCallback(); - this.addEventListener("keydown", this._keydown); - this.addEventListener("click", this._toggle); + this.setupListeners(); } disconnectedCallback(): void { super.disconnectedCallback(); + this.destroyListeners(); + } + + @query("#switch") + private switch!: HTMLDivElement; + + setupListeners() { + if (this.switch && !this._mc) { + this._mc = new Manager(this.switch, { + touchAction: this.vertical ? "pan-x" : "pan-y", + }); + this._mc.add( + new Swipe({ + direction: this.vertical ? DIRECTION_VERTICAL : DIRECTION_HORIZONTAL, + }) + ); + + this._mc.add(new Tap({ event: "singletap" })); + + if (this.vertical) { + this._mc.on("swipeup", () => { + if (this.disabled) return; + this.checked = !!this.reversed; + fireEvent(this, "change"); + }); + + this._mc.on("swipedown", () => { + if (this.disabled) return; + this.checked = !this.reversed; + fireEvent(this, "change"); + }); + } else { + this._mc.on("swiperight", () => { + if (this.disabled) return; + this.checked = !this.reversed; + fireEvent(this, "change"); + }); + + this._mc.on("swipeleft", () => { + if (this.disabled) return; + this.checked = !!this.reversed; + fireEvent(this, "change"); + }); + } + + this._mc.on("singletap", () => { + if (this.disabled) return; + this._toggle(); + }); + this.addEventListener("keydown", this._keydown); + } + } + + destroyListeners() { + if (this._mc) { + this._mc.destroy(); + this._mc = undefined; + } this.removeEventListener("keydown", this._keydown); - this.removeEventListener("click", this._toggle); } private _keydown(ev: any) { @@ -73,7 +139,7 @@ export class HaControlSwitch extends LitElement { protected render(): TemplateResult { return html` -
+