diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000..936394a327 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,127 @@ +name: CI + +on: + push: + branches: + - dev + - master + pull_request: + branches: + - dev + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Check out files from GitHub + uses: actions/checkout@v2 + - name: Setting up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get yarn cache path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Fetching Yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies + run: yarn install + env: + CI: true + - name: Build icons + run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app + - name: Build translations + run: ./node_modules/.bin/gulp build-translations + - name: Run eslint + run: ./node_modules/.bin/eslint src hassio/src gallery/src + - name: Run tslint + run: ./node_modules/.bin/tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' + - name: Run tsc + run: ./node_modules/.bin/tsc + test: + runs-on: ubuntu-latest + steps: + - name: Check out files from GitHub + uses: actions/checkout@v2 + - name: Setting up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get yarn cache path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Fetching Yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies + run: yarn install + env: + CI: true + - name: Run Mocha + run: npm run mocha + build: + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Check out files from GitHub + uses: actions/checkout@v2 + - name: Setting up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get yarn cache path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Fetching Yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies + run: yarn install + env: + CI: true + - name: Build Application + run: ./node_modules/.bin/gulp build-app + env: + TRAVIS: "true" + supervisor: + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Check out files from GitHub + uses: actions/checkout@v2 + - name: Setting up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get yarn cache path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Fetching Yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies + run: yarn install + env: + CI: true + - name: Build Application + run: ./node_modules/.bin/gulp build-hassio + env: + TRAVIS: "true" diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml new file mode 100644 index 0000000000..a06c511744 --- /dev/null +++ b/.github/workflows/demo.yaml @@ -0,0 +1,39 @@ +name: Demo + +on: + push: + branches: + - dev +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Check out files from GitHub + uses: actions/checkout@v2 + - name: Setting up Node.js + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: Get yarn cache path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Fetching Yarn cache + uses: actions/cache@v1 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies + run: yarn install + env: + CI: true + - name: Build Demo + run: ./node_modules/.bin/gulp build-demo + - name: Deploy to Netlify + uses: netlify/actions/cli@master + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} + with: + args: deploy --dir=demo/dist --prod diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bbe715a830..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -sudo: false -language: node_js -cache: - yarn: true - directories: - - bower_components -install: yarn install -script: - - npm run build - - hassio/script/build_hassio - # Because else eslint fails because hassio has cleaned that build - - ./node_modules/.bin/gulp gen-icons-app - - npm run test - # - xvfb-run wct --module-resolution=node --npm - # - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi' -dist: trusty -addons: - sauce_connect: true diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index fa1badd560..1bb401c36a 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -15,6 +15,7 @@ import { import { LovelaceConfig, getLovelaceCollection, + fetchResources, } from "../../../../src/data/lovelace"; import "./hc-launch-screen"; import { castContext } from "../cast_context"; @@ -23,6 +24,8 @@ import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages"; import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources"; import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; +let resourcesLoaded = false; + @customElement("hc-main") export class HcMain extends HassElement { @property() private _showDemo = false; @@ -34,6 +37,7 @@ export class HcMain extends HassElement { @property() private _error?: string; private _unsubLovelace?: UnsubscribeFunc; + private _urlPath?: string | null; public processIncomingMessage(msg: HassMessage) { if (msg.type === "connect") { @@ -108,6 +112,7 @@ export class HcMain extends HassElement { if (this.hass) { status.hassUrl = this.hass.auth.data.hassUrl; status.lovelacePath = this._lovelacePath!; + status.urlPath = this._urlPath; } if (senderId) { @@ -163,8 +168,19 @@ export class HcMain extends HassElement { this._error = "Cannot show Lovelace because we're not connected."; return; } - if (!this._unsubLovelace) { - const llColl = getLovelaceCollection(this.hass!.connection); + if (!resourcesLoaded) { + resourcesLoaded = true; + loadLovelaceResources( + await fetchResources(this.hass!.connection), + this.hass!.auth.data.hassUrl + ); + } + if (!this._unsubLovelace || this._urlPath !== msg.urlPath) { + this._urlPath = msg.urlPath; + if (this._unsubLovelace) { + this._unsubLovelace(); + } + const llColl = getLovelaceCollection(this.hass!.connection, msg.urlPath); // We first do a single refresh because we need to check if there is LL // configuration. try { @@ -194,12 +210,6 @@ export class HcMain extends HassElement { private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { castContext.setApplicationState(lovelaceConfig.title!); this._lovelaceConfig = lovelaceConfig; - if (lovelaceConfig.resources) { - loadLovelaceResources( - lovelaceConfig.resources, - this.hass!.auth.data.hassUrl - ); - } } private _handleShowDemo(_msg: ShowDemoMessage) { diff --git a/hassio/src/addon-view/hassio-addon-audio.ts b/hassio/src/addon-view/hassio-addon-audio.ts index 84357963b4..eeda1084a5 100644 --- a/hassio/src/addon-view/hassio-addon-audio.ts +++ b/hassio/src/addon-view/hassio-addon-audio.ts @@ -128,22 +128,27 @@ class HassioAddonAudio extends LitElement { private _setInputDevice(ev): void { const device = ev.detail.item.getAttribute("device"); - this._selectedInput = device || null; + this._selectedInput = device; } private _setOutputDevice(ev): void { const device = ev.detail.item.getAttribute("device"); - this._selectedOutput = device || null; + this._selectedOutput = device; } private async _addonChanged(): Promise { - this._selectedInput = this.addon.audio_input; - this._selectedOutput = this.addon.audio_output; + this._selectedInput = + this.addon.audio_input === null ? "default" : this.addon.audio_input; + this._selectedOutput = + this.addon.audio_output === null ? "default" : this.addon.audio_output; if (this._outputDevices) { return; } - const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" }; + const noDevice: HassioHardwareAudioDevice = { + device: "default", + name: "Default", + }; try { const { audio } = await fetchHassioHardwareAudio(this.hass); @@ -168,8 +173,10 @@ class HassioAddonAudio extends LitElement { private async _saveSettings(): Promise { this._error = undefined; const data: HassioAddonSetOptionParams = { - audio_input: this._selectedInput || null, - audio_output: this._selectedOutput || null, + audio_input: + this._selectedInput === "default" ? null : this._selectedInput, + audio_output: + this._selectedOutput === "default" ? null : this._selectedOutput, }; try { await setHassioAddonOption(this.hass, this.addon.slug, data); diff --git a/hassio/src/addon-view/hassio-addon-info.ts b/hassio/src/addon-view/hassio-addon-info.ts index d8f5ff69db..f706c2ae66 100644 --- a/hassio/src/addon-view/hassio-addon-info.ts +++ b/hassio/src/addon-view/hassio-addon-info.ts @@ -452,7 +452,7 @@ class HassioAddonInfo extends LitElement { ` : ""} diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index e7e1fe3f96..3ae19bd425 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -42,7 +42,9 @@ export class HassioUpdate extends LitElement { !!value && (value.last_version ? value.version !== value.last_version - : value.version !== value.version_latest) + : value.version_latest + ? value.version !== value.version_latest + : false) ); }).length; @@ -102,7 +104,7 @@ export class HassioUpdate extends LitElement { releaseNotesUrl: string, icon?: string ): TemplateResult { - if (lastVersion === curVersion) { + if (!lastVersion || lastVersion === curVersion) { return html``; } return html` diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index d375747e3d..09364b4233 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -148,7 +148,7 @@ class HassioSupervisorInfo extends LitElement { !confirm(`WARNING: Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature. -This inludes beta releases for: +This includes beta releases for: - Home Assistant (Release Candidates) - Hass.io supervisor - Host system`) diff --git a/setup.py b/setup.py index 458b6661a1..b37397edb7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200220.5", + version="20200228.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/cast/receiver_messages.ts b/src/cast/receiver_messages.ts index 5b2f82c5b2..50757ffbde 100644 --- a/src/cast/receiver_messages.ts +++ b/src/cast/receiver_messages.ts @@ -21,6 +21,7 @@ export interface ConnectMessage extends BaseCastMessage { export interface ShowLovelaceViewMessage extends BaseCastMessage { type: "show_lovelace_view"; viewPath: string | number | null; + urlPath: string | null; } export interface ShowDemoMessage extends BaseCastMessage { @@ -43,11 +44,13 @@ export const castSendAuth = (cast: CastManager, auth: Auth) => export const castSendShowLovelaceView = ( cast: CastManager, - viewPath: ShowLovelaceViewMessage["viewPath"] + viewPath: ShowLovelaceViewMessage["viewPath"], + urlPath?: string | null ) => cast.sendMessage({ type: "show_lovelace_view", viewPath, + urlPath: urlPath || null, }); export const castSendShowDemo = (cast: CastManager) => diff --git a/src/cast/sender_messages.ts b/src/cast/sender_messages.ts index 2dc7ab2438..e9a7f074a0 100644 --- a/src/cast/sender_messages.ts +++ b/src/cast/sender_messages.ts @@ -8,6 +8,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage { showDemo: boolean; hassUrl?: string; lovelacePath?: string | number | null; + urlPath?: string | null; } export type SenderMessage = ReceiverStatusMessage; diff --git a/src/common/dom/dynamic-element-directive.ts b/src/common/dom/dynamic-element-directive.ts index 25ca9cfec8..a7b74ce1cd 100644 --- a/src/common/dom/dynamic-element-directive.ts +++ b/src/common/dom/dynamic-element-directive.ts @@ -4,7 +4,7 @@ export const dynamicElement = directive( (tag: string, properties?: { [key: string]: any }) => (part: Part): void => { if (!(part instanceof NodePart)) { throw new Error( - "dynamicContentDirective can only be used in content bindings" + "dynamicElementDirective can only be used in content bindings" ); } diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 9d36310a2e..aff6fc346d 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -23,7 +23,7 @@ const fixedIcons = { homeassistant: "hass:home-assistant", homekit: "hass:home-automation", image_processing: "hass:image-filter-frames", - input_boolean: "hass:drawing", + input_boolean: "hass:toggle-switch-outline", input_datetime: "hass:calendar-clock", input_number: "hass:ray-vertex", input_select: "hass:format-list-bulleted", diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index 565fd065e2..8f868e000a 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -29,6 +29,10 @@ export const iconColorCSS = css` color: var(--heat-color, #ff8100); } + ha-icon[data-domain="climate"][data-state="drying"] { + color: var(--dry-color, #efbd07); + } + ha-icon[data-domain="alarm_control_panel"] { color: var(--alarm-color-armed, var(--label-badge-red)); } diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 28b657cd1c..73188403e6 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -571,6 +571,18 @@ export class HaDataTable extends BaseElement { width: 24px; } + .mdc-data-table__header-cell--icon { + text-align: center; + } + + .mdc-data-table__cell--icon:first-child ha-icon { + margin-left: 8px; + } + + .mdc-data-table__cell--icon:first-child state-badge { + margin-right: -8px; + } + .mdc-data-table__header-cell { font-family: Roboto, sans-serif; -moz-osx-font-smoothing: grayscale; @@ -598,10 +610,6 @@ export class HaDataTable extends BaseElement { text-align: left; } - .mdc-data-table__header-cell--icon { - text-align: center; - } - /* custom from here */ :host { @@ -615,27 +623,39 @@ export class HaDataTable extends BaseElement { } .mdc-data-table__header-cell { overflow: hidden; + position: relative; } + .mdc-data-table__header-cell span { + position: relative; + left: 0px; + } + .mdc-data-table__header-cell.sortable { cursor: pointer; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon) - span { - position: relative; - left: -24px; - } - .mdc-data-table__header-cell.not-sorted > * { + .mdc-data-table__header-cell > * { transition: left 0.2s ease 0s; } + .mdc-data-table__header-cell ha-icon { + top: 15px; + position: absolute; + } .mdc-data-table__header-cell.not-sorted ha-icon { - left: -36px; + left: -20px; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover + .mdc-data-table__header-cell:not(.not-sorted) span, + .mdc-data-table__header-cell.not-sorted:hover span { + left: 24px; + } + .mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted) + span, + .mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover span { - left: 0px; + left: 12px; } + .mdc-data-table__header-cell:not(.not-sorted) ha-icon, .mdc-data-table__header-cell:hover.not-sorted ha-icon { - left: 0px; + left: 12px; } .table-header { border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index ff770e7bb7..290f5ad22f 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -1,12 +1,23 @@ -import { customElement, CSSResult, css } from "lit-element"; +import { customElement, CSSResult, css, html } from "lit-element"; +import "@polymer/paper-icon-button/paper-icon-button"; import "@material/mwc-dialog"; import { style } from "@material/mwc-dialog/mwc-dialog-css"; // tslint:disable-next-line import { Dialog } from "@material/mwc-dialog"; -import { Constructor } from "../types"; +import { Constructor, HomeAssistant } from "../types"; // tslint:disable-next-line const MwcDialog = customElements.get("mwc-dialog") as Constructor; +export const createCloseHeading = (hass: HomeAssistant, title: string) => html` + ${title} + +`; + @customElement("ha-dialog") export class HaDialog extends MwcDialog { protected static get styles(): CSSResult[] { @@ -19,6 +30,15 @@ export class HaDialog extends MwcDialog { .mdc-dialog__container { align-items: var(--vertial-align-dialog, center); } + .mdc-dialog__title::before { + display: block; + height: 20px; + } + .close_button { + position: absolute; + right: 16px; + top: 12px; + } `, ]; } diff --git a/src/components/ha-icon-input.ts b/src/components/ha-icon-input.ts new file mode 100644 index 0000000000..9621e267ed --- /dev/null +++ b/src/components/ha-icon-input.ts @@ -0,0 +1,65 @@ +import { + html, + css, + LitElement, + TemplateResult, + property, + customElement, +} from "lit-element"; + +import "@polymer/paper-input/paper-input"; +import "./ha-icon"; +import { fireEvent } from "../common/dom/fire_event"; + +@customElement("ha-icon-input") +export class HaIconInput extends LitElement { + @property() public value?: string; + @property() public label?: string; + @property() public placeholder?: string; + @property({ attribute: "error-message" }) public errorMessage?: string; + @property({ type: Boolean }) public disabled = false; + + protected render(): TemplateResult { + return html` + + ${this.value || this.placeholder + ? html` + + + ` + : ""} + + `; + } + + private _valueChanged(ev: CustomEvent) { + this.value = ev.detail.value; + fireEvent( + this, + "value-changed", + { value: ev.detail.value }, + { + bubbles: false, + composed: false, + } + ); + } + + static get styles() { + return css` + ha-icon { + position: relative; + bottom: 4px; + } + `; + } +} diff --git a/src/components/ha-related-items.ts b/src/components/ha-related-items.ts index d1b900578b..bc810e48e0 100644 --- a/src/components/ha-related-items.ts +++ b/src/components/ha-related-items.ts @@ -70,9 +70,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { } if (Object.keys(this._related).length === 0) { return html` -

- ${this.hass.localize("ui.components.related-items.no_related_found")} -

+ ${this.hass.localize("ui.components.related-items.no_related_found")} `; } return html` diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 0e93596464..789e3f86bc 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -46,7 +46,18 @@ const SORT_VALUE_URL_PATHS = { config: 11, }; -const panelSorter = (a, b) => { +const panelSorter = (a: PanelInfo, b: PanelInfo) => { + // Put all the Lovelace at the top. + const aLovelace = a.component_name === "lovelace"; + const bLovelace = b.component_name === "lovelace"; + + if (aLovelace && !bLovelace) { + return -1; + } + if (bLovelace) { + return 1; + } + const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS; const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS; diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index a637a28cc3..046fcd36ca 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -6,6 +6,13 @@ import { afterNextRender } from "../common/util/render-status"; // tslint:disable-next-line import { HaCodeEditor } from "./ha-code-editor"; +declare global { + // for fire event + interface HASSDomEvents { + "editor-refreshed": undefined; + } +} + const isEmpty = (obj: object) => { if (typeof obj !== "object") { return false; @@ -37,6 +44,7 @@ export class HaYamlEditor extends LitElement { if (this._editor?.codemirror) { this._editor.codemirror.refresh(); } + afterNextRender(() => fireEvent(this, "editor-refreshed")); }); } diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 58fad95cf1..c4b94aab21 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -6,14 +6,23 @@ import { debounce } from "../common/util/debounce"; export interface EntityRegistryEntry { entity_id: string; name: string; + icon?: string; platform: string; config_entry_id?: string; device_id?: string; disabled_by: string | null; } +export interface ExtEntityRegistryEntry extends EntityRegistryEntry { + unique_id: string; + capabilities: object; + original_name?: string; + original_icon?: string; +} + export interface EntityRegistryEntryUpdateParams { name?: string | null; + icon?: string | null; disabled_by?: string | null; new_entity_id?: string; } @@ -29,12 +38,21 @@ export const computeEntityRegistryName = ( return state ? computeStateName(state) : null; }; +export const getExtendedEntityRegistryEntry = ( + hass: HomeAssistant, + entityId: string +): Promise => + hass.callWS({ + type: "config/entity_registry/get", + entity_id: entityId, + }); + export const updateEntityRegistryEntry = ( hass: HomeAssistant, entityId: string, updates: Partial -): Promise => - hass.callWS({ +): Promise => + hass.callWS({ type: "config/entity_registry/update", entity_id: entityId, ...updates, diff --git a/src/data/frontend.ts b/src/data/frontend.ts index 83a2db1ac7..8760859c65 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -59,3 +59,12 @@ export const getOptimisticFrontendUserDataCollection = < `_frontendUserData-${userDataKey}`, () => fetchFrontendUserData(conn, userDataKey) ); + +export const subscribeFrontendUserData = ( + conn: Connection, + userDataKey: UserDataKey, + onChange: (state: FrontendUserData[UserDataKey] | null) => void +) => + getOptimisticFrontendUserDataCollection(conn, userDataKey).subscribe( + onChange + ); diff --git a/src/data/input-select.ts b/src/data/input-select.ts deleted file mode 100644 index c119cd0290..0000000000 --- a/src/data/input-select.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { HomeAssistant } from "../types"; - -export const setInputSelectOption = ( - hass: HomeAssistant, - entity: string, - option: string -) => - hass.callService("input_select", "select_option", { - option, - entity_id: entity, - }); diff --git a/src/data/input_boolean.ts b/src/data/input_boolean.ts new file mode 100644 index 0000000000..b0393bbd92 --- /dev/null +++ b/src/data/input_boolean.ts @@ -0,0 +1,43 @@ +import { HomeAssistant } from "../types"; + +export interface InputBoolean { + id: string; + name: string; + icon?: string; + initial?: boolean; +} + +export interface InputBooleanMutableParams { + name: string; + icon: string; + initial: boolean; +} + +export const fetchInputBoolean = (hass: HomeAssistant) => + hass.callWS({ type: "input_boolean/list" }); + +export const createInputBoolean = ( + hass: HomeAssistant, + values: InputBooleanMutableParams +) => + hass.callWS({ + type: "input_boolean/create", + ...values, + }); + +export const updateInputBoolean = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_boolean/update", + input_boolean_id: id, + ...updates, + }); + +export const deleteInputBoolean = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_boolean/delete", + input_boolean_id: id, + }); diff --git a/src/data/input_datetime.ts b/src/data/input_datetime.ts index 600035783f..16a2f1b09d 100644 --- a/src/data/input_datetime.ts +++ b/src/data/input_datetime.ts @@ -1,5 +1,22 @@ import { HomeAssistant } from "../types"; +export interface InputDateTime { + id: string; + name: string; + icon?: string; + initial?: string; + has_time: boolean; + has_date: boolean; +} + +export interface InputDateTimeMutableParams { + name: string; + icon: string; + initial: string; + has_time: boolean; + has_date: boolean; +} + export const setInputDateTimeValue = ( hass: HomeAssistant, entityId: string, @@ -9,3 +26,32 @@ export const setInputDateTimeValue = ( const param = { entity_id: entityId, time, date }; hass.callService(entityId.split(".", 1)[0], "set_datetime", param); }; + +export const fetchInputDateTime = (hass: HomeAssistant) => + hass.callWS({ type: "input_datetime/list" }); + +export const createInputDateTime = ( + hass: HomeAssistant, + values: InputDateTimeMutableParams +) => + hass.callWS({ + type: "input_datetime/create", + ...values, + }); + +export const updateInputDateTime = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_datetime/update", + input_datetime_id: id, + ...updates, + }); + +export const deleteInputDateTime = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_datetime/delete", + input_datetime_id: id, + }); diff --git a/src/data/input_number.ts b/src/data/input_number.ts new file mode 100644 index 0000000000..2a1d9c46ad --- /dev/null +++ b/src/data/input_number.ts @@ -0,0 +1,53 @@ +import { HomeAssistant } from "../types"; + +export interface InputNumber { + id: string; + name: string; + min: number; + max: number; + icon?: string; + initial?: number; + step?: number; + mode?: "box" | "slider"; + unit_of_measurement?: string; +} + +export interface InputNumberMutableParams { + name: string; + icon: string; + initial: number; + min: number; + max: number; + step: number; + mode: "box" | "slider"; + unit_of_measurement?: string; +} + +export const fetchInputNumber = (hass: HomeAssistant) => + hass.callWS({ type: "input_number/list" }); + +export const createInputNumber = ( + hass: HomeAssistant, + values: InputNumberMutableParams +) => + hass.callWS({ + type: "input_number/create", + ...values, + }); + +export const updateInputNumber = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_number/update", + input_number_id: id, + ...updates, + }); + +export const deleteInputNumber = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_number/delete", + input_number_id: id, + }); diff --git a/src/data/input_select.ts b/src/data/input_select.ts new file mode 100644 index 0000000000..3c68d5f66b --- /dev/null +++ b/src/data/input_select.ts @@ -0,0 +1,55 @@ +import { HomeAssistant } from "../types"; + +export interface InputSelect { + id: string; + name: string; + options: string[]; + icon?: string; + initial?: string; +} + +export interface InputSelectMutableParams { + name: string; + icon: string; + initial: string; + options: string[]; +} + +export const setInputSelectOption = ( + hass: HomeAssistant, + entity: string, + option: string +) => + hass.callService("input_select", "select_option", { + option, + entity_id: entity, + }); + +export const fetchInputSelect = (hass: HomeAssistant) => + hass.callWS({ type: "input_select/list" }); + +export const createInputSelect = ( + hass: HomeAssistant, + values: InputSelectMutableParams +) => + hass.callWS({ + type: "input_select/create", + ...values, + }); + +export const updateInputSelect = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_select/update", + input_select_id: id, + ...updates, + }); + +export const deleteInputSelect = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_select/delete", + input_select_id: id, + }); diff --git a/src/data/input_text.ts b/src/data/input_text.ts index 04ee6c334c..5e6c7e1225 100644 --- a/src/data/input_text.ts +++ b/src/data/input_text.ts @@ -1,7 +1,57 @@ import { HomeAssistant } from "../types"; +export interface InputText { + id: string; + name: string; + icon?: string; + initial?: string; + min?: number; + max?: number; + pattern?: string; + mode?: "text" | "password"; +} + +export interface InputTextMutableParams { + name: string; + icon: string; + initial: string; + min: number; + max: number; + pattern: string; + mode: "text" | "password"; +} + export const setValue = (hass: HomeAssistant, entity: string, value: string) => hass.callService(entity.split(".", 1)[0], "set_value", { value, entity_id: entity, }); + +export const fetchInputText = (hass: HomeAssistant) => + hass.callWS({ type: "input_text/list" }); + +export const createInputText = ( + hass: HomeAssistant, + values: InputTextMutableParams +) => + hass.callWS({ + type: "input_text/create", + ...values, + }); + +export const updateInputText = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "input_text/update", + input_text_id: id, + ...updates, + }); + +export const deleteInputText = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "input_text/delete", + input_text_id: id, + }); diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 9dc35d9cef..cde4dda192 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -1,12 +1,57 @@ import { HomeAssistant } from "../types"; -import { Connection, getCollection } from "home-assistant-js-websocket"; +import { + Connection, + getCollection, + HassEventBase, +} from "home-assistant-js-websocket"; import { HASSDomEvent } from "../common/dom/fire_event"; export interface LovelaceConfig { title?: string; views: LovelaceViewConfig[]; background?: string; - resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>; +} + +export interface LovelaceResource { + id: string; + type: "css" | "js" | "module" | "html"; + url: string; +} + +export interface LovelaceResourcesMutableParams { + res_type: "css" | "js" | "module" | "html"; + url: string; +} + +export type LovelaceDashboard = + | LovelaceYamlDashboard + | LovelaceStorageDashboard; + +interface LovelaceGenericDashboard { + id: string; + url_path: string; + require_admin: boolean; + sidebar?: { icon: string; title: string }; +} + +export interface LovelaceYamlDashboard extends LovelaceGenericDashboard { + mode: "yaml"; + filename: string; +} + +export interface LovelaceStorageDashboard extends LovelaceGenericDashboard { + mode: "storage"; +} + +export interface LovelaceDashboardMutableParams { + require_admin: boolean; + sidebar: { icon: string; title: string } | null; +} + +export interface LovelaceDashboardCreateParams + extends LovelaceDashboardMutableParams { + url_path: string; + mode: "storage"; } export interface LovelaceViewConfig { @@ -95,47 +140,139 @@ export type ActionConfig = | NoActionConfig | CustomActionConfig; +type LovelaceUpdatedEvent = HassEventBase & { + event_type: "lovelace_updated"; + data: { + url_path: string | null; + mode: "yaml" | "storage"; + }; +}; + +export const fetchResources = (conn: Connection): Promise => + conn.sendMessagePromise({ + type: "lovelace/resources", + }); + +export const createResource = ( + hass: HomeAssistant, + values: LovelaceResourcesMutableParams +) => + hass.callWS({ + type: "lovelace/resources/create", + ...values, + }); + +export const updateResource = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "lovelace/resources/update", + resource_id: id, + ...updates, + }); + +export const deleteResource = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "lovelace/resources/delete", + resource_id: id, + }); + +export const fetchDashboards = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "lovelace/dashboards/list", + }); + +export const createDashboard = ( + hass: HomeAssistant, + values: LovelaceDashboardCreateParams +) => + hass.callWS({ + type: "lovelace/dashboards/create", + ...values, + }); + +export const updateDashboard = ( + hass: HomeAssistant, + id: string, + updates: Partial +) => + hass.callWS({ + type: "lovelace/dashboards/update", + dashboard_id: id, + ...updates, + }); + +export const deleteDashboard = (hass: HomeAssistant, id: string) => + hass.callWS({ + type: "lovelace/dashboards/delete", + dashboard_id: id, + }); + export const fetchConfig = ( conn: Connection, + urlPath: string | null, force: boolean ): Promise => conn.sendMessagePromise({ type: "lovelace/config", + url_path: urlPath, force, }); export const saveConfig = ( hass: HomeAssistant, + urlPath: string | null, config: LovelaceConfig ): Promise => hass.callWS({ type: "lovelace/config/save", + url_path: urlPath, config, }); -export const deleteConfig = (hass: HomeAssistant): Promise => +export const deleteConfig = ( + hass: HomeAssistant, + urlPath: string | null +): Promise => hass.callWS({ type: "lovelace/config/delete", + url_path: urlPath, }); export const subscribeLovelaceUpdates = ( conn: Connection, + urlPath: string | null, onChange: () => void -) => conn.subscribeEvents(onChange, "lovelace_updated"); +) => + conn.subscribeEvents((ev) => { + if (ev.data.url_path === urlPath) { + onChange(); + } + }, "lovelace_updated"); -export const getLovelaceCollection = (conn: Connection) => +export const getLovelaceCollection = ( + conn: Connection, + urlPath: string | null = null +) => getCollection( conn, - "_lovelace", - (conn2) => fetchConfig(conn2, false), + `_lovelace_${urlPath ?? ""}`, + (conn2) => fetchConfig(conn2, urlPath, false), (_conn, store) => - subscribeLovelaceUpdates(conn, () => - fetchConfig(conn, false).then((config) => store.setState(config, true)) + subscribeLovelaceUpdates(conn, urlPath, () => + fetchConfig(conn, urlPath, false).then((config) => + store.setState(config, true) + ) ) ); export interface WindowWithLovelaceProm extends Window { llConfProm?: Promise; + llResProm?: Promise; } export interface ActionHandlerOptions { diff --git a/src/data/sensor.ts b/src/data/sensor.ts index e4da2f9825..7c05a30c08 100644 --- a/src/data/sensor.ts +++ b/src/data/sensor.ts @@ -1 +1,2 @@ export const SENSOR_DEVICE_CLASS_BATTERY = "battery"; +export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp"; diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 77b37f2092..ff19b3d4bb 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -129,6 +129,10 @@ class DialogBox extends LitElement { return [ haStyleDialog, css` + :host([inert]) { + pointer-events: initial !important; + cursor: initial !important; + } ha-paper-dialog { min-width: 400px; max-width: 500px; diff --git a/src/dialogs/ha-more-info-dialog.js b/src/dialogs/ha-more-info-dialog.js index 878f17473d..135855db5b 100644 --- a/src/dialogs/ha-more-info-dialog.js +++ b/src/dialogs/ha-more-info-dialog.js @@ -8,7 +8,6 @@ import "../resources/ha-style"; import "./more-info/more-info-controls"; import { computeStateDomain } from "../common/entity/compute_state_domain"; -import { isComponentLoaded } from "../common/config/is_component_loaded"; import DialogMixin from "../mixins/dialog-mixin"; @@ -81,7 +80,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { hass="[[hass]]" state-obj="[[stateObj]]" dialog-element="[[_dialogElement()]]" - registry-entry="[[_registryInfo]]" large="{{large}}" > `; @@ -102,8 +100,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { observer: "_largeChanged", }, - _registryInfo: Object, - dataDomain: { computed: "_computeDomain(stateObj)", reflectToAttribute: true, @@ -127,11 +123,10 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { return hass.states[hass.moreInfoEntityId] || null; } - async _stateObjChanged(newVal, oldVal) { + async _stateObjChanged(newVal) { if (!newVal) { this.setProperties({ opened: false, - _registryInfo: null, large: false, }); return; @@ -144,25 +139,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { this.opened = true; }) ); - - if ( - !isComponentLoaded(this.hass, "config") || - (oldVal && oldVal.entity_id === newVal.entity_id) - ) { - return; - } - - if (this.hass.user.is_admin) { - try { - const info = await this.hass.callWS({ - type: "config/entity_registry/get", - entity_id: newVal.entity_id, - }); - this._registryInfo = info; - } catch (err) { - this._registryInfo = null; - } - } } _dialogOpenChanged(newVal) { diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index 2d2b3eece7..3dda7275b0 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -45,7 +45,7 @@ class MoreInfoCamera extends LitElement { return html` diff --git a/src/dialogs/more-info/controls/more-info-person.ts b/src/dialogs/more-info/controls/more-info-person.ts index 4e5ae33c32..d4c2bb2e0c 100644 --- a/src/dialogs/more-info/controls/more-info-person.ts +++ b/src/dialogs/more-info/controls/more-info-person.ts @@ -39,7 +39,8 @@ class MoreInfoPerson extends LitElement { > ` : ""} - ${this.hass.user?.is_admin && + ${!__DEMO__ && + this.hass.user?.is_admin && this.stateObj.state === "not_home" && this.stateObj.attributes.latitude && this.stateObj.attributes.longitude diff --git a/src/dialogs/more-info/more-info-controls.js b/src/dialogs/more-info/more-info-controls.js index c4ff5e4f6f..0da3f5ac7c 100644 --- a/src/dialogs/more-info/more-info-controls.js +++ b/src/dialogs/more-info/more-info-controls.js @@ -22,7 +22,7 @@ import LocalizeMixin from "../../mixins/localize-mixin"; import { computeRTL } from "../../common/util/compute_rtl"; import { removeEntityRegistryEntry } from "../../data/entity_registry"; import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { showEntityRegistryDetailDialog } from "../../panels/config/entities/show-dialog-entity-registry-detail"; +import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; @@ -88,7 +88,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
[[_computeStateName(stateObj)]]
-