Compare commits

...

23 Commits

Author SHA1 Message Date
Paulus Schoutsen
e3a137c675 Version bump to 20180911.0 2018-09-11 21:35:28 +02:00
Paulus Schoutsen
10aa99abdc Update translations 2018-09-11 21:35:08 +02:00
Paulus Schoutsen
34567d451f Add UI for tokens (#1656)
* Add UI for tokens

* Update strings

* Update text

* Update text
2018-09-11 21:29:40 +02:00
PhracturedBlue
494e3dc62c Fix authorization and display issues in mailbox view (#1610)
* Mailbox: Fix authorization issues.  Remove backdrop

* Fix linting issues

* Use HA Dialog system.  Fix authorization

* Add back missing backdrop

* Linting errors

* Use callApi.  Add error checking and spinner

* linting error

* more linting errors

* minor requested fixes

* Use let/const.  Fix lint issues

* Remove blob test that can never fail

* More let vs var fixes

* More minor requested fixes

* Rework code to use fetchWithAuth

* Async tweaks

* Lint

* Fix onboarding

* Add credentials for onboarding

* Lint
2018-09-11 11:33:57 +02:00
Paulus Schoutsen
0997274f29 Update external auth (#1655)
* Update external auth

* Lint
2018-09-11 10:24:01 +02:00
cdce8p
76161329b6 Add lovelace template extension points (#1653) 2018-09-10 23:15:29 +02:00
John Arild Berentsen
8505750958 Load ozw-log in new window, and add tail-like button. (#1652)
* Load log in new window, and add tail

* avoid duplicate code
2018-09-10 21:53:16 +02:00
Paulus Schoutsen
4077105db1 Version bump to 20180910.0 2018-09-10 13:28:51 +02:00
Paulus Schoutsen
3f31d83a55 Update translations 2018-09-10 13:28:33 +02:00
Paulus Schoutsen
d729e3c567 Update HAWS to 3.1.2 2018-09-10 13:25:50 +02:00
Paulus Schoutsen
9af75f9a43 Prevent changing domain entity ID (#1650) 2018-09-10 13:14:21 +02:00
Alessandro Staniscia
d32d334a2e Review Docker management (#1113)
* Review Docker management ( linked with #934 )

*  fix comment by @balloob

* Explicit remove of  package-lock.json

* moved on feature branch, merge docker scripts, added documetation

* Used alphine as requested by @balloob on https://github.com/home-assistant/home-assistant-polymer/pull/947 and followed the @mcspr comment https://github.com/home-assistant/home-assistant-polymer/issues/934

* Remove package-lock from gitignore, we don't use npm

* Update for new build instructions
2018-09-10 11:58:18 +02:00
Charles Garwood
94006a843c ZWave Panel Updates (#1647)
* Add padding to zwave log text

* Replace entity dropdown with actual entity_id

* Add Node Information button for more-info popup, and remove less-functional Node Info card.

* Fix indentation

* Address review comments

* Fix lint/mixin

* Update comment
2018-09-10 10:16:10 +02:00
Jason Hu
4790590327 Try to resolve workbox warning (#1648) 2018-09-10 10:15:20 +02:00
Charles Garwood
7cf7763e21 Fix body stream already read error (#1646) 2018-09-08 21:06:07 +02:00
Paulus Schoutsen
0d7979a72f Add revoke token to (external) auth (#1639)
* Add revoke token to external auth

* Lint

* Update to HAWS 3.1.1

* Fix constant
2018-09-07 20:37:06 +02:00
Paulus Schoutsen
300425e698 Redirect to onboarding from auth page (#1640)
* Redirect to onboarding from auth page

* Remove left over trial code
2018-09-07 20:13:00 +02:00
Stephen Vanterpool
59010baf89 Fix the way calls are made over the javascript bridge (#1644)
* Fix the way calls are made over the javascript bridge

* Update external_auth.js
2018-09-07 20:12:52 +02:00
Paulus Schoutsen
47fcb122a2 Don't delete system generated user (#1638) 2018-09-07 19:41:06 +02:00
Paulus Schoutsen
bbb50b1397 Better handle auth (#1637)
* Better handle auth

* Lint
2018-09-07 19:40:56 +02:00
Paulus Schoutsen
ae8724d699 Compress using zopfli (#1636) 2018-09-05 11:41:03 +02:00
Paulus Schoutsen
2169f6979d Remove link to alexa web 2018-09-04 15:02:35 +02:00
Paulus Schoutsen
9cc577e9c7 Add external auth (#1621)
* Add external auth

* Lint

* Warn when external auth not present
2018-09-03 09:00:39 -07:00
68 changed files with 1881 additions and 681 deletions

View File

@@ -1,25 +1,31 @@
FROM node:8.2.1-alpine FROM node:8.9-alpine
# install yarn # install yarn
ENV PATH /root/.yarn/bin:$PATH ENV PATH /root/.yarn/bin:$PATH
## Install/force base tools
RUN apk update \ RUN apk update \
&& apk add curl bash binutils tar git python3 \ && apk add make g++ curl bash binutils tar git python2 python3 \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& /bin/bash \ && /bin/bash \
&& touch ~/.bashrc \ && touch ~/.bashrc
&& curl -o- -L https://yarnpkg.com/install.sh | bash
## Install yarn
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
## Setup the project
RUN mkdir -p /frontend RUN mkdir -p /frontend
WORKDIR /frontend WORKDIR /frontend
ENV NODE_ENV production COPY package.json yarn.lock ./
COPY package.json ./ RUN yarn install --frozen-lockfile
RUN yarn
COPY bower.json ./
RUN ./node_modules/.bin/bower install --allow-root
COPY . . COPY . .
CMD [ "/bin/bash", "./script/build_frontend" ]
COPY script/docker_entrypoint.sh /usr/bin/docker_entrypoint.sh
RUN chmod +x /usr/bin/docker_entrypoint.sh
CMD [ "docker_entrypoint.sh" ]

View File

@@ -16,6 +16,18 @@ This is the repository for the official [Home Assistant](https://home-assistant.
- Gallery: `cd gallery && script/develop_gallery` - Gallery: `cd gallery && script/develop_gallery`
- Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html) - Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html)
## Frontend development
### Classic environment
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
### Docker environment
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
* `sh ./script/docker_run.sh build` Build all the project with one command
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
## License ## License
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.

View File

@@ -10,7 +10,9 @@
"build": "script/build_frontend", "build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src test-mocha && polymer lint", "lint": "eslint src hassio/src gallery/src test-mocha && polymer lint",
"mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts", "mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha" "test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
"bash": "sh ./script/docker_run.sh bash $npm_package_version"
}, },
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -68,7 +70,7 @@
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.0", "eslint-import-resolver-webpack": "^0.10.0",
"fecha": "^2.3.3", "fecha": "^2.3.3",
"home-assistant-js-websocket": "^3.0.0", "home-assistant-js-websocket": "^3.1.2",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"js-yaml": "^3.12.0", "js-yaml": "^3.12.0",
"leaflet": "^1.3.1", "leaflet": "^1.3.1",
@@ -84,6 +86,7 @@
"xss": "^1.0.3" "xss": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"@gfx/zopfli": "^1.0.8",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-eslint": "^8.2.3", "babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4", "babel-loader": "^7.1.4",
@@ -93,7 +96,7 @@
"babel-preset-env": "^1.7.0", "babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"chai": "^4.1.2", "chai": "^4.1.2",
"compression-webpack-plugin": "^1.1.11", "compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.1", "copy-webpack-plugin": "^4.5.1",
"del": "^3.0.0", "del": "^3.0.0",
"eslint": "^4.19.1", "eslint": "^4.19.1",

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Docker entry point inspired by travis build and script/build_frontend
# Stop on errors
set -e
# Build the frontend but not used the npm run build
/bin/bash script/build_frontend
# TEST
npm run test
#
#xvfb-run wct

103
script/docker_run.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
# Basic Docker Management scripts
# With this script you can build software, or enter an agnostic development environment and run commands interactively.
check_mandatory_tools(){
if [ "x$(which docker)" == "x" ]; then
echo "UNKNOWN - Missing docker binary! Are you sure it is installed and reachable?"
exit 3
fi
docker info > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "UNKNOWN - Unable to talk to the docker daemon! Maybe the docker daemon is not running"
exit 3
fi
}
check_dev_image(){
if [[ "$(docker images -q ${IMAGE_NAME}:$IMAGE_TAG 2> /dev/null)" == "" ]]; then
echo "UNKNOWN - Can't find the development docker image ${IMAGE_NAME}:$IMAGE_TAG"
while true; do
read -p "Do you want to create it now?" yn
case $yn in
[Yy]* ) create_image; break;;
[Nn]* ) exit 3;;
* ) echo "Please answer y or n";;
esac
done
fi
}
# Building the basic image for compiling the production frontend
create_image(){
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
}
#
# Execute interactive bash on basic image
#
run_bash_on_docker(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash
}
#
# Execute the basic image for compiling the production frontend
#
build_all(){
check_dev_image
docker run -it \
-v $PWD/:/frontend/ \
-v /frontend/node_modules \
-v /frontend/bower_components \
${IMAGE_NAME}:${IMAGE_TAG} /bin/bash script/build_frontend
}
# Init Global Variable
IMAGE_NAME=home_assistant_fe_image
IMAGE_TAG=${2:-latest}
check_mandatory_tools
case "$1" in
setup_env)
create_image
;;
bash)
run_bash_on_docker
;;
build)
build_all
;;
*)
echo "NAME"
echo " Docker Management."
echo ""
echo "SYNOPSIS"
echo " ${0} command [version]"
echo ""
echo "DESCRIPTION"
echo " With this script you can build software, or enter an agnostic development environment and run commands interactively."
echo ""
echo " The command are:"
echo " setup_env Create develop images"
echo " bash Run bash on develop enviroments"
echo " build Run silent build"
echo ""
echo " The version is optional, if not inserted it assumes \"latest\". "
exit 1
;;
esac
exit 0

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup(name='home-assistant-frontend', setup(name='home-assistant-frontend',
version='20180903.0', version='20180911.0',
description='The Home Assistant frontend', description='The Home Assistant frontend',
url='https://github.com/home-assistant/home-assistant-polymer', url='https://github.com/home-assistant/home-assistant-polymer',
author='The Home Assistant Authors', author='The Home Assistant Authors',

View File

@@ -94,6 +94,12 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
const response = await window.providersPromise; const response = await window.providersPromise;
const authProviders = await response.json(); const authProviders = await response.json();
// Forward to main screen which will redirect to right onboarding page.
if (response.status === 400 && authProviders.code === 'onboarding_required') {
location.href = '/';
return;
}
if (authProviders.length === 0) { if (authProviders.length === 0) {
alert('No auth providers returned. Unable to finish login.'); alert('No auth providers returned. Unable to finish login.');
return; return;

View File

@@ -0,0 +1,70 @@
/**
* Auth class that connects to a native app for authentication.
*/
import { Auth } from 'home-assistant-js-websocket';
const CALLBACK_SET_TOKEN = 'externalAuthSetToken';
const CALLBACK_REVOKE_TOKEN = 'externalAuthRevokeToken';
if (!window.externalApp && !window.webkit) {
throw new Error('External auth requires either externalApp or webkit defined on Window object.');
}
export default class ExternalAuth extends Auth {
constructor(hassUrl) {
super();
this.data = {
hassUrl,
access_token: '',
// This will trigger connection to do a refresh right away
expires: 0,
};
}
async refreshAccessToken() {
const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) => (success ? resolve(data) : reject(data));
});
// Allow promise to set resolve on window object.
await 0;
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload);
} else {
window.webkit.messageHandlers.getExternalAuth.postMessage(callbackPayload);
}
// Response we expect back:
// {
// "access_token": "qwere",
// "expires_in": 1800
// }
const tokens = await responseProm;
this.data.access_token = tokens.access_token;
this.data.expires = (tokens.expires_in * 1000) + Date.now();
}
async revoke() {
const responseProm = new Promise((resolve, reject) => {
window[CALLBACK_REVOKE_TOKEN] = (success, data) => (success ? resolve(data) : reject(data));
});
// Allow promise to set resolve on window object.
await 0;
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload);
} else {
window.webkit.messageHandlers.revokeExternalAuth.postMessage(callbackPayload);
}
await responseProm;
}
}

View File

@@ -9,6 +9,7 @@ import EventsMixin from '../../mixins/events-mixin.js';
import LocalizeMixin from '../../mixins/localize-mixin.js'; import LocalizeMixin from '../../mixins/localize-mixin.js';
import computeStateName from '../../common/entity/compute_state_name.js'; import computeStateName from '../../common/entity/compute_state_name.js';
import computeDomain from '../../common/entity/compute_domain.js';
import isComponentLoaded from '../../common/config/is_component_loaded.js'; import isComponentLoaded from '../../common/config/is_component_loaded.js';
/* /*
@@ -44,7 +45,10 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
<app-toolbar> <app-toolbar>
<paper-icon-button icon="hass:arrow-left" on-click="_backTapped"></paper-icon-button> <paper-icon-button icon="hass:arrow-left" on-click="_backTapped"></paper-icon-button>
<div main-title="">[[_computeStateName(stateObj)]]</div> <div main-title="">[[_computeStateName(stateObj)]]</div>
<paper-button on-click="_save">[[localize('ui.dialogs.more_info_settings.save')]]</paper-button> <paper-button
on-click="_save"
disabled='[[_computeInvalid(_entityId)]]'
>[[localize('ui.dialogs.more_info_settings.save')]]</paper-button>
</app-toolbar> </app-toolbar>
<div class="form"> <div class="form">
@@ -55,6 +59,8 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
<paper-input <paper-input
value="{{_entityId}}" value="{{_entityId}}"
label="[[localize('ui.dialogs.more_info_settings.entity_id')]]" label="[[localize('ui.dialogs.more_info_settings.entity_id')]]"
error-message="Domain needs to stay the same"
invalid='[[_computeInvalid(_entityId)]]'
></paper-input> ></paper-input>
</div> </div>
`; `;
@@ -90,6 +96,10 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
return isComponentLoaded(hass, 'config.entity_registry'); return isComponentLoaded(hass, 'config.entity_registry');
} }
_computeInvalid(entityId) {
return computeDomain(this.stateObj.entity_id) !== computeDomain(entityId);
}
_registryInfoChanged(newVal) { _registryInfoChanged(newVal) {
if (newVal) { if (newVal) {
this.setProperties({ this.setProperties({

View File

@@ -4,6 +4,7 @@ import {
subscribeConfig, subscribeConfig,
subscribeEntities, subscribeEntities,
subscribeServices, subscribeServices,
ERR_INVALID_AUTH,
} from 'home-assistant-js-websocket'; } from 'home-assistant-js-websocket';
import { loadTokens, saveTokens } from '../common/auth/token_storage.js'; import { loadTokens, saveTokens } from '../common/auth/token_storage.js';
@@ -11,21 +12,45 @@ import { subscribePanels } from '../data/ws-panels.js';
import { subscribeThemes } from '../data/ws-themes.js'; import { subscribeThemes } from '../data/ws-themes.js';
import { subscribeUser } from '../data/ws-user.js'; import { subscribeUser } from '../data/ws-user.js';
window.hassAuth = getAuth({ const hassUrl = `${location.protocol}//${location.host}`;
hassUrl: `${location.protocol}//${location.host}`, const isExternal = location.search.includes('external_auth=1');
saveTokens,
loadTokens: () => Promise.resolve(loadTokens()),
});
window.hassConnection = window.hassAuth.then((auth) => { const authProm = isExternal ?
if (location.search.includes('auth_callback=1')) { () => import('../common/auth/external_auth.js')
history.replaceState(null, null, location.pathname); .then(mod => new mod.default(hassUrl)) :
() => getAuth({
hassUrl,
saveTokens,
loadTokens: () => Promise.resolve(loadTokens()),
});
const connProm = async (auth) => {
try {
const conn = await createConnection({ auth });
// Clear url if we have been able to establish a connection
if (location.search.includes('auth_callback=1')) {
history.replaceState(null, null, location.pathname);
}
return { auth, conn };
} catch (err) {
if (err !== ERR_INVALID_AUTH) {
throw err;
}
// We can get invalid auth if auth tokens were stored that are no longer valid
// Clear stored tokens.
if (!isExternal) saveTokens(null);
auth = await authProm();
const conn = await createConnection({ auth });
return { auth, conn };
} }
return createConnection({ auth }); };
});
window.hassConnection = authProm().then(connProm);
// Start fetching some of the data that we will need. // Start fetching some of the data that we will need.
window.hassConnection.then((conn) => { window.hassConnection.then(({ conn }) => {
const noop = () => {}; const noop = () => {};
subscribeEntities(conn, noop); subscribeEntities(conn, noop);
subscribeConfig(conn, noop); subscribeConfig(conn, noop);

View File

@@ -27,9 +27,16 @@ export default superClass => class extends superClass {
}); });
} }
_handleLogout() { async _handleLogout() {
this.hass.connection.close(); try {
clearState(); await this.hass.auth.revoke();
document.location.href = '/'; this.hass.connection.close();
clearState();
document.location.href = '/';
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert('Log out failed');
}
} }
}; };

View File

@@ -13,6 +13,7 @@ import EventsMixin from '../../mixins/events-mixin.js';
import { getState } from '../../util/ha-pref-storage.js'; import { getState } from '../../util/ha-pref-storage.js';
import { getActiveTranslation } from '../../util/hass-translation.js'; import { getActiveTranslation } from '../../util/hass-translation.js';
import { fetchWithAuth } from '../../util/fetch-with-auth.js';
import hassCallApi from '../../util/hass-call-api.js'; import hassCallApi from '../../util/hass-call-api.js';
import computeStateName from '../../common/entity/compute_state_name.js'; import computeStateName from '../../common/entity/compute_state_name.js';
import { subscribePanels } from '../../data/ws-panels'; import { subscribePanels } from '../../data/ws-panels';
@@ -25,7 +26,16 @@ export default superClass =>
} }
async _handleConnProm() { async _handleConnProm() {
const [auth, conn] = await Promise.all([window.hassAuth, window.hassConnection]); let auth;
let conn;
try {
const result = await window.hassConnection;
auth = result.auth;
conn = result.conn;
} catch (err) {
this._error = true;
return;
}
this.hass = Object.assign({ this.hass = Object.assign({
auth, auth,
@@ -80,23 +90,9 @@ export default superClass =>
throw err; throw err;
} }
}, },
callApi: async (method, path, parameters) => { callApi: async (method, path, parameters) =>
const host = window.location.protocol + '//' + window.location.host; hassCallApi(auth, method, path, parameters),
fetchWithAuth: (path, init) => fetchWithAuth(auth, `${auth.data.hassUrl}${path}`, init),
try {
if (auth.expired) await auth.refreshAccessToken();
} catch (err) {
if (err === ERR_INVALID_AUTH) {
// Trigger auth flow
location.reload();
// ensure further JS is not executed
await new Promise(() => {});
}
throw err;
}
return await hassCallApi(host, auth, method, path, parameters);
},
// For messages that do not get a response // For messages that do not get a response
sendWS: (msg) => { sendWS: (msg) => {
// eslint-disable-next-line // eslint-disable-next-line

View File

@@ -51,7 +51,7 @@ class HomeAssistant extends ext(PolymerElement, [
</template> </template>
<template is="dom-if" if="[[!showMain]]" restamp> <template is="dom-if" if="[[!showMain]]" restamp>
<ha-init-page></ha-init-page> <ha-init-page error='[[_error]]'></ha-init-page>
</template> </template>
`; `;
} }
@@ -73,6 +73,10 @@ class HomeAssistant extends ext(PolymerElement, [
computed: 'computePanelUrl(routeData)', computed: 'computePanelUrl(routeData)',
observer: 'panelUrlChanged', observer: 'panelUrlChanged',
}, },
_error: {
type: Boolean,
value: false,
}
}; };
} }

View File

@@ -1,12 +1,8 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/paper-button/paper-button.js'; import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-checkbox/paper-checkbox.js';
import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-spinner/paper-spinner.js'; import '@polymer/paper-spinner/paper-spinner.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js'; import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocket';
import LocalizeMixin from '../mixins/localize-mixin.js'; import LocalizeMixin from '../mixins/localize-mixin.js';
import EventsMixin from '../mixins/events-mixin.js'; import EventsMixin from '../mixins/events-mixin.js';
@@ -26,82 +22,26 @@ class HaInitPage extends EventsMixin(LocalizeMixin(PolymerElement)) {
<div class="layout vertical center center-center fit"> <div class="layout vertical center center-center fit">
<img src="/static/icons/favicon-192x192.png" height="192"> <img src="/static/icons/favicon-192x192.png" height="192">
<paper-spinner active="true"></paper-spinner> <paper-spinner active="[[!error]]"></paper-spinner>
Loading data <template is='dom-if' if='[[error]]'>
Unable to connect to Home Assistant.
<paper-button on-click='_retry'>Retry</paper-button>
</template>
<template is='dom-if' if='[[!error]]'>
Loading data
</template>
</div> </div>
`; `;
} }
ready() { static get properties() {
super.ready(); return {
this.addEventListener('keydown', ev => this.passwordKeyDown(ev)); error: Boolean,
};
} }
computeLoadingMsg(isValidating) { _retry() {
return isValidating ? 'Connecting' : 'Loading data'; location.reload();
}
computeShowSpinner(forceShowLoading, isValidating) {
return forceShowLoading || isValidating;
}
isValidatingChanged(newVal) {
if (!newVal) {
setTimeout(() => {
if (this.$.passwordInput.inputElement.inputElement) {
this.$.passwordInput.inputElement.inputElement.focus();
}
}, 10);
}
}
passwordKeyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
this.validatePassword();
ev.preventDefault();
// clear error after we start typing again
} else if (this.errorMessage) {
this.errorMessage = '';
}
}
validatePassword() {
var auth = this.password;
this.$.hideKeyboardOnFocus.focus();
const connProm = window.createHassConnection(auth);
this.fire('try-connection', { connProm });
if (this.$.rememberLogin.checked) {
connProm.then(function () {
localStorage.authToken = auth;
});
}
}
handleConnectionPromiseChanged(newVal) {
if (!newVal) return;
var el = this;
this.isValidating = true;
this.connectionPromise.then(
function () {
el.isValidating = false;
el.password = '';
},
function (errCode) {
el.isValidating = false;
if (errCode === ERR_CANNOT_CONNECT) {
el.errorMessage = 'Unable to connect';
} else if (errCode === ERR_INVALID_AUTH) {
el.errorMessage = 'Invalid password';
} else {
el.errorMessage = 'Unknown error: ' + errCode;
}
}
);
} }
} }

View File

@@ -4,11 +4,8 @@ import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-button/paper-button.js'; import '@polymer/paper-button/paper-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js'; import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import hassCallApi from '../util/hass-call-api.js';
import localizeLiteMixin from '../mixins/localize-lite-mixin.js'; import localizeLiteMixin from '../mixins/localize-lite-mixin.js';
const callApi = (method, path, data) => hassCallApi('', {}, method, path, data);
class HaOnboarding extends localizeLiteMixin(PolymerElement) { class HaOnboarding extends localizeLiteMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
@@ -141,12 +138,23 @@ class HaOnboarding extends localizeLiteMixin(PolymerElement) {
this._errorMsg = ''; this._errorMsg = '';
try { try {
await callApi('post', 'onboarding/users', { const response = await fetch('/api/onboarding/users', {
name: this._name, method: 'POST',
username: this._username, credentials: 'same-origin',
password: this._password, body: JSON.stringify({
name: this._name,
username: this._username,
password: this._password,
})
}); });
if (!response.ok) {
// eslint-disable-next-line
throw {
message: `Bad response from server: ${response.status}`
};
}
document.location = '/'; document.location = '/';
} catch (err) { } catch (err) {
// eslint-disable-next-line // eslint-disable-next-line

View File

@@ -97,9 +97,7 @@ class HaConfigCloudAccount extends EventsMixin(PolymerElement) {
With the Alexa integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Alexa-enabled device. With the Alexa integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Alexa-enabled device.
<ul> <ul>
<li> <li>
<a href="https://alexa.amazon.com/spa/index.html#skills/dp/B0772J1QKB/?ref=skill_dsk_skb_sr_2" target="_blank"> To activate, search in the Alexa app for the Home Assistant Smart Home skill.
Activate the Home Assistant skill for Alexa
</a>
</li> </li>
<li> <li>
<a href="https://www.home-assistant.io/cloud/alexa/" target="_blank"> <a href="https://www.home-assistant.io/cloud/alexa/" target="_blank">

View File

@@ -29,9 +29,6 @@ class HaUserEditor extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
paper-card:last-child { paper-card:last-child {
margin-bottom: 16px; margin-bottom: 16px;
} }
paper-button {
display: block;
}
</style> </style>
<hass-subpage header="View user"> <hass-subpage header="View user">
@@ -57,9 +54,12 @@ class HaUserEditor extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
</paper-card> </paper-card>
<paper-card> <paper-card>
<div class='card-actions'> <div class='card-actions'>
<paper-button on-click='_deleteUser'> <paper-button on-click='_deleteUser' disabled='[[user.system_generated]]'>
[[localize('ui.panel.config.users.editor.delete_user')]] [[localize('ui.panel.config.users.editor.delete_user')]]
</paper-button> </paper-button>
<template is='dom-if' if='[[user.system_generated]]'>
Unable to remove system generated users.
</template>
</div> </div>
</paper-card> </paper-card>
</hass-subpage> </hass-subpage>

View File

@@ -50,7 +50,12 @@ class HaUserPicker extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
<div>[[_withDefault(user.name, 'Unnamed User')]]</div> <div>[[_withDefault(user.name, 'Unnamed User')]]</div>
<div secondary="">[[user.id]]</div> <div secondary="">
[[user.id]]
<template is='dom-if' if='[[user.system_generated]]'>
- System Generated
</template>
</div>
</paper-item-body> </paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon> <iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item> </paper-item>

View File

@@ -21,7 +21,6 @@ import './zwave-groups.js';
import './zwave-log.js'; import './zwave-log.js';
import './zwave-network.js'; import './zwave-network.js';
import './zwave-node-config.js'; import './zwave-node-config.js';
import './zwave-node-information.js';
import './zwave-usercodes.js'; import './zwave-usercodes.js';
import './zwave-values.js'; import './zwave-values.js';
import './zwave-node-protection.js'; import './zwave-node-protection.js';
@@ -29,12 +28,14 @@ import './zwave-node-protection.js';
import sortByName from '../../../common/entity/states_sort_by_name.js'; import sortByName from '../../../common/entity/states_sort_by_name.js';
import computeStateName from '../../../common/entity/compute_state_name.js'; import computeStateName from '../../../common/entity/compute_state_name.js';
import computeStateDomain from '../../../common/entity/compute_state_domain.js'; import computeStateDomain from '../../../common/entity/compute_state_domain.js';
import EventsMixin from '../../../mixins/events-mixin.js';
import LocalizeMixin from '../../../mixins/localize-mixin.js'; import LocalizeMixin from '../../../mixins/localize-mixin.js';
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/ */
class HaConfigZwave extends LocalizeMixin(PolymerElement) { class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style ha-form-style"> <style include="iron-flex ha-style ha-form-style">
@@ -203,13 +204,14 @@ class HaConfigZwave extends LocalizeMixin(PolymerElement) {
service="test_node" service="test_node"
hidden$="[[!showHelp]]"> hidden$="[[!showHelp]]">
</ha-service-description> </ha-service-description>
<paper-button on-click="_nodeMoreInfo">Node Information</paper-button>
</div> </div>
<div class="device-picker"> <div class="device-picker">
<paper-dropdown-menu label="Entities of this node" dynamic-align="" class="flex"> <paper-dropdown-menu label="Entities of this node" dynamic-align="" class="flex">
<paper-listbox slot="dropdown-content" selected="{{selectedEntity}}"> <paper-listbox slot="dropdown-content" selected="{{selectedEntity}}">
<template is="dom-repeat" items="[[entities]]" as="state"> <template is="dom-repeat" items="[[entities]]" as="state">
<paper-item>[[computeSelectCaptionEnt(state)]]</paper-item> <paper-item>[[state.entity_id]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
@@ -269,12 +271,6 @@ class HaConfigZwave extends LocalizeMixin(PolymerElement) {
</paper-card> </paper-card>
<template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]"> <template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]">
<!--Node info card-->
<zwave-node-information
id="zwave-node-information"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
></zwave-node-information>
<!--Value card--> <!--Value card-->
<zwave-values <zwave-values
@@ -563,6 +559,10 @@ class HaConfigZwave extends LocalizeMixin(PolymerElement) {
}; };
} }
_nodeMoreInfo() {
this.fire('hass-more-info', { entityId: this.nodes[this.selectedNode].entity_id });
}
_saveEntity() { _saveEntity() {
const data = { const data = {
ignored: this.entityIgnored, ignored: this.entityIgnored,

View File

@@ -26,20 +26,18 @@ class OzwLog extends PolymerElement {
padding-right: 24px; padding-right: 24px;
padding-bottom: 24px; padding-bottom: 24px;
} }
</style> </style>
<ha-config-section is-wide="[[isWide]]"> <ha-config-section is-wide="[[isWide]]">
<span slot="header">OZW Log</span> <span slot="header">OZW Log</span>
<paper-card> <paper-card>
<div class="device-picker"> <div class="device-picker">
<paper-input label="Number of last log lines." type="number" min="0" max="1000" step="10" value="{{numLogLines}}"> <paper-input label="Number of last log lines." type="number" min="0" max="1000" step="10" value="{{_numLogLines}}">
</paper-input> </paper-input>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<paper-button raised="" on-click="refreshLog">Refresh</paper-button> <paper-button raised="true" on-click="_openLogWindow">Load</paper-button>
</div> <paper-button raised="true" on-click="_tailLog" disabled="{{_completeLog}}">Tail</paper-button>
<div class="help-text">
<pre>[[ozwLogs]]</pre>
</div>
</paper-card> </paper-card>
</ha-config-section> </ha-config-section>
`; `;
@@ -54,25 +52,53 @@ class OzwLog extends PolymerElement {
value: false, value: false,
}, },
ozwLogs: { _ozwLogs: String,
type: String,
value: 'Refresh to pull log' _completeLog: {
type: Boolean,
value: true
}, },
numLogLines: { _numLogLines: {
type: Number, type: Number,
value: 0 value: 0,
observer: '_isCompleteLog'
}, },
_intervalId: String,
}; };
} }
refreshLog() { async _tailLog() {
this.ozwLogs = 'Loading ozw log...'; const ozwWindow = await this._openLogWindow();
this.hass.callApi('GET', 'zwave/ozwlog?lines=' + this.numLogLines) this.setProperties({
.then((info) => { _intervalId: setInterval(() => { this._refreshLog(ozwWindow); }, 1500) });
this.ozwLogs = info; }
});
async _openLogWindow() {
const info = await this.hass.callApi('GET', 'zwave/ozwlog?lines=' + this._numLogLines);
this.setProperties({ _ozwLogs: info });
const ozwWindow = window.open('', 'OpenZwave internal log', 'toolbar');
ozwWindow.document.title = 'OpenZwave internal logfile';
ozwWindow.document.body.innerText = this._ozwLogs;
return ozwWindow;
}
async _refreshLog(ozwWindow) {
if (ozwWindow.closed === true) {
clearInterval(this._intervalId);
this.setProperties({ _intervalId: null });
} else {
const info = await this.hass.callApi('GET', 'zwave/ozwlog?lines=' + this._numLogLines);
this.setProperties({ _ozwLogs: info });
ozwWindow.document.body.innerText = this._ozwLogs;
}
}
_isCompleteLog() {
if (this._numLogLines !== '0') {
this.setProperties({ _completeLog: false });
} else { this.setProperties({ _completeLog: true }); }
} }
} }
customElements.define('ozw-log', OzwLog); customElements.define('ozw-log', OzwLog);

View File

@@ -1,74 +0,0 @@
import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-card/paper-card.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
class ZwaveNodeInformation extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
.node-info {
margin-left: 16px;
}
paper-card {
display: block;
margin: 0 auto;
max-width: 600px;
}
paper-button[toggles][active] {
background: lightgray;
}
</style>
<div class="content">
<paper-card heading="Node Information">
<div class="card-actions">
<paper-button toggles="" raised="" noink="" active="{{nodeInfoActive}}">Show</paper-button>
</div>
<template is="dom-if" if="{{nodeInfoActive}}">
<template is="dom-repeat" items="[[selectedNodeAttrs]]" as="state">
<div class="node-info">
<span>[[state]]</span>
</div>
</template>
</template>
</paper-card>
</div>
`;
}
static get properties() {
return {
nodes: Array,
selectedNode: {
type: Number,
value: -1,
observer: 'nodeChanged'
},
selectedNodeAttrs: Array,
nodeInfoActive: Boolean,
};
}
nodeChanged(selectedNode) {
if (!this.nodes || selectedNode === -1) return;
const nodeAttrs = this.nodes[this.selectedNode].attributes;
const att = [];
Object.keys(nodeAttrs).forEach((key) => {
att.push(key + ': ' + nodeAttrs[key]);
});
this.selectedNodeAttrs = att.sort();
}
}
customElements.define('zwave-node-information', ZwaveNodeInformation);

View File

@@ -9,6 +9,24 @@ import computeStateName from '../../../common/entity/compute_state_name.js';
class HuiGenericEntityRow extends PolymerElement { class HuiGenericEntityRow extends PolymerElement {
static get template() { static get template() {
return html`
${this.styleTemplate}
<template is="dom-if" if="[[_stateObj]]">
${this.stateBadgeTemplate}
<div class="flex">
${this.infoTemplate}
<slot></slot>
</div>
</template>
<template is="dom-if" if="[[!_stateObj]]">
<div class="not-found">
Entity not available: [[config.entity]]
</div>
</template>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
:host { :host {
@@ -50,36 +68,36 @@ class HuiGenericEntityRow extends PolymerElement {
flex: 0 0 40px; flex: 0 0 40px;
} }
</style> </style>
<template is="dom-if" if="[[_stateObj]]"> `;
<state-badge }
state-obj="[[_stateObj]]"
override-icon="[[config.icon]]" static get stateBadgeTemplate() {
></state-badge> return html`
<div class="flex"> <state-badge
<div class="info"> state-obj="[[_stateObj]]"
[[_computeName(config.name, _stateObj)]] override-icon="[[config.icon]]"
<template is="dom-if" if="[[config.secondary_info]]"> ></state-badge>
<template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]"> `;
<div class="secondary"> }
[[_stateObj.entity_id]]
</div> static get infoTemplate() {
</template> return html`
<template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]"> <div class="info">
<ha-relative-time [[_computeName(config.name, _stateObj)]]
hass="[[hass]]" <template is="dom-if" if="[[config.secondary_info]]">
datetime="[[_stateObj.last_changed]]" <template is="dom-if" if="[[_equals(config.secondary_info, 'entity-id')]]">
></ha-relative-time> <div class="secondary">
</template> [[_stateObj.entity_id]]
</template> </div>
</div> </template>
<slot></slot> <template is="dom-if" if="[[_equals(config.secondary_info, 'last-changed')]]">
</div> <ha-relative-time
</template> hass="[[hass]]"
<template is="dom-if" if="[[!_stateObj]]"> datetime="[[_stateObj.last_changed]]"
<div class="not-found"> ></ha-relative-time>
Entity not available: [[config.entity]] </template>
</div> </template>
</template> </div>
`; `;
} }

View File

@@ -6,21 +6,33 @@ import '../components/hui-generic-entity-row.js';
class HuiClimateEntityRow extends PolymerElement { class HuiClimateEntityRow extends PolymerElement {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.climateControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
ha-climate-state { ha-climate-state {
text-align: right; text-align: right;
} }
</style> </style>
<hui-generic-entity-row `;
}
static get climateControlTemplate() {
return html`
<ha-climate-state
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" state-obj="[[_stateObj]]"
> ></ha-climate-state>
<ha-climate-state
hass="[[hass]]"
state-obj="[[_stateObj]]"
></ha-climate-state>
</hui-generic-entity-row>
`; `;
} }

View File

@@ -8,6 +8,18 @@ import CoverEntity from '../../../util/cover-model.js';
class HuiCoverEntityRow extends PolymerElement { class HuiCoverEntityRow extends PolymerElement {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.coverControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
ha-cover-controls, ha-cover-controls,
@@ -15,17 +27,17 @@ class HuiCoverEntityRow extends PolymerElement {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
> static get coverControlTemplate() {
<template is="dom-if" if="[[!_entityObj.isTiltOnly]]"> return html`
<ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls> <template is="dom-if" if="[[!_entityObj.isTiltOnly]]">
</template> <ha-cover-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-controls>
<template is="dom-if" if="[[_entityObj.isTiltOnly]]"> </template>
<ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls> <template is="dom-if" if="[[_entityObj.isTiltOnly]]">
</template> <ha-cover-tilt-controls hass="[[hass]]" state-obj="[[_stateObj]]"></ha-cover-tilt-controls>
</hui-generic-entity-row> </template>
`; `;
} }

View File

@@ -18,21 +18,27 @@ class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) {
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
<template is="dom-if" if="[[_canToggle]]"> ${this.groupControlTemplate}
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[_stateObj]]"
></ha-entity-toggle>
</template>
<template is="dom-if" if="[[!_canToggle]]">
<div>
[[_computeState(_stateObj)]]
</div>
</template>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get groupControlTemplate() {
return html`
<template is="dom-if" if="[[_canToggle]]">
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[_stateObj]]"
></ha-entity-toggle>
</template>
<template is="dom-if" if="[[!_canToggle]]">
<div>
[[_computeState(_stateObj)]]
</div>
</template>
`;
}
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,

View File

@@ -9,6 +9,19 @@ import '../components/hui-generic-entity-row.js';
class HuiInputNumberEntityRow extends mixinBehaviors([IronResizableBehavior], PolymerElement) { class HuiInputNumberEntityRow extends mixinBehaviors([IronResizableBehavior], PolymerElement) {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
id="input_number_card"
>
${this.inputNumberControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
.flex { .flex {
@@ -23,41 +36,40 @@ class HuiInputNumberEntityRow extends mixinBehaviors([IronResizableBehavior], Po
text-align: right; text-align: right;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
id="input_number_card" static get inputNumberControlTemplate() {
> return html`
<div> <div>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]"> <template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
<div class="flex"> <div class="flex">
<paper-slider <paper-slider
min="[[_min]]"
max="[[_max]]"
value="{{_value}}"
step="[[_step]]"
pin
on-change="_selectedValueChanged"
ignore-bar-touch
></paper-slider>
<span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span>
</div>
</template>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]">
<paper-input
no-label-float
auto-validate
pattern="[0-9]+([\\.][0-9]+)?"
step="[[_step]]"
min="[[_min]]" min="[[_min]]"
max="[[_max]]" max="[[_max]]"
value="{{_value}}" value="{{_value}}"
type="number" step="[[_step]]"
pin
on-change="_selectedValueChanged" on-change="_selectedValueChanged"
></paper-input> ignore-bar-touch
</template> ></paper-slider>
</div> <span class="state">[[_value]] [[_stateObj.attributes.unit_of_measurement]]</span>
</hui-generic-entity-row> </div>
</template>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'box')]]">
<paper-input
no-label-float
auto-validate
pattern="[0-9]+([\\.][0-9]+)?"
step="[[_step]]"
min="[[_min]]"
max="[[_max]]"
value="{{_value}}"
type="number"
on-change="_selectedValueChanged"
></paper-input>
</template>
</div>
`; `;
} }

View File

@@ -16,21 +16,7 @@ import EventsMixin from '../../../mixins/events-mixin.js';
class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) { class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style> ${this.styleTemplate}
:host {
display: flex;
align-items: center;
}
paper-dropdown-menu {
margin-left: 16px;
flex: 1;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
<template is="dom-if" if="[[_stateObj]]"> <template is="dom-if" if="[[_stateObj]]">
<state-badge state-obj="[[_stateObj]]"></state-badge> <state-badge state-obj="[[_stateObj]]"></state-badge>
<paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]"> <paper-dropdown-menu on-click="_stopPropagation" selected-item-label="{{_selected}}" label="[[_computeName(_config.name, _stateObj)]]">
@@ -49,6 +35,26 @@ class HuiInputSelectEntityRow extends EventsMixin(PolymerElement) {
`; `;
} }
static get styleTemplate() {
return html`
<style>
:host {
display: flex;
align-items: center;
}
paper-dropdown-menu {
margin-left: 16px;
flex: 1;
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
`;
}
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,

View File

@@ -11,21 +11,27 @@ class HuiInputTextEntityRow extends PolymerElement {
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
<paper-input ${this.inputTextControlTemplate}
no-label-float
minlength="[[_stateObj.attributes.min]]"
maxlength="[[_stateObj.attributes.max]]"
value="{{_value}}"
auto-validate="[[_stateObj.attributes.pattern]]"
pattern="[[_stateObj.attributes.pattern]]"
type="[[_stateObj.attributes.mode]]"
on-change="_selectedValueChanged"
placeholder="(empty value)"
></paper-input>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get inputTextControlTemplate() {
return html`
<paper-input
no-label-float
minlength="[[_stateObj.attributes.min]]"
maxlength="[[_stateObj.attributes.max]]"
value="{{_value}}"
auto-validate="[[_stateObj.attributes.pattern]]"
pattern="[[_stateObj.attributes.pattern]]"
type="[[_stateObj.attributes.mode]]"
on-change="_selectedValueChanged"
placeholder="(empty value)"
></paper-input>
`;
}
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,

View File

@@ -11,6 +11,18 @@ import LocalizeMixin from '../../../mixins/localize-mixin.js';
*/ */
class HuiLockEntityRow extends LocalizeMixin(PolymerElement) { class HuiLockEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.lockControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
@@ -19,14 +31,14 @@ class HuiLockEntityRow extends LocalizeMixin(PolymerElement) {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
> static get lockControlTemplate() {
<paper-button on-click="_callService"> return html`
[[_computeButtonTitle(_stateObj.state)]] <paper-button on-click="_callService">
</paper-button> [[_computeButtonTitle(_stateObj.state)]]
</hui-generic-entity-row> </paper-button>
`; `;
} }

View File

@@ -11,6 +11,18 @@ import LocalizeMixin from '../../../mixins/localize-mixin.js';
*/ */
class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) { class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.sceneControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
@@ -19,14 +31,14 @@ class HuiSceneEntityRow extends LocalizeMixin(PolymerElement) {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
> static get sceneControlTemplate() {
<paper-button on-click="_callService"> return html`
[[localize('ui.card.scene.activate')]] <paper-button on-click="_callService">
</paper-button> [[localize('ui.card.scene.activate')]]
</hui-generic-entity-row> </paper-button>
`; `;
} }

View File

@@ -12,6 +12,18 @@ import LocalizeMixin from '../../../mixins/localize-mixin.js';
*/ */
class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) { class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.scriptControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
paper-button { paper-button {
@@ -20,17 +32,17 @@ class HuiScriptEntityRow extends LocalizeMixin(PolymerElement) {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
> static get scriptControlTemplate() {
<template is="dom-if" if="[[_stateObj.attributes.can_cancel]]"> return html`
<ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle> <template is="dom-if" if="[[_stateObj.attributes.can_cancel]]">
</template> <ha-entity-toggle state-obj="[[_stateObj]]" hass="[[hass]]"></ha-entity-toggle>
<template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]"> </template>
<paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button> <template is="dom-if" if="[[!_stateObj.attributes.can_cancel]]">
</template> <paper-button on-click="_callService">[[localize('ui.card.script.execute')]]</paper-button>
</hui-generic-entity-row> </template>
`; `;
} }

View File

@@ -12,20 +12,32 @@ import LocalizeMixin from '../../../mixins/localize-mixin.js';
*/ */
class HuiTextEntityRow extends LocalizeMixin(PolymerElement) { class HuiTextEntityRow extends LocalizeMixin(PolymerElement) {
static get template() { static get template() {
return html`
${this.styleTemplate}
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
>
${this.textControlTemplate}
</hui-generic-entity-row>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
div { div {
text-align: right; text-align: right;
} }
</style> </style>
<hui-generic-entity-row `;
hass="[[hass]]" }
config="[[_config]]"
> static get textControlTemplate() {
<div> return html`
[[_computeState(_stateObj)]] <div>
</div> [[_computeState(_stateObj)]]
</hui-generic-entity-row> </div>
`; `;
} }

View File

@@ -13,13 +13,19 @@ class HuiTimerEntityRow extends PolymerElement {
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
<div> ${this.timerControlTemplate}
[[_computeDisplay(_stateObj, _timeRemaining)]]
</div>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get timerControlTemplate() {
return html`
<div>
[[_computeDisplay(_stateObj, _timeRemaining)]]
</div>
`;
}
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,

View File

@@ -18,21 +18,27 @@ class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) {
hass="[[hass]]" hass="[[hass]]"
config="[[_config]]" config="[[_config]]"
> >
<template is="dom-if" if="[[_canToggle]]"> ${this.toggleControlTemplate}
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[_stateObj]]"
></ha-entity-toggle>
</template>
<template is="dom-if" if="[[!_canToggle]]">
<div>
[[_computeState(_stateObj)]]
</div>
</template>
</hui-generic-entity-row> </hui-generic-entity-row>
`; `;
} }
static get toggleControlTemplate() {
return html`
<template is="dom-if" if="[[_canToggle]]">
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[_stateObj]]"
></ha-entity-toggle>
</template>
<template is="dom-if" if="[[!_canToggle]]">
<div>
[[_computeState(_stateObj)]]
</div>
</template>
`;
}
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,

View File

@@ -7,6 +7,19 @@ import callService from '../common/call-service.js';
class HuiCallServiceRow extends PolymerElement { class HuiCallServiceRow extends PolymerElement {
static get template() { static get template() {
return html`
${this.styleTemplate}
<ha-icon icon="[[_config.icon]]"></ha-icon>
<div class="flex">
<div>
[[_config.name]]
</div>
<paper-button on-click="_callService">[[_config.action_name]]</paper-button>
</div>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
:host { :host {
@@ -36,13 +49,6 @@ class HuiCallServiceRow extends PolymerElement {
margin-right: -.57em; margin-right: -.57em;
} }
</style> </style>
<ha-icon icon="[[_config.icon]]"></ha-icon>
<div class="flex">
<div>
[[_config.name]]
</div>
<paper-button on-click="_callService">[[_config.action_name]]</paper-button>
</div>
`; `;
} }

View File

@@ -5,6 +5,18 @@ import '../../../components/ha-icon.js';
class HuiWeblinkRow extends PolymerElement { class HuiWeblinkRow extends PolymerElement {
static get template() { static get template() {
return html`
${this.styleTemplate}
<a href="[[_config.url]]">
<ha-icon icon="[[_config.icon]]"></ha-icon>
<div>
[[_config.name]]
</div>
</a>
`;
}
static get styleTemplate() {
return html` return html`
<style> <style>
a { a {
@@ -24,12 +36,6 @@ class HuiWeblinkRow extends PolymerElement {
margin-left: 16px; margin-left: 16px;
} }
</style> </style>
<a href="[[_config.url]]">
<ha-icon icon="[[_config.icon]]"></ha-icon>
<div>
[[_config.name]]
</div>
</a>
`; `;
} }

View File

@@ -0,0 +1,158 @@
import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-dialog/paper-dialog.js';
import '@polymer/paper-spinner/paper-spinner.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../resources/ha-style.js';
import LocalizeMixin from '../../mixins/localize-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
class HaDialogShowAudioMessage extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style-dialog">
.error {
color: red;
}
@media all and (max-width: 500px) {
paper-dialog {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
position: fixed !important;
bottom: 0px;
left: 0px;
right: 0px;
overflow: scroll;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
}
paper-dialog {
border-radius: 2px;
}
paper-dialog p {
color: var(--secondary-text-color);
}
.icon {
float: right;
}
</style>
<paper-dialog id="mp3dialog" with-backdrop opened="{{_opened}}" on-opened-changed="_openedChanged">
<h2>
[[localize('ui.panel.mailbox.playback_title')]]
<div class='icon'>
<template is="dom-if" if="[[_loading]]">
<paper-spinner active></paper-spinner>
</template>
<template is="dom-if" if="[[!_loading]]">
<paper-icon-button
on-click='openDeleteDialog'
icon='hass:delete'
></paper-icon-button>
</template>
</div>
</h2>
<div id="transcribe"></div>
<div>
<template is="dom-if" if="[[_errorMsg]]">
<div class='error'>[[_errorMsg]]</div>
</template>
<audio id="mp3" preload="none" controls> <source id="mp3src" src="" type="audio/mpeg" /></audio>
</div>
</paper-dialog>
`;
}
static get properties() {
return {
hass: Object,
_currentMessage: Object,
// Error message when can't talk to server etc
_errorMsg: String,
_loading: {
type: Boolean,
value: false,
},
_opened: {
type: Boolean,
value: false,
},
};
}
showDialog({ hass, message }) {
this.hass = hass;
this._loading = true;
this._errorMsg = null;
this._currentMessage = message;
this._opened = true;
this.$.transcribe.innerText = message.message;
const platform = message.platform;
const mp3 = this.$.mp3;
mp3.src = null;
const url = `/api/mailbox/media/${platform}/${message.sha}`;
this.hass.fetchWithAuth(url)
.then((response) => {
if (response.ok) {
return response.blob();
}
return Promise.reject({
status: response.status,
statusText: response.statusText
});
})
.then((blob) => {
this._loading = false;
mp3.src = window.URL.createObjectURL(blob);
mp3.play();
})
.catch((err) => {
this._loading = false;
this._errorMsg = `Error loading audio: ${err.statusText}`;
});
}
openDeleteDialog() {
if (confirm(this.localize('ui.panel.mailbox.delete_prompt'))) {
this.deleteSelected();
}
}
deleteSelected() {
const msg = this._currentMessage;
this.hass.callApi('DELETE', `mailbox/delete/${msg.platform}/${msg.sha}`);
this._dialogDone();
}
_dialogDone() {
this.$.mp3.pause();
this.setProperties({
_currentMessage: null,
_errorMsg: null,
_loading: false,
_opened: false,
});
}
_openedChanged(ev) {
// Closed dialog by clicking on the overlay
// Check against dialogClosedCallback to make sure we didn't change
// programmatically
if (!ev.detail.value) {
this._dialogDone();
}
}
}
customElements.define('ha-dialog-show-audio-message', HaDialogShowAudioMessage);

View File

@@ -3,7 +3,6 @@ import '@polymer/app-layout/app-header/app-header.js';
import '@polymer/app-layout/app-toolbar/app-toolbar.js'; import '@polymer/app-layout/app-toolbar/app-toolbar.js';
import '@polymer/paper-button/paper-button.js'; import '@polymer/paper-button/paper-button.js';
import '@polymer/paper-card/paper-card.js'; import '@polymer/paper-card/paper-card.js';
import '@polymer/paper-dialog/paper-dialog.js';
import '@polymer/paper-input/paper-textarea.js'; import '@polymer/paper-input/paper-textarea.js';
import '@polymer/paper-item/paper-item-body.js'; import '@polymer/paper-item/paper-item-body.js';
import '@polymer/paper-item/paper-item.js'; import '@polymer/paper-item/paper-item.js';
@@ -17,6 +16,8 @@ import '../../resources/ha-style.js';
import formatDateTime from '../../common/datetime/format_date_time.js'; import formatDateTime from '../../common/datetime/format_date_time.js';
import LocalizeMixin from '../../mixins/localize-mixin.js'; import LocalizeMixin from '../../mixins/localize-mixin.js';
let registeredDialog = false;
/* /*
* @appliesMixin LocalizeMixin * @appliesMixin LocalizeMixin
*/ */
@@ -57,32 +58,8 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
paper-dialog {
border-radius: 2px;
}
paper-dialog p {
color: var(--secondary-text-color);
}
#mp3dialog paper-icon-button {
float: right;
}
@media all and (max-width: 450px) { @media all and (max-width: 450px) {
paper-dialog {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
position: fixed !important;
bottom: 0px;
left: 0px;
right: 0px;
overflow: scroll;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.content { .content {
width: auto; width: auto;
padding: 0; padding: 0;
@@ -128,28 +105,6 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
</paper-card> </paper-card>
</div> </div>
</app-header-layout> </app-header-layout>
<paper-dialog with-backdrop id="mp3dialog" on-iron-overlay-closed="_mp3Closed">
<h2>
[[localize('ui.panel.mailbox.playback_title')]]
<paper-icon-button
on-click='openDeleteDialog'
icon='hass:delete'
></paper-icon-button>
</h2>
<div id="transcribe"></div>
<div>
<audio id="mp3" preload="none" controls> <source id="mp3src" src="" type="audio/mpeg" /></audio>
</div>
</paper-dialog>
<paper-dialog with-backdrop id="confirmdel">
<p>[[localize('ui.panel.mailbox.delete_prompt')]]</p>
<div class="buttons">
<paper-button dialog-dismiss>[[localize('ui.common.cancel')]]</paper-button>
<paper-button dialog-confirm autofocus on-click="deleteSelected">[[localize('ui.panel.mailbox.delete_button')]]</paper-button>
</div>
</paper-dialog>
`; `;
} }
@@ -176,15 +131,19 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
_messages: { _messages: {
type: Array, type: Array,
}, },
currentMessage: {
type: Object,
},
}; };
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
this.fire('register-dialog', {
dialogShowEvent: 'show-audio-message-dialog',
dialogTag: 'ha-dialog-show-audio-message',
dialogImport: () => import('./ha-dialog-show-audio-message.js'),
});
}
this.hassChanged = this.hassChanged.bind(this); this.hassChanged = this.hassChanged.bind(this);
this.hass.connection.subscribeEvents(this.hassChanged, 'mailbox_updated') this.hass.connection.subscribeEvents(this.hassChanged, 'mailbox_updated')
.then(function (unsub) { this._unsubEvents = unsub; }.bind(this)); .then(function (unsub) { this._unsubEvents = unsub; }.bind(this));
@@ -209,35 +168,19 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
} }
openMP3Dialog(event) { openMP3Dialog(event) {
var platform = event.model.item.platform; this.fire('show-audio-message-dialog', {
this.currentMessage = event.model.item; hass: this.hass,
this.$.mp3dialog.open(); message: event.model.item,
this.$.mp3src.src = '/api/mailbox/media/' + platform + '/' + event.model.item.sha; });
this.$.transcribe.innerText = event.model.item.message;
this.$.mp3.load();
this.$.mp3.play();
} }
_mp3Closed() {
this.$.mp3.pause();
}
openDeleteDialog() {
this.$.confirmdel.open();
}
deleteSelected() {
var msg = this.currentMessage;
this.hass.callApi('DELETE', 'mailbox/delete/' + msg.platform + '/' + msg.sha);
this.$.mp3dialog.close();
}
getMessages() { getMessages() {
const items = this.platforms.map(function (platform) { const items = this.platforms.map(function (platform) {
return this.hass.callApi('GET', 'mailbox/messages/' + platform).then(function (values) { return this.hass.callApi('GET', `mailbox/messages/${platform}`).then(function (values) {
var platformItems = []; const platformItems = [];
var arrayLength = values.length; const arrayLength = values.length;
for (var i = 0; i < arrayLength; i++) { for (let i = 0; i < arrayLength; i++) {
var datetime = formatDateTime(new Date(values[i].info.origtime * 1000)); const datetime = formatDateTime(new Date(values[i].info.origtime * 1000));
platformItems.push({ platformItems.push({
timestamp: datetime, timestamp: datetime,
caller: values[i].info.callerid, caller: values[i].info.callerid,
@@ -251,15 +194,9 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
}); });
}.bind(this)); }.bind(this));
return Promise.all(items).then(function (platformItems) { return Promise.all(items).then(function (platformItems) {
var arrayLength = items.length; return [].concat(...platformItems).sort(function (a, b) {
var final = [];
for (var i = 0; i < arrayLength; i++) {
final = final.concat(platformItems[i]);
}
final.sort(function (a, b) {
return new Date(b.timestamp) - new Date(a.timestamp); return new Date(b.timestamp) - new Date(a.timestamp);
}); });
return final;
}); });
} }

View File

@@ -0,0 +1,127 @@
import '@polymer/paper-button/paper-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import EventsMixin from '../../mixins/events-mixin.js';
import LocalizeMixin from '../../mixins/localize-mixin.js';
import formatDateTime from '../../common/datetime/format_date_time.js';
import '../../resources/ha-style.js';
import './ha-settings-row.js';
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class HaLongLivedTokens extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style">
paper-card {
display: block;
}
.card-content {
margin: -1em 0;
}
a {
color: var(--primary-color);
}
paper-icon-button {
color: var(--primary-text-color);
}
</style>
<paper-card heading="[[localize('ui.panel.profile.long_lived_access_tokens.header')]]">
<div class="card-content">
<p>
[[localize('ui.panel.profile.long_lived_access_tokens.description')]]
<a href='https://developers.home-assistant.io/docs/en/auth_api.html#making-authenticated-requests' target='_blank'>
[[localize('ui.panel.profile.long_lived_access_tokens.learn_auth_requests')]]
</a>
</p>
<template is='dom-if' if='[[!_tokens.length]]'>
<p>[[localize('ui.panel.profile.long_lived_access_tokens.empty_state')]]</p>
</template>
</div>
<template is='dom-repeat' items='[[_tokens]]'>
<ha-settings-row>
<span slot='heading'>[[item.client_name]]</span>
<span slot='description'>[[_formatCreatedAt(item.created_at)]]</span>
<paper-icon-button icon="hass:delete" on-click='_handleDelete'></paper-icon-button>
</ha-settings-row>
</template>
<div class='card-actions'>
<paper-button on-click='_handleCreate'>
[[localize('ui.panel.profile.long_lived_access_tokens.create')]]
</paper-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
refreshTokens: Array,
_tokens: {
type: Array,
computed: '_computeTokens(refreshTokens)'
}
};
}
_computeTokens(refreshTokens) {
return refreshTokens.filter(tkn => tkn.type === 'long_lived_access_token').reverse();
}
_formatTitle(name) {
return this.localize(
'ui.panel.profile.long_lived_access_tokens.token_title',
'name', name
);
}
_formatCreatedAt(created) {
return this.localize(
'ui.panel.profile.long_lived_access_tokens.created_at',
'date', formatDateTime(new Date(created))
);
}
async _handleCreate() {
const name = prompt(this.localize('ui.panel.profile.long_lived_access_tokens.prompt_name'));
if (!name) return;
try {
const token = await this.hass.callWS({
type: 'auth/long_lived_access_token',
lifespan: 3650,
client_name: name,
});
prompt(this.localize('ui.panel.profile.long_lived_access_tokens.prompt_copy_token'), token);
this.fire('hass-refresh-tokens');
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert(this.localize('ui.panel.profile.long_lived_access_tokens.create_failed'));
}
}
async _handleDelete(ev) {
if (!confirm(this.localize('ui.panel.profile.long_lived_access_tokens.confirm_delete', 'name', ev.model.item.client_name))) {
return;
}
try {
await this.hass.callWS({
type: 'auth/delete_refresh_token',
refresh_token_id: ev.model.item.id,
});
this.fire('hass-refresh-tokens');
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert(this.localize('ui.panel.profile.long_lived_access_tokens.delete_failed'));
}
}
}
customElements.define('ha-long-lived-access-tokens-card', HaLongLivedTokens);

View File

@@ -15,6 +15,9 @@ import EventsMixin from '../../mixins/events-mixin.js';
import './ha-change-password-card.js'; import './ha-change-password-card.js';
import './ha-mfa-modules-card.js'; import './ha-mfa-modules-card.js';
import './ha-refresh-tokens-card.js';
import './ha-long-lived-access-tokens-card.js';
import './ha-pick-language-row.js'; import './ha-pick-language-row.js';
import './ha-pick-theme-row.js'; import './ha-pick-theme-row.js';
import './ha-push-notifications-row.js'; import './ha-push-notifications-row.js';
@@ -84,7 +87,22 @@ class HaPanelProfile extends EventsMixin(PolymerElement) {
<ha-change-password-card hass="[[hass]]"></ha-change-password-card> <ha-change-password-card hass="[[hass]]"></ha-change-password-card>
</template> </template>
<ha-mfa-modules-card hass='[[hass]]' mfa-modules='[[hass.user.mfa_modules]]'></ha-mfa-modules-card> <ha-mfa-modules-card
hass='[[hass]]'
mfa-modules='[[hass.user.mfa_modules]]'
></ha-mfa-modules-card>
<ha-refresh-tokens-card
hass='[[hass]]'
refresh-tokens='[[_refreshTokens]]'
on-hass-refresh-tokens='_refreshRefreshTokens'
></ha-refresh-tokens-card>
<ha-long-lived-access-tokens-card
hass='[[hass]]'
refresh-tokens='[[_refreshTokens]]'
on-hass-refresh-tokens='_refreshRefreshTokens'
></ha-long-lived-access-tokens-card>
</div> </div>
</app-header-layout> </app-header-layout>
`; `;
@@ -95,9 +113,21 @@ class HaPanelProfile extends EventsMixin(PolymerElement) {
hass: Object, hass: Object,
narrow: Boolean, narrow: Boolean,
showMenu: Boolean, showMenu: Boolean,
_refreshTokens: Array,
}; };
} }
connectedCallback() {
super.connectedCallback();
this._refreshRefreshTokens();
}
async _refreshRefreshTokens() {
this._refreshTokens = await this.hass.callWS({
type: 'auth/refresh_tokens'
});
}
_handleLogOut() { _handleLogOut() {
this.fire('hass-logout'); this.fire('hass-logout');
} }

View File

@@ -0,0 +1,82 @@
import '@polymer/paper-icon-button/paper-icon-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import EventsMixin from '../../mixins/events-mixin.js';
import LocalizeMixin from '../../mixins/localize-mixin.js';
import formatDateTime from '../../common/datetime/format_date_time.js';
import './ha-settings-row.js';
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class HaRefreshTokens extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
paper-card {
display: block;
}
paper-icon-button {
color: var(--primary-text-color);
}
</style>
<paper-card heading="[[localize('ui.panel.profile.refresh_tokens.header')]]">
<div class="card-content">[[localize('ui.panel.profile.refresh_tokens.description')]]</div>
<template is='dom-repeat' items='[[_computeTokens(refreshTokens)]]'>
<ha-settings-row>
<span slot='heading'>[[_formatTitle(item.client_id)]]</span>
<span slot='description'>[[_formatCreatedAt(item.created_at)]]</span>
<paper-icon-button icon="hass:delete" on-click='_handleDelete'></paper-icon-button>
</ha-settings-row>
</template>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
refreshTokens: Array,
};
}
_computeTokens(refreshTokens) {
return refreshTokens.filter(tkn => tkn.type === 'normal').reverse();
}
_formatTitle(clientId) {
return this.localize(
'ui.panel.profile.refresh_tokens.token_title',
'clientId', clientId
);
}
_formatCreatedAt(created) {
return this.localize(
'ui.panel.profile.refresh_tokens.created_at',
'date', formatDateTime(new Date(created))
);
}
async _handleDelete(ev) {
if (!confirm(this.localize('ui.panel.profile.refresh_tokens.confirm_delete', 'name', ev.model.item.client_id))) {
return;
}
try {
await this.hass.callWS({
type: 'auth/delete_refresh_token',
refresh_token_id: ev.model.item.id,
});
this.fire('hass-refresh-tokens');
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert(this.localize('ui.panel.profile.refresh_tokens.delete_failed'));
}
}
}
customElements.define('ha-refresh-tokens-card', HaRefreshTokens);

View File

@@ -746,6 +746,27 @@
"error_no_theme": "No themes available.", "error_no_theme": "No themes available.",
"link_promo": "Learn about themes", "link_promo": "Learn about themes",
"dropdown_label": "Theme" "dropdown_label": "Theme"
},
"refresh_tokens": {
"header": "Refresh Tokens",
"description": "Each refresh token represents a login session. Refresh tokens will be automatically removed when you click log out. Below a list of refresh tokens that are currently active for your account.",
"token_title": "Refresh token for {clientId}",
"created_at": "Created at {date}",
"confirm_delete": "Are you sure you want to delete the refresh token for {name}?",
"delete_failed": "Failed to delete the refresh token."
},
"long_lived_access_tokens": {
"header": "Long-Lived Access Tokens",
"description": "Create long-lived access tokens to allow your scripts to interact with your Home Assistant instance. Each token will be valid for 10 years from creation. The following long-lived access tokens are currently active.",
"learn_auth_requests": "Learn how to make authenticated requests.",
"created_at": "Created at {date}",
"confirm_delete": "Are you sure you want to delete the access token for {name}?",
"delete_failed": "Failed to delete the access token.",
"create": "Create Token",
"create_failed": "Failed to create the access token.",
"prompt_name": "Name?",
"prompt_copy_token": "Copy your access token. It will not be shown again.",
"empty_state": "You have no long-lived access tokens yet."
} }
}, },
"shopping-list": { "shopping-list": {

View File

@@ -0,0 +1,9 @@
export const fetchWithAuth = async (auth, input, init = {}) => {
if (auth.expired) await auth.refreshAccessToken();
init.credentials = 'same-origin';
if (!init.headers) {
init.headers = {};
}
init.headers.authorization = `Bearer ${auth.accessToken}`;
return await fetch(input, init);
};

View File

@@ -1,52 +1,57 @@
export default function hassCallApi(host, auth, method, path, parameters) { import { fetchWithAuth } from './fetch-with-auth.js';
var url = host + '/api/' + path;
return new Promise(function (resolve, reject) { /* eslint-disable no-throw-literal */
var req = new XMLHttpRequest();
req.open(method, url, true);
req.setRequestHeader('authorization', `Bearer ${auth.accessToken}`);
req.onload = function () { export default async function hassCallApi(auth, method, path, parameters) {
let body = req.responseText; const url = `${auth.data.hassUrl}/api/${path}`;
const contentType = req.getResponseHeader('content-type');
if (contentType && contentType.indexOf('application/json') !== -1) { const init = {
try { method: method,
body = JSON.parse(req.responseText); headers: {},
} catch (err) { };
reject({
error: 'Unable to parse JSON response',
status_code: req.status,
body: body,
});
return;
}
}
if (req.status > 199 && req.status < 300) { if (parameters) {
resolve(body); init.headers['Content-Type'] = 'application/json;charset=UTF-8';
} else { init.body = JSON.stringify(parameters);
reject({ }
error: 'Response error: ' + req.status,
status_code: req.status, let response;
body: body
}); try {
} response = await fetchWithAuth(auth, url, init);
} catch (err) {
throw {
error: 'Request error',
status_code: undefined,
body: undefined,
}; };
}
req.onerror = function () { let body = null;
reject({
error: 'Request error',
status_code: req.status,
body: req.responseText,
});
};
if (parameters) { const contentType = response.headers.get('content-type');
req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
req.send(JSON.stringify(parameters)); if (contentType && contentType.includes('application/json')) {
} else { try {
req.send(); body = await response.json();
} catch (err) {
throw {
error: 'Unable to parse JSON response',
status_code: err.status,
body: null,
};
} }
}); } else {
body = await response.text();
}
if (!response.ok) {
throw {
error: `Response error: ${response.status}`,
status_code: response.status,
body: body
};
}
return body;
} }

View File

@@ -562,7 +562,8 @@
} }
}, },
"error": { "error": {
"invalid_auth": "Невалидно потребителско име или парола" "invalid_auth": "Невалидно потребителско име или парола",
"invalid_code": "Невалиден код за аутентикация"
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inicializuji", "initializing": "Inicializuji",
"authorizing_client": "Chystáte se dát {clientId} práva pro Home Assistant instanci.", "authorizing_client": "Chystáte se dát {clientId} práva pro Home Assistant instanci.",
"logging_in_with": "Přihlásit se pomocí ** {authProviderName} **.", "logging_in_with": "Přihlásit se pomocí **{authProviderName}**.",
"pick_auth_provider": "Nebo se přihlaste s", "pick_auth_provider": "Nebo se přihlaste s",
"abort_intro": "Přihlášení bylo zrušeno", "abort_intro": "Přihlášení bylo zrušeno",
"form": { "form": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Initialiserer", "initializing": "Initialiserer",
"authorizing_client": "Du er ved at give {clientId} adgang til din Home Assistant-instans.", "authorizing_client": "Du er ved at give {clientId} adgang til din Home Assistant-instans.",
"logging_in_with": "Log ind med ** {authProviderName} **.", "logging_in_with": "Log ind med **{authProviderName}**.",
"pick_auth_provider": "Eller log ind med", "pick_auth_provider": "Eller log ind med",
"abort_intro": "Login afbrudt", "abort_intro": "Login afbrudt",
"form": { "form": {
@@ -569,7 +569,7 @@
"data": { "data": {
"code": "To-faktor godkendelseskode" "code": "To-faktor godkendelseskode"
}, },
"description": "Åbn ** {mfa_module_name} ** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:" "description": "Åbn **{mfa_module_name}** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:"
} }
}, },
"error": { "error": {
@@ -590,9 +590,9 @@
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "To faktor godkendelseskode" "code": "To-faktor godkendelseskode"
}, },
"description": "Åbn ** {mfa_module_name} ** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:" "description": "Åbn **{mfa_module_name}** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:"
} }
}, },
"error": { "error": {
@@ -601,7 +601,7 @@
}, },
"abort": { "abort": {
"no_api_password_set": "Du har ikke konfigureret en API-adgangskode.", "no_api_password_set": "Du har ikke konfigureret en API-adgangskode.",
"login_expired": "Sessionen er udløbet, log venligst ind igen" "login_expired": "Session er udløbet, log ind igen."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -545,6 +545,27 @@
"error_no_theme": "No themes available.", "error_no_theme": "No themes available.",
"link_promo": "Learn about themes", "link_promo": "Learn about themes",
"dropdown_label": "Theme" "dropdown_label": "Theme"
},
"refresh_tokens": {
"header": "Refresh Tokens",
"description": "Each refresh token represents a login session. Refresh tokens will be automatically removed when you click log out. Below a list of refresh tokens that are currently active for your account.",
"token_title": "Refresh token for {clientId}",
"created_at": "Created at {date}",
"confirm_delete": "Are you sure you want to delete the refresh token for {name}?",
"delete_failed": "Failed to delete the refresh token."
},
"long_lived_access_tokens": {
"header": "Long-Lived Access Tokens",
"description": "Create long-lived access tokens to allow your scripts to interact with your Home Assistant instance. Each token will be valid for 10 years from creation. The following long-lived access tokens are currently active.",
"learn_auth_requests": "Learn how to make authenticated requests.",
"created_at": "Created at {date}",
"confirm_delete": "Are you sure you want to delete the access token for {name}?",
"delete_failed": "Failed to delete the access token.",
"create": "Create Token",
"create_failed": "Failed to create the access token.",
"prompt_name": "Name?",
"prompt_copy_token": "Copy your access token. It will not be shown again.",
"empty_state": "You have no long-lived access tokens yet."
} }
}, },
"page-authorize": { "page-authorize": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inicializando", "initializing": "Inicializando",
"authorizing_client": "Está por dar acceso a {clientId} a su instancia de Home Assistant.", "authorizing_client": "Está por dar acceso a {clientId} a su instancia de Home Assistant.",
"logging_in_with": "Iniciando sesión con ** {authProviderName} **.", "logging_in_with": "Iniciando sesión con **{authProviderName}**.",
"pick_auth_provider": "O inicia sesión con", "pick_auth_provider": "O inicia sesión con",
"abort_intro": "Inicio de sesión cancelado", "abort_intro": "Inicio de sesión cancelado",
"form": { "form": {
@@ -590,18 +590,18 @@
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "Código de autenticado de dos factores" "code": "Código de autenticación de dos factores"
}, },
"description": "Abra el **{mfa_module_name}** en su dispositivo para ver su código de autenticar de dos factores y verificar su identidad:" "description": "Abra el **{mfa_module_name}** en su dispositivo para ver su código de autenticar de dos factores y verificar su identidad:"
} }
}, },
"error": { "error": {
"invalid_auth": "Contraseña API inválida", "invalid_auth": "Contraseña API inválida",
"invalid_code": "Código de autenticado invalido" "invalid_code": "Código de autenticación inválido"
}, },
"abort": { "abort": {
"no_api_password_set": "No tienes una contraseña API configurada.", "no_api_password_set": "No tienes una contraseña API configurada.",
"login_expired": "La sesión ha expirado, por favor inicie sesión nuevamente." "login_expired": "La sesión expiró, por favor inicie sesión nuevamente."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inicializando", "initializing": "Inicializando",
"authorizing_client": "Está por dar acceso a {clientId} a su instancia de Home Assistant.", "authorizing_client": "Está por dar acceso a {clientId} a su instancia de Home Assistant.",
"logging_in_with": "Iniciando sesión con ** {authProviderName} **.", "logging_in_with": "Iniciando sesión con **{authProviderName}**.",
"pick_auth_provider": "O inicia sesión con", "pick_auth_provider": "O inicia sesión con",
"abort_intro": "Inicio de sesión cancelado", "abort_intro": "Inicio de sesión cancelado",
"form": { "form": {
@@ -590,18 +590,18 @@
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "Código de autenticar de dos factores" "code": "Código de autenticado de dos factores"
}, },
"description": "Abra el **{mfa_module_name}** en su dispositivo para ver su código autenticado de dos factores y verificar su identidad:" "description": "Abra el **{mfa_module_name}** en su dispositivo para ver su código autenticado de dos factores y verificar su identidad:"
} }
}, },
"error": { "error": {
"invalid_auth": "Contraseña de API inválida", "invalid_auth": "Contraseña de API inválida",
"invalid_code": "Código de verificación invalido" "invalid_code": "Código de autenticación inválido"
}, },
"abort": { "abort": {
"no_api_password_set": "No tienes una contraseña de API configurada.", "no_api_password_set": "No tienes una contraseña de API configurada.",
"login_expired": "Sesión vencida, por favor iniciar sesión de nuevo." "login_expired": "La sesión expiró, por favor inicie sesión de nuevo."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -546,6 +546,95 @@
"link_promo": "Lisateave teemade kohta", "link_promo": "Lisateave teemade kohta",
"dropdown_label": "Teema" "dropdown_label": "Teema"
} }
},
"page-authorize": {
"initializing": "Lähtestan",
"authorizing_client": "Kavatsed anda {clientId} jaoks juurdepääsu oma Home Assistant serverile.",
"logging_in_with": "Login sisse **{authProviderName}** abil.",
"pick_auth_provider": "Või logi sisse, kasutades",
"abort_intro": "Sisselogimine katkestatud",
"form": {
"working": "Palun oota",
"unknown_error": "Midagi läks valesti",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Kasutajanimi",
"password": "Salasõna"
}
},
"mfa": {
"data": {
"code": "Kaheastmeline autentimiskood"
},
"description": "Ava oma seadmes **{mfa_module_name}**, et näha oma kahetasemelise autentimise koodi ja tõendada oma isikut:"
}
},
"error": {
"invalid_auth": "Vale kasutajanimi või salasõna",
"invalid_code": "Vigane autentimiskood"
},
"abort": {
"login_expired": "Sessioon aegus, palun logi uuesti sisse."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API salasõna"
},
"description": "Palun seadista http seadetes oma API salasõna:"
},
"mfa": {
"data": {
"code": "Kaheastmeline autentimiskood"
},
"description": "Ava oma seadmes **{mfa_module_name}**, et näha oma kahetasemelise autentimise koodi ja tõendada oma isikut:"
}
},
"error": {
"invalid_auth": "Vale API salasõna",
"invalid_code": "Vigane autentimiskood"
},
"abort": {
"no_api_password_set": "Sul pole API salasõna seadistatud",
"login_expired": "Sessioon aegus, palun logi uuesti sisse."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Kasutaja"
},
"description": "Palun vali kasutaja, kellena soovid sisse logida:"
}
},
"abort": {
"not_whitelisted": "Sinu arvuti ei ole lubatute nimekirjas."
}
}
}
}
},
"page-onboarding": {
"intro": "Kas oled valmis oma kodu ellu äratama, oma privaatsust tagasi võitma ja ühinema ülemaailmse nokitsejate kogukonnaga?",
"user": {
"intro": "Alustame kasutajakonto loomisega.",
"required_field": "Nõutud",
"data": {
"name": "Nimi",
"username": "Kasutajanimi",
"password": "Salasõna"
},
"create_account": "Loo konto",
"error": {
"required_fields": "Täida kõik nõutud väljad"
}
}
} }
}, },
"sidebar": { "sidebar": {
@@ -710,6 +799,11 @@
"ask": "Kas soovid selle sisselogimise salvestada?", "ask": "Kas soovid selle sisselogimise salvestada?",
"decline": "Tänan ei", "decline": "Tänan ei",
"confirm": "Salvesta sisselogimine" "confirm": "Salvesta sisselogimine"
},
"notification_drawer": {
"click_to_configure": "{entity} seadistamiseks klõpsa nuppu",
"empty": "Teavitusi pole",
"title": "Teavitused"
} }
}, },
"domain": { "domain": {

View File

@@ -569,7 +569,7 @@
"data": { "data": {
"code": "Code d'authentification à deux facteurs" "code": "Code d'authentification à deux facteurs"
}, },
"description": "Ouvrez le ** {mfa_module_name} ** sur votre appareil pour afficher votre code d'authentification à deux facteurs et vérifier votre identité:" "description": "Ouvrez le **{mfa_module_name}** sur votre appareil pour afficher votre code d'authentification à deux facteurs et vérifier votre identité:"
} }
}, },
"error": { "error": {
@@ -592,7 +592,7 @@
"data": { "data": {
"code": "Code d'authentification à deux facteurs" "code": "Code d'authentification à deux facteurs"
}, },
"description": "Ouvrez le ** {mfa_module_name} ** sur votre appareil pour afficher votre code d'authentification à deux facteurs et vérifier votre identité:" "description": "Ouvrez le **{mfa_module_name}** sur votre appareil pour afficher votre code d'authentification à deux facteurs et vérifier votre identité:"
} }
}, },
"error": { "error": {

View File

@@ -3,7 +3,7 @@
"config": "Konfigurasi", "config": "Konfigurasi",
"states": "Ikhtisar", "states": "Ikhtisar",
"map": "Peta", "map": "Peta",
"logbook": "Buku Catatan", "logbook": "Catatan Log",
"history": "Riwayat", "history": "Riwayat",
"mailbox": "Kotak pesan", "mailbox": "Kotak pesan",
"shopping_list": "Daftar belanja", "shopping_list": "Daftar belanja",
@@ -72,8 +72,8 @@
"on": "Tidak aman" "on": "Tidak aman"
}, },
"presence": { "presence": {
"off": "di luar", "off": "Keluar",
"on": "di rumah" "on": "Rumah"
}, },
"battery": { "battery": {
"off": "Normal", "off": "Normal",
@@ -164,7 +164,7 @@
"locked": "Terkunci", "locked": "Terkunci",
"unlocked": "Terbuka", "unlocked": "Terbuka",
"ok": "OK", "ok": "OK",
"problem": "Malasah" "problem": "Masalah"
}, },
"input_boolean": { "input_boolean": {
"off": "Off", "off": "Off",
@@ -183,6 +183,7 @@
"on": "On", "on": "On",
"playing": "Memainkan", "playing": "Memainkan",
"paused": "Jeda", "paused": "Jeda",
"idle": "Diam",
"standby": "Siaga" "standby": "Siaga"
}, },
"plant": { "plant": {
@@ -231,6 +232,13 @@
"snowy-rainy": "Bersalju, hujan", "snowy-rainy": "Bersalju, hujan",
"sunny": "Cerah", "sunny": "Cerah",
"windy": "Berangin" "windy": "Berangin"
},
"vacuum": {
"cleaning": "Membersihkan",
"error": "Kesalahan",
"idle": "Siaga",
"paused": "Berhenti",
"returning": "Kembali ke dock"
} }
}, },
"state_badge": { "state_badge": {
@@ -334,13 +342,15 @@
"to": "Ke" "to": "Ke"
}, },
"homeassistant": { "homeassistant": {
"label": "Home Assistant",
"event": "Event:", "event": "Event:",
"start": "Mulai", "start": "Mulai",
"shutdown": "Matikan" "shutdown": "Matikan"
}, },
"mqtt": { "mqtt": {
"label": "MQTT", "label": "MQTT",
"topic": "Topik" "topic": "Topik",
"payload": "Payload (opsional)"
}, },
"numeric_state": { "numeric_state": {
"label": "Status nomor", "label": "Status nomor",
@@ -460,6 +470,122 @@
"zwave": { "zwave": {
"caption": "Z-Wave", "caption": "Z-Wave",
"description": "Kelola jaringan Z-Wave anda" "description": "Kelola jaringan Z-Wave anda"
},
"users": {
"caption": "Pengguna",
"description": "Kelola pengguna",
"picker": {
"title": "Pengguna"
},
"editor": {
"rename_user": "Ubah nama pengguna",
"change_password": "Ganti kata sandi",
"activate_user": "Aktifkan pengguna",
"deactivate_user": "Nonaktifkan pengguna",
"delete_user": "Hapus pengguna"
}
}
},
"profile": {
"push_notifications": {
"header": "Pemberitahuan push",
"description": "Kirim pemberitahuan ke perangkat ini.",
"error_load_platform": "Konfigurasi notify.html5.",
"error_use_https": "Membutuhkan SSL diaktifkan untuk frontend.",
"push_notifications": "Pemberitahuan push",
"link_promo": "Pelajari lebih lanjut"
},
"language": {
"header": "Bahasa",
"link_promo": "Bantu menerjemahkan",
"dropdown_label": "Bahasa"
},
"themes": {
"header": "Tema",
"error_no_theme": "Tidak ada tema yang tersedia.",
"link_promo": "Pelajari tentang tema",
"dropdown_label": "Tema"
}
},
"page-authorize": {
"initializing": "Inisialisasi",
"authorizing_client": "Anda akan memberi akses {clientId} ke Home Assistant Anda.",
"logging_in_with": "Login dengan **{authProviderName}**.",
"pick_auth_provider": "Atau login dengan",
"abort_intro": "Login dibatalkan",
"form": {
"working": "Mohon tunggu",
"unknown_error": "Ada yang salah",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Nama pengguna",
"password": "Kata sandi"
}
},
"mfa": {
"data": {
"code": "Kode Autentikasi Dua Faktor"
},
"description": "Buka ** {mfa_module_name} ** pada perangkat Anda untuk melihat kode otentikasi dua-faktor Anda dan verifikasi identitas Anda:"
}
},
"error": {
"invalid_auth": "Username dan password salah",
"invalid_code": "Kode autentikasi tidak valid"
},
"abort": {
"login_expired": "Sesi kedaluwarsa, harap masuk lagi."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "Kata Sandi API"
},
"description": "Silakan masukkan kata sandi API di konfigurasi http Anda:"
}
},
"error": {
"invalid_auth": "Kata sandi API tidak valid"
},
"abort": {
"no_api_password_set": "Anda tidak memiliki kata sandi API yang dikonfigurasi."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Pengguna"
},
"description": "Silakan pilih pengguna yang ingin login sebagai:"
}
},
"abort": {
"not_whitelisted": "Komputer Anda tidak masuk daftar putih."
}
}
}
}
},
"page-onboarding": {
"intro": "Apakah Anda siap untuk membuat rumah Anda lebih hidup, merebut kembali privasi Anda dan bergabung dengan komunitas tinkerers dunia?",
"user": {
"intro": "Mari kita mulai dengan membuat akun pengguna.",
"required_field": "Wajib",
"data": {
"name": "Nama",
"username": "Username",
"password": "Kata sandi"
},
"create_account": "Buat Akun",
"error": {
"required_fields": "Isi semua bidang yang wajib diisi"
}
} }
} }
}, },
@@ -515,6 +641,51 @@
"w": "B" "w": "B"
}, },
"forecast": "Ramalan cuaca" "forecast": "Ramalan cuaca"
},
"alarm_control_panel": {
"code": "Kode",
"clear_code": "Hapus"
},
"automation": {
"last_triggered": "Terakhir terpicu",
"trigger": "Pemicu"
},
"cover": {
"position": "Posisi",
"tilt_position": "Posisi kemiringan"
},
"fan": {
"speed": "Kecepatan",
"direction": "Arah"
},
"light": {
"brightness": "Kecerahan",
"color_temperature": "Temperatur warna",
"white_value": "Nilai putih",
"effect": "Efek"
},
"climate": {
"on_off": "Hidup \/ mati",
"target_temperature": "Target suhu",
"target_humidity": "Target kelembaban",
"swing_mode": "Mode ayunan"
},
"lock": {
"lock": "Kunci",
"unlock": "Membuka"
},
"media_player": {
"source": "Sumber",
"sound_mode": "Mode suara"
},
"vacuum": {
"actions": {
"resume_cleaning": "Lanjutkan pembersihan",
"return_to_base": "Kembali ke dock",
"start_cleaning": "Mulai membersihkan",
"turn_on": "Nyalakan",
"turn_off": "Matikan"
}
} }
}, },
"components": { "components": {
@@ -527,6 +698,9 @@
"service": "Layanan" "service": "Layanan"
}, },
"relative_time": { "relative_time": {
"past": "lalu",
"future": "dalam",
"never": "Tak pernah",
"duration": { "duration": {
"second": "{count} {count, plural,\n one {detik}\n other {detik}\n}", "second": "{count} {count, plural,\n one {detik}\n other {detik}\n}",
"minute": "{count} {count, plural,\n one {menit}\n other {menit}\n}", "minute": "{count} {count, plural,\n one {menit}\n other {menit}\n}",
@@ -534,15 +708,35 @@
"day": "{count} {count, plural,\n one {hari}\n other {hari}\n}", "day": "{count} {count, plural,\n one {hari}\n other {hari}\n}",
"week": "{count} {count, plural,\n one {minggu}\n other {minggu}\n}" "week": "{count} {count, plural,\n one {minggu}\n other {minggu}\n}"
} }
},
"history_charts": {
"loading_history": "Memuat riwayat status ...",
"no_history_found": "Tidak ada riwayat status yang ditemukan."
} }
}, },
"notification_toast": { "notification_toast": {
"entity_turned_on": "{entity} sudah dinyalakan.",
"entity_turned_off": "{entity} sudah dimatikan.",
"service_called": "Layanan {service} dipanggil.",
"service_call_failed": "Gagal memanggil layanan {service} .",
"connection_lost": "Koneksi terputus. Menghubungan kembali ..." "connection_lost": "Koneksi terputus. Menghubungan kembali ..."
}, },
"dialogs": {
"more_info_settings": {
"save": "Simpan",
"name": "Nama",
"entity_id": "Entiti ID"
}
},
"auth_store": { "auth_store": {
"ask": "Apakah Anda ingin menyimpan login ini?", "ask": "Apakah Anda ingin menyimpan login ini?",
"decline": "Tidak, terima kasih", "decline": "Tidak, terima kasih",
"confirm": "Simpan login" "confirm": "Simpan login"
},
"notification_drawer": {
"click_to_configure": "Klik tombol untuk mengonfigurasi {entity}",
"empty": "Tidak ada pemberitahuan",
"title": "Pemberitahuan"
} }
}, },
"domain": { "domain": {
@@ -577,7 +771,8 @@
"sun": "Matahari", "sun": "Matahari",
"switch": "Sakelar", "switch": "Sakelar",
"updater": "Updater", "updater": "Updater",
"zwave": "Z-Wave" "zwave": "Z-Wave",
"vacuum": "Vakum"
}, },
"attribute": { "attribute": {
"weather": { "weather": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inizializzazione", "initializing": "Inizializzazione",
"authorizing_client": "Stai per dare accesso {clientId} alla tua istanza di Assistente Home.", "authorizing_client": "Stai per dare accesso {clientId} alla tua istanza di Assistente Home.",
"logging_in_with": "Accesso con ** {authProviderName} **.", "logging_in_with": "Accesso con **{authProviderName}**.",
"pick_auth_provider": "Oppure accedi con", "pick_auth_provider": "Oppure accedi con",
"abort_intro": "Login interrotto", "abort_intro": "Login interrotto",
"form": { "form": {
@@ -569,7 +569,7 @@
"data": { "data": {
"code": "Codice di autenticazione a due fattori" "code": "Codice di autenticazione a due fattori"
}, },
"description": "Apri ** {mfa_module_name} ** sul tuo dispositivo per visualizzare il codice di autenticazione a due fattori e verificare la tua identità:" "description": "Apri il **{mfa_module_name}** sul tuo dispositivo per visualizzare il codice di autenticazione a due fattori e verificare la tua identità:"
} }
}, },
"error": { "error": {
@@ -592,7 +592,7 @@
"data": { "data": {
"code": "Codice di autenticazione a due fattori" "code": "Codice di autenticazione a due fattori"
}, },
"description": "Apri il **{mfa_module_name}** sul tuo dispositivo per vedere il codice di autenticazione a due fattori (2FA) e verifica la tua identità:" "description": "Apri il **{mfa_module_name}** sul tuo dispositivo per visualizzare il codice di autenticazione a due fattori e verificare la tua identità:"
} }
}, },
"error": { "error": {
@@ -601,7 +601,7 @@
}, },
"abort": { "abort": {
"no_api_password_set": "Non hai una password API configurata.", "no_api_password_set": "Non hai una password API configurata.",
"login_expired": "Sessione scaduta, effettua nuovamente l'accesso." "login_expired": "Sessione scaduta, effettua nuovamente il login."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -569,7 +569,7 @@
"data": { "data": {
"code": "2 단계 인증 코드" "code": "2 단계 인증 코드"
}, },
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 ** {mfa_module_name} ** 을(를) 열어주세요:" "description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 **{mfa_module_name}** 을(를) 열어주세요:"
} }
}, },
"error": { "error": {
@@ -586,13 +586,13 @@
"data": { "data": {
"password": "API 비밀번호" "password": "API 비밀번호"
}, },
"description": "http config 에 API 암호를 입력해주세요:" "description": "configuration.yaml 에 설정한 api_password 를 입력해주세요:"
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "2 단계 인증 코드" "code": "2 단계 인증 코드"
}, },
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 ** {mfa_module_name} ** 을(를) 열어주세요:" "description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 **{mfa_module_name}** 을(를) 열어주세요:"
} }
}, },
"error": { "error": {
@@ -600,8 +600,8 @@
"invalid_code": "잘못된 인증 코드" "invalid_code": "잘못된 인증 코드"
}, },
"abort": { "abort": {
"no_api_password_set": "API 비밀번호를 구성하지 않았습니다.", "no_api_password_set": "API 비밀번호를 설정하지 않았습니다.",
"login_expired": "세션이 만료되었습니다. 다시 로그인 해주세요" "login_expired": "세션이 만료되었습니다. 다시 로그인 해주세요."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -64,9 +64,12 @@
"not_home": "Prom" "not_home": "Prom"
}, },
"media_player": { "media_player": {
"off": "Izslēgts",
"on": "Ieslēgts",
"playing": "Atskaņo", "playing": "Atskaņo",
"paused": "Apturēts", "paused": "Apturēts",
"idle": "Dīkstāvē" "idle": "Dīkstāvē",
"standby": "Gaidīšanas režīmā"
}, },
"plant": { "plant": {
"ok": "Labi", "ok": "Labi",

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Initialiserer", "initializing": "Initialiserer",
"authorizing_client": "Du er i ferd med å gi {clientId} tilgang til din Home Assistant", "authorizing_client": "Du er i ferd med å gi {clientId} tilgang til din Home Assistant",
"logging_in_with": "Logg inn med ** {authProviderName} **.", "logging_in_with": "Logg inn med **{authProviderName}**.",
"pick_auth_provider": "Eller logg inn med", "pick_auth_provider": "Eller logg inn med",
"abort_intro": "Innlogging avbrutt", "abort_intro": "Innlogging avbrutt",
"form": { "form": {
@@ -569,7 +569,7 @@
"data": { "data": {
"code": "To-faktor autentiseringskode" "code": "To-faktor autentiseringskode"
}, },
"description": "Åpne ** {mfa_module_name} ** på enheten din for å se din tofaktors autentiseringskode og bekrefte identiteten din:" "description": "Åpne **{mfa_module_name}** på enheten din for å se din tofaktors autentiseringskode og bekrefte identiteten din:"
} }
}, },
"error": { "error": {
@@ -592,7 +592,7 @@
"data": { "data": {
"code": "To-faktor autentiseringskode" "code": "To-faktor autentiseringskode"
}, },
"description": "Åpne **{mfa_module_name}** på din enhet for å vise to-faktor autentiseringkode og vertifiser din identitet." "description": "Åpne **{mfa_module_name}** på enheten din for å se din tofaktors autentiseringskode og bekrefte identiteten din:"
} }
}, },
"error": { "error": {
@@ -601,7 +601,7 @@
}, },
"abort": { "abort": {
"no_api_password_set": "Du har ikke konfigurert et API-passord.", "no_api_password_set": "Du har ikke konfigurert et API-passord.",
"login_expired": "Sesjonen har utløpt, vennligst logg inn igjen: " "login_expired": "Økten er utløpt, vennligst logg inn på nytt"
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -18,7 +18,7 @@
"state": { "state": {
"default": { "default": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony", "on": "Włączony",
"unknown": "nieznany", "unknown": "nieznany",
"unavailable": "niedostępny" "unavailable": "niedostępny"
}, },
@@ -41,7 +41,7 @@
"binary_sensor": { "binary_sensor": {
"default": { "default": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"moisture": { "moisture": {
"off": "sucho", "off": "sucho",
@@ -122,7 +122,7 @@
}, },
"calendar": { "calendar": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"camera": { "camera": {
"recording": "nagrywanie", "recording": "nagrywanie",
@@ -131,7 +131,7 @@
}, },
"climate": { "climate": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony", "on": "Włączony",
"heat": "ogrzewanie", "heat": "ogrzewanie",
"cool": "chłodzenie", "cool": "chłodzenie",
"idle": "nieaktywny", "idle": "nieaktywny",
@@ -162,7 +162,7 @@
}, },
"fan": { "fan": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"group": { "group": {
"off": "wyłączony", "off": "wyłączony",
@@ -181,7 +181,7 @@
}, },
"input_boolean": { "input_boolean": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"light": { "light": {
"off": "wyłączony", "off": "wyłączony",
@@ -193,7 +193,7 @@
}, },
"media_player": { "media_player": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony", "on": "Włączony",
"playing": "odtwarzanie", "playing": "odtwarzanie",
"paused": "pauza", "paused": "pauza",
"idle": "nieaktywny", "idle": "nieaktywny",
@@ -205,37 +205,37 @@
}, },
"remote": { "remote": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"scene": { "scene": {
"scening": "sceny" "scening": "sceny"
}, },
"script": { "script": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"sensor": { "sensor": {
"off": "wyłączony", "off": "wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"sun": { "sun": {
"above_horizon": "powyżej horyzontu", "above_horizon": "Powyżej horyzontu",
"below_horizon": "poniżej horyzontu" "below_horizon": "Poniżej horyzontu"
}, },
"switch": { "switch": {
"off": "wyłączony", "off": "Wyłączony",
"on": "włączony" "on": "Włączony"
}, },
"zwave": { "zwave": {
"default": { "default": {
"initializing": "inicjalizacja", "initializing": "Inicjalizacja",
"dead": "martwy", "dead": "Nieaktywny",
"sleeping": "uśpiony", "sleeping": "Uśpiony",
"ready": "gotowy" "ready": "Gotowy"
}, },
"query_stage": { "query_stage": {
"initializing": "inicjalizacja ({query_stage})", "initializing": "inicjalizacja ({query_stage})",
"dead": "martwy ({query_stage})" "dead": "Nieaktywny ({query_stage})"
} }
}, },
"weather": { "weather": {
@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inicjowanie", "initializing": "Inicjowanie",
"authorizing_client": "Czy na pewno chcesz dać dostęp {clientId} do Twojej instancji Home Assistant.", "authorizing_client": "Czy na pewno chcesz dać dostęp {clientId} do Twojej instancji Home Assistant.",
"logging_in_with": "Logowanie za pomocą ** {authProviderName} **.", "logging_in_with": "Logowanie za pomocą **{authProviderName}**.",
"pick_auth_provider": "Lub zaloguj się za pomocą", "pick_auth_provider": "Lub zaloguj się za pomocą",
"abort_intro": "Logowanie przerwane", "abort_intro": "Logowanie przerwane",
"form": { "form": {
@@ -569,7 +569,7 @@
"data": { "data": {
"code": "Dwuskładnikowy kod uwierzytelniający" "code": "Dwuskładnikowy kod uwierzytelniający"
}, },
"description": "Otwórz ** {mfa_module_name} ** na urządzeniu, aby wyświetlić dwuskładnikowy kod uwierzytelniający i zweryfikować swoją tożsamość:" "description": "Otwórz **{mfa_module_name}** na urządzeniu, aby wyświetlić dwuskładnikowy kod uwierzytelniający i zweryfikować swoją tożsamość:"
} }
}, },
"error": { "error": {
@@ -592,7 +592,7 @@
"data": { "data": {
"code": "Dwuskładnikowy kod uwierzytelniający" "code": "Dwuskładnikowy kod uwierzytelniający"
}, },
"description": "Otwórz ** {mfa_module_name} ** na urządzeniu, aby wyświetlić dwuskładnikowy kod uwierzytelniający i zweryfikować swoją tożsamość:" "description": "Otwórz **{mfa_module_name}** na urządzeniu, aby wyświetlić dwuskładnikowy kod uwierzytelniający i zweryfikować swoją tożsamość:"
} }
}, },
"error": { "error": {
@@ -610,7 +610,7 @@
"data": { "data": {
"user": "Użytkownik" "user": "Użytkownik"
}, },
"description": "Proszę wybrać użytkownika, na którego chcesz się zalogować jako:" "description": "Proszę wybrać użytkownika, na którego chcesz się zalogować:"
} }
}, },
"abort": { "abort": {
@@ -621,7 +621,7 @@
} }
}, },
"page-onboarding": { "page-onboarding": {
"intro": "Czy jesteś gotowy, aby obudzić swój dom, odzyskać prywatność i dołączyć do światowej społeczności majsterkowiczów?", "intro": "Czy jesteś gotowy, aby ożywić swój dom, odzyskać prywatność i dołączyć do światowej społeczności majsterkowiczów?",
"user": { "user": {
"intro": "Zacznijmy od utworzenia konta użytkownika.", "intro": "Zacznijmy od utworzenia konta użytkownika.",
"required_field": "Wymagane", "required_field": "Wymagane",

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Iniciando", "initializing": "Iniciando",
"authorizing_client": "Você está prestes a dar acesso pra {clientId} à sua instância do Home Assistant.", "authorizing_client": "Você está prestes a dar acesso pra {clientId} à sua instância do Home Assistant.",
"logging_in_with": "Fazendo login com ** {authProviderName} **.", "logging_in_with": "Fazendo login com **{authProviderName}**.",
"pick_auth_provider": "Ou entre com", "pick_auth_provider": "Ou entre com",
"abort_intro": "Login cancelado", "abort_intro": "Login cancelado",
"form": { "form": {

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "A inicializar", "initializing": "A inicializar",
"authorizing_client": "Você está prestes a dar acesso a {clientId} à sua instância do Home Assistant.", "authorizing_client": "Você está prestes a dar acesso a {clientId} à sua instância do Home Assistant.",
"logging_in_with": "Fazendo login com ** {authProviderName} **.", "logging_in_with": "Fazendo login com **{authProviderName}**.",
"pick_auth_provider": "Ou faça login com", "pick_auth_provider": "Ou faça login com",
"abort_intro": "Login abortado", "abort_intro": "Login abortado",
"form": { "form": {

View File

@@ -526,11 +526,53 @@
} }
} }
}, },
"profile": {
"push_notifications": {
"header": "Notificări",
"description": "Trimiteți notificări către acest dispozitiv.",
"error_load_platform": "Configurați notify.html5.",
"error_use_https": "Necesită SSL activat pentru interfaţă.",
"push_notifications": "Notificări",
"link_promo": "Aflați mai multe"
},
"language": {
"header": "Limba",
"link_promo": "Ajută la traducere",
"dropdown_label": "Limba"
},
"themes": {
"header": "Temă",
"error_no_theme": "Nu există teme disponibile.",
"link_promo": "Aflați mai multe despre teme",
"dropdown_label": "Temă"
}
},
"page-authorize": { "page-authorize": {
"initializing": "Inițializează",
"logging_in_with": "Conectare cu **{authProviderName}**.",
"pick_auth_provider": "Sau conectați-vă cu",
"abort_intro": "Conectare intrerupta",
"form": { "form": {
"working": "Te rog așteaptă",
"unknown_error": "Ceva n-a mers bine",
"providers": { "providers": {
"homeassistant": { "homeassistant": {
"step": {
"init": {
"data": {
"username": "Nume de utilizator",
"password": "Parola"
}
},
"mfa": {
"data": {
"code": "Autentificare cu doi factori"
},
"description": "Deschideti **{mfa_module_name}** pe dispozitivul d-voastra pentru a vedea codul de 'two-factor authentication' si a va verifica indentitatea:"
}
},
"error": { "error": {
"invalid_auth": "nume de utilizator sau parola incorecte",
"invalid_code": "Codul 'Two-factor Authentication' invalid" "invalid_code": "Codul 'Two-factor Authentication' invalid"
}, },
"abort": { "abort": {
@@ -540,31 +582,57 @@
"legacy_api_password": { "legacy_api_password": {
"step": { "step": {
"init": { "init": {
"data": {
"password": "Parola API"
},
"description": "Introduceti parola API in http config:" "description": "Introduceti parola API in http config:"
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "Codul 'Two-factor Authentication'" "code": "Autentificare cu doi factori"
}, },
"description": "Deschideti **{mfa_module_name}** pe dispozitivul d-voastra pentru a vedea codul de 'two-factor authentication' si a va verifica indentitatea:" "description": "Deschideti **{mfa_module_name}** pe dispozitivul d-voastra pentru a vedea codul de 'two-factor authentication' si a va verifica indentitatea:"
} }
}, },
"error": { "error": {
"invalid_code": "Cod de autentificare invalid" "invalid_auth": "Parola API nevalidă",
"invalid_code": "Codul 'Two-factor Authentication' invalid"
}, },
"abort": { "abort": {
"login_expired": "Sesiune expirata, e nevoie sa va logati din nou." "no_api_password_set": "Nu aveți o parolă API configurată.",
"login_expired": "Sesiunea a expirat, va rugam logati-va din nou."
} }
}, },
"trusted_networks": { "trusted_networks": {
"step": { "step": {
"init": { "init": {
"data": {
"user": "Utilizator"
},
"description": "Selectati utilizatorul cu care doriti sa va logati:" "description": "Selectati utilizatorul cu care doriti sa va logati:"
} }
},
"abort": {
"not_whitelisted": "Calculatorul dvs. nu este pe lista albă."
} }
} }
} }
} }
},
"page-onboarding": {
"user": {
"intro": "Să începem prin crearea unui cont de utilizator.",
"required_field": "Necesar",
"data": {
"name": "Nume",
"username": "Nume de utilizator",
"password": "Parola"
},
"create_account": "Creează cont",
"error": {
"required_fields": "Completați toate câmpurile obligatorii"
}
}
} }
}, },
"sidebar": { "sidebar": {
@@ -724,6 +792,16 @@
"name": "Nume", "name": "Nume",
"entity_id": "ID-ul entității" "entity_id": "ID-ul entității"
} }
},
"auth_store": {
"ask": "Doriți să salvați aceste date de conectare?",
"decline": "Nu, mulţumesc",
"confirm": "Salvați datele de conectare"
},
"notification_drawer": {
"click_to_configure": "Faceți clic pe buton pentru a configura {entity}",
"empty": "Nicio notificare",
"title": "Notificări"
} }
}, },
"domain": { "domain": {

View File

@@ -319,7 +319,7 @@
"introduction": "Изменение конфигурации может быть утомительным процессом. Мы знаем. Этот раздел попытается сделать вашу жизнь немного легче.", "introduction": "Изменение конфигурации может быть утомительным процессом. Мы знаем. Этот раздел попытается сделать вашу жизнь немного легче.",
"validation": { "validation": {
"heading": "Проверка конфигурации", "heading": "Проверка конфигурации",
"introduction": "Проверьте свою конфигурацию, если вы внесли в нее некоторые изменения и хотите убедиться, что она действительна", "introduction": "Проверьте свою конфигурацию, если вы внесли в нее некоторые изменения и хотите убедиться в ее работоспособности",
"check_config": "Проверить конфигурацию", "check_config": "Проверить конфигурацию",
"valid": "Конфигурация выполнена верно!", "valid": "Конфигурация выполнена верно!",
"invalid": "Ошибка в конфигурации" "invalid": "Ошибка в конфигурации"
@@ -504,7 +504,7 @@
} }
}, },
"script": { "script": {
"caption": "Скрипт", "caption": "Скрипты",
"description": "Создавайте и редактируйте скрипты" "description": "Создавайте и редактируйте скрипты"
}, },
"zwave": { "zwave": {
@@ -782,8 +782,8 @@
} }
}, },
"notification_toast": { "notification_toast": {
"entity_turned_on": "{entity} включение.", "entity_turned_on": "{entity} включается",
"entity_turned_off": "{entity} выключение.", "entity_turned_off": "{entity} выключается",
"service_called": "Вызов службы {service}.", "service_called": "Вызов службы {service}.",
"service_call_failed": "Не удалось вызвать службу {service}.", "service_call_failed": "Не удалось вызвать службу {service}.",
"connection_lost": "Соединение потеряно. Переподключение ..." "connection_lost": "Соединение потеряно. Переподключение ..."

View File

@@ -550,7 +550,7 @@
"page-authorize": { "page-authorize": {
"initializing": "Inicializacija", "initializing": "Inicializacija",
"authorizing_client": "Dali boste {clientId} dostop do vašega Home assistenta.", "authorizing_client": "Dali boste {clientId} dostop do vašega Home assistenta.",
"logging_in_with": "Prijava z ** {authProviderName} **.", "logging_in_with": "Prijava z **{authProviderName}**.",
"pick_auth_provider": "Ali se prijavite z", "pick_auth_provider": "Ali se prijavite z",
"abort_intro": "Prijava prekinjena", "abort_intro": "Prijava prekinjena",
"form": { "form": {
@@ -569,7 +569,7 @@
"data": { "data": {
"code": "Dvofaktorska koda za avtorizacijo" "code": "Dvofaktorska koda za avtorizacijo"
}, },
"description": "V svoji napravi odprite ** {mfa_module_name} **, da si ogledate svojo dvofaktorsko kodo za preverjanje pristnosti in preverite svojo identiteto:" "description": "V svoji napravi odprite **{mfa_module_name}**, da si ogledate svojo dvofaktorsko kodo za preverjanje pristnosti in preverite svojo identiteto:"
} }
}, },
"error": { "error": {
@@ -590,9 +590,9 @@
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "Dvofaktorska avtorizacijska koda" "code": "Dvofaktorska koda za avtorizacijo"
}, },
"description": "Odprite **{mfa_module_name}** na vaši napravi za ogled dvofaktorske avtorizacijske kode in potrdite vašo identiteto:" "description": "V svoji napravi odprite **{mfa_module_name}**, da si ogledate svojo dvofaktorsko kodo za preverjanje pristnosti in preverite svojo identiteto:"
} }
}, },
"error": { "error": {
@@ -601,7 +601,7 @@
}, },
"abort": { "abort": {
"no_api_password_set": "Nimate nastavljenega gesla API-ja.", "no_api_password_set": "Nimate nastavljenega gesla API-ja.",
"login_expired": "Seja je potekla, prosim ponovno se prijavite." "login_expired": "Seja je potekla, prosimo, prijavite se znova."
} }
}, },
"trusted_networks": { "trusted_networks": {

View File

@@ -549,10 +549,10 @@
}, },
"page-authorize": { "page-authorize": {
"initializing": "Initierar", "initializing": "Initierar",
"authorizing_client": "Du håller på att ge {clientid} behörighet till din Home Assistant.", "authorizing_client": "Du håller på att ge {clientId} behörighet till din Home Assistant.",
"logging_in_with": "Loggar in med **{authProviderName}**.", "logging_in_with": "Loggar in med **{authProviderName}**.",
"pick_auth_provider": "Eller logga in med", "pick_auth_provider": "Eller logga in med",
"abort_intro": "Login avbruten", "abort_intro": "Inloggning avbruten",
"form": { "form": {
"working": "Vänligen vänta", "working": "Vänligen vänta",
"unknown_error": "Något gick fel", "unknown_error": "Något gick fel",
@@ -568,7 +568,8 @@
"mfa": { "mfa": {
"data": { "data": {
"code": "Tvåfaktorsautentiseringskod" "code": "Tvåfaktorsautentiseringskod"
} },
"description": "Öppna **{mfa_module_name}** på din enhet för att visa din tvåfaktors autentiseringskod och verifiera din identitet:"
} }
}, },
"error": { "error": {
@@ -589,18 +590,18 @@
}, },
"mfa": { "mfa": {
"data": { "data": {
"code": "Tvåfaktorsautentiseringskod:" "code": "Tvåfaktorsautentiseringskod"
}, },
"description": "Öppna **(mfa_module_name)** på din enhet för att se din tvåfaktorsautentiseringskod och verifiera din identitet:" "description": "Öppna **{mfa_module_name}** på din enhet för att visa din tvåfaktors autentiseringskod och verifiera din identitet:"
} }
}, },
"error": { "error": {
"invalid_auth": "Ogiltigt API-lösenord", "invalid_auth": "Ogiltigt API-lösenord",
"invalid_code": "Felaktig autentiserings kod" "invalid_code": "Ogiltig autentiseringskod"
}, },
"abort": { "abort": {
"no_api_password_set": "Du har inget API lösenord konfigurerat", "no_api_password_set": "Du har inget API lösenord konfigurerat",
"login_expired": "Sessionen avslutades, logga in igen." "login_expired": "Sessionen avslutades. Logga in igen."
} }
}, },
"trusted_networks": { "trusted_networks": {
@@ -620,6 +621,7 @@
} }
}, },
"page-onboarding": { "page-onboarding": {
"intro": "Är du redo att väcka ditt hem, återta din integritet och gå med i en världsomspännande gemenskap av hemmafixare?",
"user": { "user": {
"intro": "Låt oss börja med att skapa ett användarkonto", "intro": "Låt oss börja med att skapa ett användarkonto",
"required_field": "Krävs", "required_field": "Krävs",
@@ -628,7 +630,10 @@
"username": "Användarnamn", "username": "Användarnamn",
"password": "Lösenord" "password": "Lösenord"
}, },
"create_account": "Skapa konto" "create_account": "Skapa konto",
"error": {
"required_fields": "Fyll i alla fält som krävs"
}
} }
} }
}, },
@@ -797,7 +802,7 @@
}, },
"notification_drawer": { "notification_drawer": {
"click_to_configure": "Klicka på knappen för att konfigurera {entity}", "click_to_configure": "Klicka på knappen för att konfigurera {entity}",
"empty": "Ingra notiser", "empty": "Inga notiser",
"title": "Notiser" "title": "Notiser"
} }
}, },

View File

@@ -24,7 +24,7 @@
}, },
"alarm_control_panel": { "alarm_control_panel": {
"armed": "Vũ trang", "armed": "Vũ trang",
"disarmed": "Giải giáp", "disarmed": "Vô hiệu hóa",
"armed_home": "Vũ trang khi ở nhà", "armed_home": "Vũ trang khi ở nhà",
"armed_away": "Vũ trang khi đi vắng", "armed_away": "Vũ trang khi đi vắng",
"armed_night": "Vũ trang đêm", "armed_night": "Vũ trang đêm",
@@ -248,7 +248,7 @@
}, },
"alarm_control_panel": { "alarm_control_panel": {
"armed": "VT", "armed": "VT",
"disarmed": "Giải giáp", "disarmed": "Vô hiệu hoá",
"pending": "Chờ", "pending": "Chờ",
"arming": "Đang VT", "arming": "Đang VT",
"disarming": "Giải giáp" "disarming": "Giải giáp"
@@ -459,6 +459,19 @@
"delete_user": "Xóa người dùng" "delete_user": "Xóa người dùng"
} }
} }
},
"page-authorize": {
"form": {
"providers": {
"trusted_networks": {
"step": {
"init": {
"description": "Vui lòng lựa chọn một tài khoản để đăng nhập"
}
}
}
}
}
} }
}, },
"sidebar": { "sidebar": {
@@ -526,8 +539,8 @@
"code": "Mã số", "code": "Mã số",
"clear_code": "Xóa", "clear_code": "Xóa",
"disarm": "Không vũ trang", "disarm": "Không vũ trang",
"arm_home": "VT Ở nhà", "arm_home": "An ninh khi ở nhà",
"arm_away": "VT Đi vắng" "arm_away": "An ninh khi đi vắng"
}, },
"automation": { "automation": {
"last_triggered": "Kích hoạt lần cuối", "last_triggered": "Kích hoạt lần cuối",
@@ -611,7 +624,7 @@
} }
}, },
"domain": { "domain": {
"alarm_control_panel": "Bảng điều khiển báo động", "alarm_control_panel": "Bảng điều khiển an ninh",
"automation": "Tự động hóa", "automation": "Tự động hóa",
"binary_sensor": "Cảm biến nhị phân", "binary_sensor": "Cảm biến nhị phân",
"calendar": "Lịch", "calendar": "Lịch",

View File

@@ -564,10 +564,20 @@
"username": "使用者名稱", "username": "使用者名稱",
"password": "使用者密碼" "password": "使用者密碼"
} }
},
"mfa": {
"data": {
"code": "兩步驟驗證碼"
},
"description": "開啟裝置上的 **{mfa_module_name}** 以獲得兩步驟驗證碼,並進行驗證:"
} }
}, },
"error": { "error": {
"invalid_auth": "使用者名稱或密碼無效" "invalid_auth": "使用者名稱或密碼無效",
"invalid_code": "驗證碼無效"
},
"abort": {
"login_expired": "登入階段已逾時,請重新登錄。"
} }
}, },
"legacy_api_password": { "legacy_api_password": {
@@ -575,14 +585,23 @@
"init": { "init": {
"data": { "data": {
"password": "API 密碼" "password": "API 密碼"
} },
"description": "請輸入 Http 設定中的 API 密鑰:"
},
"mfa": {
"data": {
"code": "兩步驟驗證碼"
},
"description": "開啟裝置上的 **{mfa_module_name}** 以獲得兩步驟驗證碼,並進行驗證:"
} }
}, },
"error": { "error": {
"invalid_auth": "API 密碼無效" "invalid_auth": "API 密碼無效",
"invalid_code": "驗證碼無效"
}, },
"abort": { "abort": {
"no_api_password_set": "尚未設定 API 密碼。" "no_api_password_set": "尚未設定 API 密碼。",
"login_expired": "登入階段已逾時,請重新登錄。"
} }
}, },
"trusted_networks": { "trusted_networks": {
@@ -590,7 +609,8 @@
"init": { "init": {
"data": { "data": {
"user": "使用者" "user": "使用者"
} },
"description": "請選擇所要登錄的用戶:"
} }
}, },
"abort": { "abort": {

View File

@@ -6,6 +6,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin'); const WorkboxPlugin = require('workbox-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin"); const CompressionPlugin = require("compression-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const zopfli = require('@gfx/zopfli');
const translationMetadata = require('./build-translations/translationMetadata.json'); const translationMetadata = require('./build-translations/translationMetadata.json');
const version = fs.readFileSync('setup.py', 'utf8').match(/\d{8}[^']*/); const version = fs.readFileSync('setup.py', 'utf8').match(/\d{8}[^']*/);
@@ -138,7 +139,10 @@ function createConfig(isProdBuild, latestBuild) {
/\.LICENSE$/, /\.LICENSE$/,
/\.py$/, /\.py$/,
/\.txt$/, /\.txt$/,
] ],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}), }),
new WorkboxPlugin.InjectManifest({ new WorkboxPlugin.InjectManifest({
swSrc: './src/entrypoints/service-worker-bootstrap.js', swSrc: './src/entrypoints/service-worker-bootstrap.js',
@@ -151,32 +155,13 @@ function createConfig(isProdBuild, latestBuild) {
/hass-icons.js$/, /hass-icons.js$/,
/\.chunk\.js$/, /\.chunk\.js$/,
], ],
// Static assets get cached during runtime. But these we want to explicitly cache
// Need to be done using templatedUrls because prefix is /static
globDirectory: '.',
globIgnores: [],
modifyUrlPrefix: {
'hass_frontend': '/static'
},
templatedUrls: { templatedUrls: {
[`/static/translations/${translationMetadata['translations']['en']['fingerprints']['en']}`]: [ [`/static/translations/${translationMetadata['translations']['en']['fingerprints']['en']}`]: 'build-translations/output/en.json',
'build-translations/output/en.json' '/static/icons/favicon-192x192.png': 'public/icons/favicon-192x192.png',
], '/static/fonts/roboto/Roboto-Light.ttf': 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf',
'/static/icons/favicon-192x192.png': [ '/static/fonts/roboto/Roboto-Medium.ttf': 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf',
'public/icons/favicon-192x192.png' '/static/fonts/roboto/Roboto-Regular.ttf': 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf',
], '/static/fonts/roboto/Roboto-Bold.ttf': 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf',
'/static/fonts/roboto/Roboto-Light.ttf': [
'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf'
],
'/static/fonts/roboto/Roboto-Medium.ttf': [
'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf'
],
'/static/fonts/roboto/Roboto-Regular.ttf': [
'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf'
],
'/static/fonts/roboto/Roboto-Bold.ttf': [
'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf'
],
} }
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({

135
yarn.lock
View File

@@ -577,6 +577,12 @@
lodash "^4.17.5" lodash "^4.17.5"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@gfx/zopfli@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@gfx/zopfli/-/zopfli-1.0.8.tgz#4bad09d5c8bd8156e018716228e9d84e2347b3f0"
dependencies:
base64-js "^1.0.0"
"@mdi/svg@^2.4.85": "@mdi/svg@^2.4.85":
version "2.7.94" version "2.7.94"
resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-2.7.94.tgz#5f2b03c363b10f7e4cf8c8a5fdc81cbd838bf44b" resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-2.7.94.tgz#5f2b03c363b10f7e4cf8c8a5fdc81cbd838bf44b"
@@ -1953,6 +1959,10 @@ agent-base@2:
extend "~3.0.0" extend "~3.0.0"
semver "~5.0.1" semver "~5.0.1"
ajv-errors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59"
ajv-keywords@^2.1.0: ajv-keywords@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
@@ -3135,7 +3145,7 @@ base64-js@1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
base64-js@^1.0.2: base64-js@^1.0.0, base64-js@^1.0.2:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
@@ -3492,7 +3502,7 @@ bytes@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
cacache@^10.0.1, cacache@^10.0.4: cacache@^10.0.4:
version "10.0.4" version "10.0.4"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
dependencies: dependencies:
@@ -3510,6 +3520,25 @@ cacache@^10.0.1, cacache@^10.0.4:
unique-filename "^1.1.0" unique-filename "^1.1.0"
y18n "^4.0.0" y18n "^4.0.0"
cacache@^11.2.0:
version "11.2.0"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965"
dependencies:
bluebird "^3.5.1"
chownr "^1.0.1"
figgy-pudding "^3.1.0"
glob "^7.1.2"
graceful-fs "^4.1.11"
lru-cache "^4.1.3"
mississippi "^3.0.0"
mkdirp "^0.5.1"
move-concurrently "^1.0.1"
promise-inflight "^1.0.1"
rimraf "^2.6.2"
ssri "^6.0.0"
unique-filename "^1.1.0"
y18n "^4.0.0"
cache-base@^1.0.1: cache-base@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -4004,13 +4033,14 @@ compressible@~2.0.14:
dependencies: dependencies:
mime-db ">= 1.34.0 < 2" mime-db ">= 1.34.0 < 2"
compression-webpack-plugin@^1.1.11: compression-webpack-plugin@^2.0.0:
version "1.1.11" version "2.0.0"
resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.11.tgz#8384c7a6ead1d2e2efb190bdfcdcf35878ed8266" resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc"
dependencies: dependencies:
cacache "^10.0.1" cacache "^11.2.0"
find-cache-dir "^1.0.0" find-cache-dir "^2.0.0"
neo-async "^2.5.0" neo-async "^2.5.0"
schema-utils "^1.0.0"
serialize-javascript "^1.4.0" serialize-javascript "^1.4.0"
webpack-sources "^1.0.1" webpack-sources "^1.0.1"
@@ -5449,6 +5479,10 @@ fecha@^2.3.3, "fecha@https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ec
version "2.3.3" version "2.3.3"
resolved "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz#bfb1e49121dd7821601af35faf4fe93dbd19200a" resolved "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz#bfb1e49121dd7821601af35faf4fe93dbd19200a"
figgy-pudding@^3.1.0, figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
figures@^1.3.5: figures@^1.3.5:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@@ -5528,6 +5562,14 @@ find-cache-dir@^1.0.0:
make-dir "^1.0.0" make-dir "^1.0.0"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
find-cache-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
dependencies:
commondir "^1.0.1"
make-dir "^1.0.0"
pkg-dir "^3.0.0"
find-index@^0.1.1: find-index@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4"
@@ -5569,6 +5611,12 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies: dependencies:
locate-path "^2.0.0" locate-path "^2.0.0"
find-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
dependencies:
locate-path "^3.0.0"
findup-sync@^0.4.2: findup-sync@^0.4.2:
version "0.4.3" version "0.4.3"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12"
@@ -6513,9 +6561,9 @@ hoek@4.x.x:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
home-assistant-js-websocket@^3.0.0: home-assistant-js-websocket@^3.1.2:
version "3.0.0" version "3.1.2"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.0.0.tgz#498828a29827bdd1f3e99cf3b5e152694cededbf" resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.1.2.tgz#c82e79eb644eac0206cd9c07d10c83c7d052c2b7"
home-or-tmp@^2.0.0: home-or-tmp@^2.0.0:
version "2.0.0" version "2.0.0"
@@ -7540,6 +7588,13 @@ locate-path@^2.0.0:
p-locate "^2.0.0" p-locate "^2.0.0"
path-exists "^3.0.0" path-exists "^3.0.0"
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
dependencies:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash._baseassign@^3.0.0: lodash._baseassign@^3.0.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
@@ -7972,7 +8027,7 @@ lru-cache@2:
version "2.7.3" version "2.7.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
lru-cache@^4.0.1, lru-cache@^4.0.2, lru-cache@^4.1.1: lru-cache@^4.0.1, lru-cache@^4.0.2, lru-cache@^4.1.1, lru-cache@^4.1.3:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies: dependencies:
@@ -8292,6 +8347,21 @@ mississippi@^2.0.0:
stream-each "^1.1.0" stream-each "^1.1.0"
through2 "^2.0.0" through2 "^2.0.0"
mississippi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
dependencies:
concat-stream "^1.5.0"
duplexify "^3.4.2"
end-of-stream "^1.1.0"
flush-write-stream "^1.0.0"
from2 "^2.1.0"
parallel-transform "^1.1.0"
pump "^3.0.0"
pumpify "^1.3.3"
stream-each "^1.1.0"
through2 "^2.0.0"
mixin-deep@^1.2.0: mixin-deep@^1.2.0:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
@@ -8878,12 +8948,24 @@ p-limit@^1.0.0, p-limit@^1.1.0:
dependencies: dependencies:
p-try "^1.0.0" p-try "^1.0.0"
p-limit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec"
dependencies:
p-try "^2.0.0"
p-locate@^2.0.0: p-locate@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
dependencies: dependencies:
p-limit "^1.1.0" p-limit "^1.1.0"
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
dependencies:
p-limit "^2.0.0"
p-map@^1.1.1: p-map@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
@@ -8898,6 +8980,10 @@ p-try@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
p-try@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
package-json@^2.0.0: package-json@^2.0.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"
@@ -9157,6 +9243,12 @@ pkg-dir@^2.0.0:
dependencies: dependencies:
find-up "^2.1.0" find-up "^2.1.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
dependencies:
find-up "^3.0.0"
plist@^2.0.1: plist@^2.0.1:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025" resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
@@ -9700,6 +9792,13 @@ pump@^2.0.0, pump@^2.0.1:
end-of-stream "^1.1.0" end-of-stream "^1.1.0"
once "^1.3.1" once "^1.3.1"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
pumpify@^1.3.3: pumpify@^1.3.3:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
@@ -10397,6 +10496,14 @@ schema-utils@^0.4.4, schema-utils@^0.4.5:
ajv "^6.1.0" ajv "^6.1.0"
ajv-keywords "^3.1.0" ajv-keywords "^3.1.0"
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
dependencies:
ajv "^6.1.0"
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
scoped-regex@^1.0.0: scoped-regex@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
@@ -10925,6 +11032,12 @@ ssri@^5.2.4:
dependencies: dependencies:
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
ssri@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
dependencies:
figgy-pudding "^3.5.1"
stable@^0.1.6: stable@^0.1.6:
version "0.1.8" version "0.1.8"
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"