Compare commits

...

40 Commits

Author SHA1 Message Date
Paulus Schoutsen
bd3d079dfb Version bump to 20180912.0 2018-09-12 13:13:49 +02:00
Paulus Schoutsen
fe776191b7 Update translations 2018-09-12 13:13:30 +02:00
Paulus Schoutsen
c546d8787d Add last used to token on profile page (#1659) 2018-09-12 13:12:26 +02:00
Jason Hu
a672b84b88 Disable delete icon if token is current used one (#1658) 2018-09-12 09:50:12 +02:00
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
Paulus Schoutsen
6ead58f62f Version bump to 20180903.0 2018-09-03 13:17:04 +02:00
Paulus Schoutsen
ec3118227c Update translations 2018-09-03 13:16:45 +02:00
Paulus Schoutsen
0d3d9bc78a Upgrade MDI icons (#1630) 2018-09-03 13:07:58 +02:00
Jason Hu
e16b3db0d4 Ask "save to login" after hassConnected (#1631) 2018-09-03 13:07:34 +02:00
Timmo
cdab874b5b Autocapitalization of username field (#1627)
* 🔨 fix capitalization of username field

* 🔨 change words to on
2018-09-03 13:06:15 +02:00
Paulus Schoutsen
bf40995b16 Show an error when invalid client id or redirect uri (#1620) 2018-09-02 10:29:38 -07:00
Paulus Schoutsen
68b3a4fbb7 Version bump to 20180831.0 2018-08-31 12:45:59 +02:00
Paulus Schoutsen
c38bfa1101 Update translations 2018-08-31 12:45:42 +02:00
Jerad Meisner
af7a85eeb7 Force line chart for climate state history. (#1617)
* Force line chart for climate state history.

* Simplify climate check
2018-08-31 12:44:07 +02:00
Paulus Schoutsen
2bd5dc21a8 Fix refresh user (#1618)
* Fix refresh user

* Lint
2018-08-31 12:28:32 +02:00
Paulus Schoutsen
18a151c8e8 Fix Safari Profile page (#1619) 2018-08-31 11:17:57 +02:00
Paulus Schoutsen
da19a1a9c6 Fix header for glance cards 2018-08-31 11:15:06 +02:00
Paulus Schoutsen
45cdb5a3e4 Use new version of HAWS (#1612)
* Use new version of HAWS

* Fix init page

* Lint

* Fix tests

* Update gitignore

* Clear old tokens, use new key to store
2018-08-31 09:45:58 +02:00
106 changed files with 2987 additions and 1178 deletions

View File

@@ -1,25 +1,31 @@
FROM node:8.2.1-alpine
FROM node:8.9-alpine
# install yarn
ENV PATH /root/.yarn/bin:$PATH
## Install/force base tools
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/* \
&& /bin/bash \
&& touch ~/.bashrc \
&& curl -o- -L https://yarnpkg.com/install.sh | bash
&& touch ~/.bashrc
## Install yarn
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
## Setup the project
RUN mkdir -p /frontend
WORKDIR /frontend
ENV NODE_ENV production
COPY package.json yarn.lock ./
COPY package.json ./
RUN yarn
COPY bower.json ./
RUN ./node_modules/.bin/bower install --allow-root
RUN yarn install --frozen-lockfile
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`
- 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
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.

1
hassio/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
hassio-icons.html

View File

@@ -10,7 +10,9 @@
"build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src test-mocha && polymer lint",
"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)",
"license": "Apache-2.0",
@@ -58,6 +60,7 @@
"@polymer/paper-tabs": "^3.0.0-pre.19",
"@polymer/paper-toast": "^3.0.0-pre.19",
"@polymer/paper-toggle-button": "^3.0.0-pre.19",
"@polymer/paper-tooltip": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.2",
"@vaadin/vaadin-combo-box": "4.1.0-alpha2",
"@vaadin/vaadin-date-picker": "3.2.0-alpha3",
@@ -68,7 +71,7 @@
"es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.0",
"fecha": "^2.3.3",
"home-assistant-js-websocket": "^2.1.0",
"home-assistant-js-websocket": "^3.1.2",
"intl-messageformat": "^2.2.0",
"js-yaml": "^3.12.0",
"leaflet": "^1.3.1",
@@ -84,6 +87,7 @@
"xss": "^1.0.3"
},
"devDependencies": {
"@gfx/zopfli": "^1.0.8",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
@@ -93,7 +97,7 @@
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"chai": "^4.1.2",
"compression-webpack-plugin": "^1.1.11",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.1",
"del": "^3.0.0",
"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
setup(name='home-assistant-frontend',
version='20180829.1',
version='20180912.0',
description='The Home Assistant frontend',
url='https://github.com/home-assistant/home-assistant-polymer',
author='The Home Assistant Authors',

View File

@@ -16,13 +16,16 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
margin: 24px 0 8px;
text-align: center;
}
.error {
color: red;
}
</style>
<form>
<template is="dom-if" if="[[_equals(_state, &quot;loading&quot;)]]">
[[localize('ui.panel.page-authorize.form.working')]]:
</template>
<template is="dom-if" if="[[_equals(_state, &quot;error&quot;)]]">
[[localize('ui.panel.page-authorize.form.unknown_error')]]:
<div class='error'>Error: [[_errorMsg]]</div>
</template>
<template is="dom-if" if="[[_equals(_state, &quot;step&quot;)]]">
<template is="dom-if" if="[[_equals(_step.type, &quot;abort&quot;)]]">
@@ -74,7 +77,8 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
_step: {
type: Object,
notify: true,
}
},
_errorMsg: String,
};
}
@@ -107,12 +111,23 @@ class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
})
});
const step = await response.json();
this._updateStep(step);
const data = await response.json();
if (response.ok) {
this._updateStep(data);
} else {
this.setProperties({
_state: 'error',
_errorMsg: data.message,
});
}
} catch (err) {
// eslint-disable-next-line
console.error('Error starting auth flow', err);
this._state = 'error';
this.setProperties({
_state: 'error',
_errorMsg: this.localize('ui.panel.page-authorize.form.unknown_error'),
});
}
}

View File

@@ -13,6 +13,10 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
static get template() {
return html`
<style>
ha-markdown {
display: block;
margin-bottom: 16px;
}
ha-markdown a {
color: var(--primary-color);
}
@@ -90,6 +94,12 @@ class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
const response = await window.providersPromise;
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) {
alert('No auth providers returned. Unable to finish login.');
return;

View File

@@ -203,7 +203,7 @@ class HaWeatherCard extends
}
getUnit(measure) {
const lengthUnit = this.hass.config.core.unit_system.length || '';
const lengthUnit = this.hass.config.unit_system.length || '';
switch (measure) {
case 'air_pressure':
return lengthUnit === 'km' ? 'hPa' : 'inHg';
@@ -212,7 +212,7 @@ class HaWeatherCard extends
case 'precipitation':
return lengthUnit === 'km' ? 'mm' : 'in';
default:
return this.hass.config.core.unit_system[measure] || '';
return this.hass.config.unit_system[measure] || '';
}
}

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

@@ -1,76 +0,0 @@
import { storeTokens, loadTokens } from './token_storage.js';
function genClientId() {
return `${location.protocol}//${location.host}/`;
}
export function redirectLogin() {
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(genClientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
return new Promise((() => {}));
}
function fetchTokenRequest(code) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'authorization_code');
data.append('code', code);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((tokens) => {
tokens.expires = (tokens.expires_in * 1000) + Date.now();
return tokens;
});
});
}
function refreshTokenRequest(tokens) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'refresh_token');
data.append('refresh_token', tokens.refresh_token);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((newTokens) => {
newTokens.expires = (newTokens.expires_in * 1000) + Date.now();
return newTokens;
});
});
}
export function resolveCode(code) {
return fetchTokenRequest(code).then((tokens) => {
storeTokens(tokens);
history.replaceState(null, null, location.pathname);
return tokens;
}, (err) => {
// eslint-disable-next-line
console.error('Resolve token failed', err);
alert('Unable to fetch tokens');
redirectLogin();
});
}
export function refreshToken() {
const tokens = loadTokens();
if (tokens === null) {
return redirectLogin();
}
return refreshTokenRequest(tokens).then((accessTokenResp) => {
const newTokens = Object.assign({}, tokens, accessTokenResp);
storeTokens(newTokens);
return newTokens;
}, () => redirectLogin());
}

View File

@@ -13,24 +13,26 @@ export function askWrite() {
return tokenCache.tokens !== undefined && tokenCache.writeEnabled === undefined;
}
export function storeTokens(tokens) {
export function saveTokens(tokens) {
tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) {
try {
storage.tokens = JSON.stringify(tokens);
storage.hassTokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line
}
}
export function enableWrite() {
tokenCache.writeEnabled = true;
storeTokens(tokenCache.tokens);
saveTokens(tokenCache.tokens);
}
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
const tokens = storage.tokens;
// Delete the old token cache.
delete storage.tokens;
const tokens = storage.hassTokens;
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;

View File

@@ -1,4 +1,4 @@
/** Return if a component is loaded. */
export default function isComponentLoaded(hass, component) {
return hass && hass.config.core.components.indexOf(component) !== -1;
return hass && hass.config.components.indexOf(component) !== -1;
}

View File

@@ -1,4 +1,4 @@
/** Get the location name from a hass object. */
export default function computeLocationName(hass) {
return hass && hass.config.core.location_name;
return hass && hass.config.location_name;
}

View File

@@ -1,5 +1,5 @@
export default function canToggleDomain(hass, domain) {
const services = hass.config.services[domain];
const services = hass.services[domain];
if (!services) { return false; }
if (domain === 'lock') {

View File

@@ -1,11 +0,0 @@
export default function parseQuery(queryString) {
const query = {};
const items = queryString.split('&');
for (let i = 0; i < items.length; i++) {
const item = items[i].split('=');
const key = decodeURIComponent(item[0]);
const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined;
query[key] = value;
}
return query;
}

View File

@@ -60,7 +60,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
@@ -73,9 +73,9 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
// We're using "!= null" on purpose so that we match both null and undefined.
if (stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null) {
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
} else if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.core.unit_system.temperature}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
} else if (stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null) {
return `${stateObj.attributes.target_humidity_low} - ${stateObj.attributes.target_humidity_high} %`;

View File

@@ -45,10 +45,13 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
async connectedCallback() {
super.connectedCallback();
if (!('serviceWorker' in navigator)) return;
if (!pushSupported) return;
try {
const reg = await navigator.serviceWorker.ready;
if (!reg.pushManager) {
return;
}
reg.pushManager.getSubscription().then((subscription) => {
this.loading = false;
this.pushChecked = !!subscription;
@@ -59,6 +62,10 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
}
handlePushChange(pushChecked) {
// Somehow this is triggered on Safari on page load causing
// it to get into a loop and crash the page.
if (!pushSupported) return;
if (pushChecked) {
this.subscribePushNotifications();
} else {

View File

@@ -17,7 +17,7 @@ class HaServiceDescription extends PolymerElement {
}
_getDescription(hass, domain, service) {
var domainServices = hass.config.services[domain];
var domainServices = hass.services[domain];
if (!domainServices) return '';
var serviceObject = domainServices[service];
if (!serviceObject) return '';

View File

@@ -34,13 +34,13 @@ class HaServicePicker extends LocalizeMixin(PolymerElement) {
if (!hass) {
this._services = [];
return;
} else if (oldHass && hass.config.services === oldHass.config.services) {
} else if (oldHass && hass.services === oldHass.services) {
return;
}
const result = [];
Object.keys(hass.config.services).sort().forEach((domain) => {
const services = Object.keys(hass.config.services[domain]).sort();
Object.keys(hass.services).sort().forEach((domain) => {
const services = Object.keys(hass.services[domain]).sort();
for (let i = 0; i < services.length; i++) {
result.push(`${domain}.${services[i]}`);

View File

@@ -10,6 +10,7 @@ import '../components/ha-icon.js';
import '../util/hass-translation.js';
import LocalizeMixin from '../mixins/localize-mixin.js';
import isComponentLoaded from '../common/config/is_component_loaded.js';
/*
* @appliesMixin LocalizeMixin
@@ -250,7 +251,7 @@ class HaSidebar extends LocalizeMixin(PolymerElement) {
}
_mqttLoaded(hass) {
return hass.config.core.components.indexOf('mqtt') !== -1;
return isComponentLoaded(hass, 'mqtt');
}
_computeUserName(user) {

View File

@@ -14,7 +14,7 @@ const DOMAINS_USE_LAST_UPDATED = ['thermostat', 'climate'];
const LINE_ATTRIBUTES_TO_KEEP = ['temperature', 'current_temperature', 'target_temp_low', 'target_temp_high'];
const stateHistoryCache = {};
function computeHistory(stateHistory, localize, language) {
function computeHistory(hass, stateHistory, localize, language) {
const lineChartDevices = {};
const timelineDevices = [];
if (!stateHistory) {
@@ -28,8 +28,12 @@ function computeHistory(stateHistory, localize, language) {
const stateWithUnit = stateInfo.find(state => 'unit_of_measurement' in state.attributes);
const unit = stateWithUnit ?
stateWithUnit.attributes.unit_of_measurement : false;
let unit = false;
if (stateWithUnit) {
unit = stateWithUnit.attributes.unit_of_measurement;
} else if (computeStateDomain(stateInfo[0]) === 'climate') {
unit = hass.config.unit_system.temperature;
}
if (!unit) {
timelineDevices.push({
@@ -311,7 +315,7 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
// Use only data from the new fetch. Old fetch is already stored in cache.data
.then(oldAndNew => oldAndNew[1])
// Convert data into format state-history-chart-* understands.
.then(stateHistory => computeHistory(stateHistory, localize, language))
.then(stateHistory => computeHistory(this.hass, stateHistory, localize, language))
// Merge old and new.
.then((stateHistory) => {
this.mergeLine(stateHistory.line, cache.data.line);
@@ -341,7 +345,7 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
}
const prom = this.fetchRecent(entityId, startTime, endTime).then(
stateHistory => computeHistory(stateHistory, localize, language),
stateHistory => computeHistory(this.hass, stateHistory, localize, language),
() => {
RECENT_CACHE[entityId] = false;
return null;
@@ -376,7 +380,7 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
const filter = startTime.toISOString() + '?end_time=' + endTime.toISOString();
const prom = this.hass.callApi('GET', 'history/period/' + filter).then(
stateHistory => computeHistory(stateHistory, localize, language),
stateHistory => computeHistory(this.hass, stateHistory, localize, language),
() => null
);

10
src/data/ws-panels.js Normal file
View File

@@ -0,0 +1,10 @@
import { createCollection } from 'home-assistant-js-websocket';
export const subscribePanels = (conn, onChange) =>
createCollection(
'_pnl',
conn_ => conn_.sendMessagePromise({ type: 'get_panels' }),
null,
conn,
onChange
);

20
src/data/ws-themes.js Normal file
View File

@@ -0,0 +1,20 @@
import { createCollection } from 'home-assistant-js-websocket';
const fetchThemes = conn => conn.sendMessagePromise({
type: 'frontend/get_themes'
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
event => store.setState(event.data, true),
'themes_updated'
);
export const subscribeThemes = (conn, onChange) =>
createCollection(
'_thm',
fetchThemes,
subscribeUpdates,
conn,
onChange
);

10
src/data/ws-user.js Normal file
View File

@@ -0,0 +1,10 @@
import { createCollection, getUser } from 'home-assistant-js-websocket';
export const subscribeUser = (conn, onChange) =>
createCollection(
'_usr',
conn_ => getUser(conn_),
null,
conn,
onChange
);

View File

@@ -125,13 +125,13 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class$="[[stateObj.attributes.operation_mode]]">
<div hidden$="[[!supportsTemperatureControls(stateObj)]]">[[localize('ui.card.climate.target_temperature')]]</div>
<template is="dom-if" if="[[supportsTemperature(stateObj)]]">
<ha-climate-control value="[[stateObj.attributes.temperature]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.max_temp]]" on-change="targetTemperatureChanged">
<ha-climate-control value="[[stateObj.attributes.temperature]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.max_temp]]" on-change="targetTemperatureChanged">
</ha-climate-control>
</template>
<template is="dom-if" if="[[supportsTemperatureRange(stateObj)]]">
<ha-climate-control value="[[stateObj.attributes.target_temp_low]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.target_temp_high]]" class="range-control-left" on-change="targetTemperatureLowChanged">
<ha-climate-control value="[[stateObj.attributes.target_temp_low]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.min_temp]]" max="[[stateObj.attributes.target_temp_high]]" class="range-control-left" on-change="targetTemperatureLowChanged">
</ha-climate-control>
<ha-climate-control value="[[stateObj.attributes.target_temp_high]]" units="[[hass.config.core.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.target_temp_low]]" max="[[stateObj.attributes.max_temp]]" class="range-control-right" on-change="targetTemperatureHighChanged">
<ha-climate-control value="[[stateObj.attributes.target_temp_high]]" units="[[hass.config.unit_system.temperature]]" step="[[computeTemperatureStepSize(hass, stateObj)]]" min="[[stateObj.attributes.target_temp_low]]" max="[[stateObj.attributes.max_temp]]" class="range-control-right" on-change="targetTemperatureHighChanged">
</ha-climate-control>
</template>
</div>
@@ -293,7 +293,7 @@ class MoreInfoClimate extends LocalizeMixin(EventsMixin(PolymerElement)) {
computeTemperatureStepSize(hass, stateObj) {
if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step;
} else if (hass.config.core.unit_system.temperature.indexOf('F') !== -1) {
} else if (hass.config.unit_system.temperature.indexOf('F') !== -1) {
return 1;
}
return 0.5;

View File

@@ -304,7 +304,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
sendTTS() {
const services = this.hass.config.services.tts;
const services = this.hass.services.tts;
const serviceKeys = Object.keys(services).sort();
let service;
let i;

View File

@@ -156,7 +156,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
}
getUnit(measure) {
const lengthUnit = this.hass.config.core.unit_system.length || '';
const lengthUnit = this.hass.config.unit_system.length || '';
switch (measure) {
case 'air_pressure':
return lengthUnit === 'km' ? 'hPa' : 'inHg';
@@ -165,7 +165,7 @@ class MoreInfoWeather extends LocalizeMixin(PolymerElement) {
case 'precipitation':
return lengthUnit === 'km' ? 'mm' : 'in';
default:
return this.hass.config.core.unit_system[measure] || '';
return this.hass.config.unit_system[measure] || '';
}
}

View File

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

View File

@@ -1,78 +1,64 @@
import {
ERR_INVALID_AUTH,
getAuth,
createConnection,
subscribeConfig,
subscribeEntities,
subscribeServices,
ERR_INVALID_AUTH,
} from 'home-assistant-js-websocket';
import { redirectLogin, resolveCode, refreshToken } from '../common/auth/token.js';
// import refreshToken_ from '../common/auth/refresh_token.js';
import parseQuery from '../common/util/parse_query.js';
import { loadTokens } from '../common/auth/token_storage.js';
import { loadTokens, saveTokens } from '../common/auth/token_storage.js';
import { subscribePanels } from '../data/ws-panels.js';
import { subscribeThemes } from '../data/ws-themes.js';
import { subscribeUser } from '../data/ws-user.js';
const init = window.createHassConnection = function (password, accessToken) {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/websocket?${__BUILD__}`;
const options = {
setupRetry: 10,
};
if (password) {
options.authToken = password;
} else if (accessToken) {
options.accessToken = accessToken.access_token;
options.expires = accessToken.expires;
const hassUrl = `${location.protocol}//${location.host}`;
const isExternal = location.search.includes('external_auth=1');
const authProm = isExternal ?
() => import('../common/auth/external_auth.js')
.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(url, options)
.then(function (conn) {
subscribeEntities(conn);
subscribeConfig(conn);
return conn;
});
};
function main() {
if (location.search) {
const query = parseQuery(location.search.substr(1));
if (query.code) {
window.hassConnection = resolveCode(query.code).then(newTokens => init(null, newTokens));
return;
}
}
const tokens = loadTokens();
window.hassConnection = authProm().then(connProm);
if (tokens == null) {
redirectLogin();
return;
}
if (Date.now() + 30000 > tokens.expires) {
// refresh access token if it will expire in 30 seconds to avoid invalid auth event
window.hassConnection = refreshToken().then(newTokens => init(null, newTokens));
return;
}
window.hassConnection = init(null, tokens).catch((err) => {
if (err !== ERR_INVALID_AUTH) throw err;
return refreshToken().then(newTokens => init(null, newTokens));
});
}
function mainLegacy() {
if (window.noAuth === '1') {
window.hassConnection = init();
} else if (window.localStorage.authToken) {
window.hassConnection = init(window.localStorage.authToken);
} else {
window.hassConnection = null;
}
}
if (window.useOAuth === '1') {
main();
} else {
mainLegacy();
}
// Start fetching some of the data that we will need.
window.hassConnection.then(({ conn }) => {
const noop = () => {};
subscribeEntities(conn, noop);
subscribeConfig(conn, noop);
subscribeServices(conn, noop);
subscribePanels(conn, noop);
subscribeThemes(conn, noop);
subscribeUser(conn, noop);
});
window.addEventListener('error', (e) => {
const homeAssistant = document.querySelector('home-assistant');

View File

@@ -1,12 +1,21 @@
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import { getUser } from 'home-assistant-js-websocket';
import { clearState } from '../../util/ha-pref-storage.js';
import { askWrite } from '../../common/auth/token_storage.js';
import { subscribeUser } from '../../data/ws-user.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-logout', () => this._handleLogout());
this.addEventListener('hass-refresh-current-user', () => this._getCurrentUser());
// HACK :( We don't have a way yet to trigger an update of `subscribeUser`
this.addEventListener('hass-refresh-current-user', () =>
getUser(this.hass.connection).then(user => this._updateHass({ user })));
}
hassConnected() {
super.hassConnected();
subscribeUser(this.hass.connection, user => this._updateHass({ user }));
afterNextRender(null, () => {
if (askWrite()) {
@@ -18,24 +27,16 @@ export default superClass => class extends superClass {
});
}
hassConnected() {
super.hassConnected();
this._getCurrentUser();
}
_getCurrentUser() {
// only for new auth
if (this.hass.connection.options.accessToken) {
this.hass.callWS({
type: 'auth/current_user',
}).then(user => this._updateHass({ user }), () => {});
async _handleLogout() {
try {
await this.hass.auth.revoke();
this.hass.connection.close();
clearState();
document.location.href = '/';
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert('Log out failed');
}
}
_handleLogout() {
this.hass.connection.close();
clearState();
document.location.href = '/';
}
};

View File

@@ -2,6 +2,8 @@ import {
ERR_INVALID_AUTH,
subscribeEntities,
subscribeConfig,
subscribeServices,
callService,
} from 'home-assistant-js-websocket';
import translationMetadata from '../../../build-translations/translationMetadata.json';
@@ -9,44 +11,34 @@ import translationMetadata from '../../../build-translations/translationMetadata
import LocalizeMixin from '../../mixins/localize-mixin.js';
import EventsMixin from '../../mixins/events-mixin.js';
import { refreshToken } from '../../common/auth/token.js';
import { getState } from '../../util/ha-pref-storage.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 computeStateName from '../../common/entity/compute_state_name.js';
import { subscribePanels } from '../../data/ws-panels';
export default superClass =>
class extends EventsMixin(LocalizeMixin(superClass)) {
constructor() {
super();
this.unsubFuncs = [];
}
ready() {
super.ready();
this.addEventListener('try-connection', e =>
this._handleNewConnProm(e.detail.connProm));
if (window.hassConnection) {
this._handleNewConnProm(window.hassConnection);
}
this._handleConnProm();
}
async _handleNewConnProm(connProm) {
this.connectionPromise = connProm;
async _handleConnProm() {
let auth;
let conn;
try {
conn = await connProm;
const result = await window.hassConnection;
auth = result.auth;
conn = result.conn;
} catch (err) {
this.connectionPromise = null;
this._error = true;
return;
}
this._setConnection(conn);
}
_setConnection(conn) {
this.hass = Object.assign({
auth,
connection: conn,
connected: true,
states: null,
@@ -64,7 +56,7 @@ export default superClass =>
moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => {
try {
await conn.callService(domain, service, serviceData);
await callService(conn, domain, service, serviceData);
let message;
let name;
@@ -98,27 +90,9 @@ export default superClass =>
throw err;
}
},
callApi: async (method, path, parameters) => {
const host = window.location.protocol + '//' + window.location.host;
const auth = conn.options;
try {
// Refresh token if it will expire in 30 seconds
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
const accessToken = await refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
}
return await hassCallApi(host, auth, method, path, parameters);
} catch (err) {
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
// If we connect with access token and get 401, refresh token and try again
const accessToken = await refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
return await hassCallApi(host, auth, method, path, parameters);
}
},
callApi: async (method, path, parameters) =>
hassCallApi(auth, method, path, parameters),
fetchWithAuth: (path, init) => fetchWithAuth(auth, `${auth.data.hassUrl}${path}`, init),
// For messages that do not get a response
sendWS: (msg) => {
// eslint-disable-next-line
@@ -138,9 +112,7 @@ export default superClass =>
err => console.log('Error', err),
);
}
// In the future we'll do this as a breaking change
// inside home-assistant-js-websocket
return resp.then(result => result.result);
return resp;
},
}, getState());
@@ -152,56 +124,26 @@ export default superClass =>
const conn = this.hass.connection;
const reconnected = () => this.hassReconnected();
const disconnected = () => this.hassDisconnected();
const reconnectError = async (_conn, err) => {
if (err !== ERR_INVALID_AUTH) return;
while (this.unsubFuncs.length) {
this.unsubFuncs.pop()();
}
const accessToken = await refreshToken();
const newConn = window.createHassConnection(null, accessToken);
newConn.then(() => this.hassReconnected());
this._handleNewConnProm(newConn);
};
conn.addEventListener('ready', reconnected);
conn.addEventListener('disconnected', disconnected);
// If we reconnect after losing connection and access token is no longer
// valid.
conn.addEventListener('reconnect-error', reconnectError);
this.unsubFuncs.push(() => {
conn.removeEventListener('ready', reconnected);
conn.removeEventListener('disconnected', disconnected);
conn.removeEventListener('reconnect-error', reconnectError);
conn.addEventListener('ready', () => this.hassReconnected());
conn.addEventListener('disconnected', () => this.hassDisconnected());
// If we reconnect after losing connection and auth is no longer valid.
conn.addEventListener('reconnect-error', (_conn, err) => {
if (err === ERR_INVALID_AUTH) location.reload();
});
subscribeEntities(conn, states => this._updateHass({ states }))
.then(unsub => this.unsubFuncs.push(unsub));
subscribeConfig(conn, config => this._updateHass({ config }))
.then(unsub => this.unsubFuncs.push(unsub));
this._loadPanels();
subscribeEntities(conn, states => this._updateHass({ states }));
subscribeConfig(conn, config => this._updateHass({ config }));
subscribeServices(conn, services => this._updateHass({ services }));
subscribePanels(conn, panels => this._updateHass({ panels }));
}
hassReconnected() {
super.hassReconnected();
this._updateHass({ connected: true });
this._loadPanels();
}
hassDisconnected() {
super.hassDisconnected();
this._updateHass({ connected: false });
}
async _loadPanels() {
const panels = await this.hass.callWS({
type: 'get_panels'
});
this._updateHass({ panels });
}
};

View File

@@ -6,6 +6,7 @@ import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import '../../layouts/home-assistant-main.js';
import '../../layouts/ha-init-page.js';
import '../../resources/ha-style.js';
import registerServiceWorker from '../../util/register-service-worker.js';
@@ -20,8 +21,6 @@ import ConnectionMixin from './connection-mixin.js';
import NotificationMixin from './notification-mixin.js';
import DisconnectToastMixin from './disconnect-toast-mixin.js';
import(/* webpackChunkName: "login-form" */ '../../layouts/login-form.js');
const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass);
class HomeAssistant extends ext(PolymerElement, [
@@ -52,21 +51,13 @@ class HomeAssistant extends ext(PolymerElement, [
</template>
<template is="dom-if" if="[[!showMain]]" restamp>
<login-form
hass="[[hass]]"
connection-promise="[[connectionPromise]]"
show-loading="[[computeShowLoading(connectionPromise, hass)]]"
></login-form>
<ha-init-page error='[[_error]]'></ha-init-page>
</template>
`;
}
static get properties() {
return {
connectionPromise: {
type: Object,
value: null,
},
hass: {
type: Object,
value: null,
@@ -82,6 +73,10 @@ class HomeAssistant extends ext(PolymerElement, [
computed: 'computePanelUrl(routeData)',
observer: 'panelUrlChanged',
},
_error: {
type: Boolean,
value: false,
}
};
}
@@ -91,13 +86,7 @@ class HomeAssistant extends ext(PolymerElement, [
}
computeShowMain(hass) {
return hass && hass.states && hass.config && hass.panels;
}
computeShowLoading(connectionPromise, hass) {
// Show loading when connecting or when connected but not all pieces loaded yet
return (connectionPromise != null
|| (hass && hass.connection && (!hass.states || !hass.config)));
return hass && hass.states && hass.config && hass.panels && hass.services;
}
computePanelUrl(routeData) {

View File

@@ -1,46 +1,33 @@
import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js';
import { storeState } from '../../util/ha-pref-storage.js';
import { subscribeThemes } from '../../data/ws-themes.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('settheme', e => this._setTheme(e));
this.addEventListener('settheme', (ev) => {
this._updateHass({ selectedTheme: ev.detail });
this._applyTheme();
storeState(this.hass);
});
}
hassConnected() {
super.hassConnected();
this.hass.callWS({
type: 'frontend/get_themes',
}).then((themes) => {
subscribeThemes(this.hass.connection, (themes) => {
this._updateHass({ themes });
applyThemesOnElement(
document.documentElement,
themes,
this.hass.selectedTheme,
true
);
this._applyTheme();
});
this.hass.connection.subscribeEvents((event) => {
this._updateHass({ themes: event.data });
applyThemesOnElement(
document.documentElement,
event.data,
this.hass.selectedTheme,
true
);
}, 'themes_updated').then(unsub => this.unsubFuncs.push(unsub));
}
_setTheme(event) {
this._updateHass({ selectedTheme: event.detail });
_applyTheme() {
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme,
true
);
storeState(this.hass);
}
};

View File

@@ -0,0 +1,48 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/paper-button/paper-button.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 LocalizeMixin from '../mixins/localize-mixin.js';
import EventsMixin from '../mixins/events-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
class HaInitPage extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
<style>
paper-spinner {
margin-bottom: 10px;
}
</style>
<div class="layout vertical center center-center fit">
<img src="/static/icons/favicon-192x192.png" height="192">
<paper-spinner active="[[!error]]"></paper-spinner>
<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>
`;
}
static get properties() {
return {
error: Boolean,
};
}
_retry() {
location.reload();
}
}
customElements.define('ha-init-page', HaInitPage);

View File

@@ -1,186 +0,0 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.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 { html } from '@polymer/polymer/lib/utils/html-tag.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 EventsMixin from '../mixins/events-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
<style>
:host {
white-space: nowrap;
}
paper-input {
display: block;
margin-bottom: 16px;
}
paper-checkbox {
margin-right: 8px;
}
paper-button {
margin-left: 72px;
}
.interact {
height: 125px;
}
#validatebox {
margin-top: 16px;
text-align: center;
}
.validatemessage {
margin-top: 10px;
}
</style>
<div class="layout vertical center center-center fit">
<img src="/static/icons/favicon-192x192.png" height="192">
<a href="#" id="hideKeyboardOnFocus"></a>
<div class="interact">
<div id="loginform" hidden$="[[showSpinner]]">
<paper-input id="passwordInput" label="[[localize('ui.login-form.password')]]" type="password" autofocus="" invalid="[[errorMessage]]" error-message="[[errorMessage]]" value="{{password}}"></paper-input>
<div class="layout horizontal center">
<paper-checkbox for="" id="rememberLogin">[[localize('ui.login-form.remember')]]</paper-checkbox>
<paper-button on-click="validatePassword">[[localize('ui.login-form.log_in')]]</paper-button>
</div>
</div>
<div id="validatebox" hidden$="[[!showSpinner]]">
<paper-spinner active="true"></paper-spinner><br>
<div class="validatemessage">[[computeLoadingMsg(isValidating)]]</div>
</div>
</div>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
connectionPromise: {
type: Object,
notify: true,
observer: 'handleConnectionPromiseChanged',
},
errorMessage: {
type: String,
value: '',
},
isValidating: {
type: Boolean,
observer: 'isValidatingChanged',
value: false,
},
showLoading: {
type: Boolean,
value: false,
},
showSpinner: {
type: Boolean,
computed: 'computeShowSpinner(showLoading, isValidating)',
},
password: {
type: String,
value: '',
},
};
}
ready() {
super.ready();
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
}
computeLoadingMsg(isValidating) {
return isValidating ? 'Connecting' : 'Loading data';
}
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;
}
}
);
}
}
customElements.define('login-form', LoginForm);

View File

@@ -4,11 +4,8 @@ import '@polymer/paper-input/paper-input.js';
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 hassCallApi from '../util/hass-call-api.js';
import localizeLiteMixin from '../mixins/localize-lite-mixin.js';
const callApi = (method, path, data) => hassCallApi('', {}, method, path, data);
class HaOnboarding extends localizeLiteMixin(PolymerElement) {
static get template() {
return html`
@@ -42,6 +39,7 @@ class HaOnboarding extends localizeLiteMixin(PolymerElement) {
value='{{_name}}'
required
auto-validate
autocapitalize='on'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
on-blur='_maybePopulateUsername'
></paper-input>
@@ -51,6 +49,7 @@ class HaOnboarding extends localizeLiteMixin(PolymerElement) {
value='{{_username}}'
required
auto-validate
autocapitalize='none'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
></paper-input>
@@ -139,12 +138,23 @@ class HaOnboarding extends localizeLiteMixin(PolymerElement) {
this._errorMsg = '';
try {
await callApi('post', 'onboarding/users', {
name: this._name,
username: this._username,
password: this._password,
const response = await fetch('/api/onboarding/users', {
method: 'POST',
credentials: 'same-origin',
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 = '/';
} catch (err) {
// 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.
<ul>
<li>
<a href="https://alexa.amazon.com/spa/index.html#skills/dp/B0772J1QKB/?ref=skill_dsk_skb_sr_2" target="_blank">
Activate the Home Assistant skill for Alexa
</a>
To activate, search in the Alexa app for the Home Assistant Smart Home skill.
</li>
<li>
<a href="https://www.home-assistant.io/cloud/alexa/" target="_blank">

View File

@@ -37,6 +37,7 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
value='{{_name}}'
required
auto-validate
autocapitalize='on'
error-message='Required'
on-blur='_maybePopulateUsername'
></paper-input>
@@ -46,6 +47,7 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
value='{{_username}}'
required
auto-validate
autocapitalize='none'
error-message='Required'
></paper-input>
<paper-input

View File

@@ -29,9 +29,6 @@ class HaUserEditor extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
paper-card:last-child {
margin-bottom: 16px;
}
paper-button {
display: block;
}
</style>
<hass-subpage header="View user">
@@ -57,9 +54,12 @@ class HaUserEditor extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
</paper-card>
<paper-card>
<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')]]
</paper-button>
<template is='dom-if' if='[[user.system_generated]]'>
Unable to remove system generated users.
</template>
</div>
</paper-card>
</hass-subpage>

View File

@@ -50,7 +50,12 @@ class HaUserPicker extends EventsMixin(NavigateMixin(LocalizeMixin(PolymerElemen
<paper-item>
<paper-item-body two-line>
<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>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>

View File

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

View File

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

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

@@ -122,10 +122,10 @@ class HaPanelDevInfo extends PolymerElement {
<p class='version'>
<a href='https://www.home-assistant.io'><img src="/static/icons/favicon-192x192.png" height="192" /></a><br />
Home Assistant<br />
[[hass.config.core.version]]
[[hass.config.version]]
</p>
<p>
Path to configuration.yaml: [[hass.config.core.config_dir]]
Path to configuration.yaml: [[hass.config.config_dir]]
</p>
<p class='develop'>
<a href='https://www.home-assistant.io/developers/credits/' target='_blank'>

View File

@@ -230,7 +230,7 @@ class HaPanelDevService extends PolymerElement {
}
_computeAttributesArray(hass, domain, service) {
const serviceDomains = hass.config.services;
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
@@ -241,7 +241,7 @@ class HaPanelDevService extends PolymerElement {
}
_computeDescription(hass, domain, service) {
const serviceDomains = hass.config.services;
const serviceDomains = hass.services;
if (!(domain in serviceDomains)) return undefined;
if (!(service in serviceDomains[domain])) return undefined;
return serviceDomains[domain][service].description;

View File

@@ -22,17 +22,14 @@ class HuiGlanceCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
padding: 16px;
}
ha-card[header] {
padding-top: 0;
}
.entities {
display: flex;
margin-bottom: -12px;
padding: 0 16px 4px;
flex-wrap: wrap;
}
.entities.no-header {
padding-top: 16px;
}
.entity {
box-sizing: border-box;
padding: 0 4px;
@@ -55,8 +52,8 @@ class HuiGlanceCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
</style>
<ha-card header$="[[_config.title]]">
<div class="entities">
<ha-card header="[[_config.title]]">
<div class$="[[_computeClasses(_config.title)]]">
<template is="dom-repeat" items="[[_configEntities]]">
<template is="dom-if" if="[[_showEntity(item, hass.states)]]">
<div class="entity" on-click="_handleClick">
@@ -96,6 +93,10 @@ class HuiGlanceCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
this._configEntities = processConfigEntities(config.entities);
}
_computeClasses(hasHeader) {
return `entities ${hasHeader ? '' : 'no-header'}`;
}
_showEntity(item, states) {
return item.entity in states;
}

View File

@@ -162,7 +162,7 @@ class HuiMapCard extends PolymerElement {
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(this.hass.config.core.latitude, this.hass.config.core.longitude),
new Leaflet.LatLng(this.hass.config.latitude, this.hass.config.longitude),
zoom || 14
);
return;

View File

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

View File

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

View File

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

View File

@@ -18,21 +18,27 @@ class HuiGroupEntityRow extends LocalizeMixin(PolymerElement) {
hass="[[hass]]"
config="[[_config]]"
>
<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>
${this.groupControlTemplate}
</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() {
return {
hass: Object,

View File

@@ -9,6 +9,19 @@ import '../components/hui-generic-entity-row.js';
class HuiInputNumberEntityRow extends mixinBehaviors([IronResizableBehavior], PolymerElement) {
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`
<style>
.flex {
@@ -23,41 +36,40 @@ class HuiInputNumberEntityRow extends mixinBehaviors([IronResizableBehavior], Po
text-align: right;
}
</style>
<hui-generic-entity-row
hass="[[hass]]"
config="[[_config]]"
id="input_number_card"
>
<div>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
<div class="flex">
<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]]"
`;
}
static get inputNumberControlTemplate() {
return html`
<div>
<template is="dom-if" if="[[_equals(_stateObj.attributes.mode, 'slider')]]">
<div class="flex">
<paper-slider
min="[[_min]]"
max="[[_max]]"
value="{{_value}}"
type="number"
step="[[_step]]"
pin
on-change="_selectedValueChanged"
></paper-input>
</template>
</div>
</hui-generic-entity-row>
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]]"
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) {
static get template() {
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>
${this.styleTemplate}
<template is="dom-if" if="[[_stateObj]]">
<state-badge state-obj="[[_stateObj]]"></state-badge>
<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() {
return {
hass: Object,

View File

@@ -11,21 +11,27 @@ class HuiInputTextEntityRow extends PolymerElement {
hass="[[hass]]"
config="[[_config]]"
>
<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>
${this.inputTextControlTemplate}
</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() {
return {
hass: Object,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,21 +18,27 @@ class HuiToggleEntityRow extends LocalizeMixin(PolymerElement) {
hass="[[hass]]"
config="[[_config]]"
>
<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>
${this.toggleControlTemplate}
</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() {
return {
hass: Object,

View File

@@ -7,6 +7,19 @@ import callService from '../common/call-service.js';
class HuiCallServiceRow extends PolymerElement {
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`
<style>
:host {
@@ -36,13 +49,6 @@ class HuiCallServiceRow extends PolymerElement {
margin-right: -.57em;
}
</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 {
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`
<style>
a {
@@ -24,12 +36,6 @@ class HuiWeblinkRow extends PolymerElement {
margin-left: 16px;
}
</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/paper-button/paper-button.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-item/paper-item-body.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 LocalizeMixin from '../../mixins/localize-mixin.js';
let registeredDialog = false;
/*
* @appliesMixin LocalizeMixin
*/
@@ -57,32 +58,8 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
display: flex;
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) {
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 {
width: auto;
padding: 0;
@@ -128,28 +105,6 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
</paper-card>
</div>
</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: {
type: Array,
},
currentMessage: {
type: Object,
},
};
}
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.hass.connection.subscribeEvents(this.hassChanged, 'mailbox_updated')
.then(function (unsub) { this._unsubEvents = unsub; }.bind(this));
@@ -209,35 +168,19 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
}
openMP3Dialog(event) {
var platform = event.model.item.platform;
this.currentMessage = event.model.item;
this.$.mp3dialog.open();
this.$.mp3src.src = '/api/mailbox/media/' + platform + '/' + event.model.item.sha;
this.$.transcribe.innerText = event.model.item.message;
this.$.mp3.load();
this.$.mp3.play();
this.fire('show-audio-message-dialog', {
hass: this.hass,
message: event.model.item,
});
}
_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() {
const items = this.platforms.map(function (platform) {
return this.hass.callApi('GET', 'mailbox/messages/' + platform).then(function (values) {
var platformItems = [];
var arrayLength = values.length;
for (var i = 0; i < arrayLength; i++) {
var datetime = formatDateTime(new Date(values[i].info.origtime * 1000));
return this.hass.callApi('GET', `mailbox/messages/${platform}`).then(function (values) {
const platformItems = [];
const arrayLength = values.length;
for (let i = 0; i < arrayLength; i++) {
const datetime = formatDateTime(new Date(values[i].info.origtime * 1000));
platformItems.push({
timestamp: datetime,
caller: values[i].info.callerid,
@@ -251,15 +194,9 @@ class HaPanelMailbox extends LocalizeMixin(PolymerElement) {
});
}.bind(this));
return Promise.all(items).then(function (platformItems) {
var arrayLength = items.length;
var final = [];
for (var i = 0; i < arrayLength; i++) {
final = final.concat(platformItems[i]);
}
final.sort(function (a, b) {
return [].concat(...platformItems).sort(function (a, b) {
return new Date(b.timestamp) - new Date(a.timestamp);
});
return final;
});
}

View File

@@ -79,7 +79,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(this.hass.config.core.latitude, this.hass.config.core.longitude),
new Leaflet.LatLng(this.hass.config.latitude, this.hass.config.longitude),
14
);
} else {

View File

@@ -0,0 +1,136 @@
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 three-line>
<span slot='heading'>[[item.client_name]]</span>
<div slot='description'>[[_formatCreatedAt(item.created_at)]]</div>
<div slot='description'>[[_formatLastUsed(item)]]</div>
<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))
);
}
_formatLastUsed(item) {
return item.last_used_at ? this.localize(
'ui.panel.profile.refresh_tokens.last_used',
'date', formatDateTime(new Date(item.last_used_at)),
'location', item.last_used_ip
) : this.localize('ui.panel.profile.refresh_tokens.not_used');
}
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-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-theme-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>
</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>
</app-header-layout>
`;
@@ -95,9 +113,21 @@ class HaPanelProfile extends EventsMixin(PolymerElement) {
hass: Object,
narrow: Boolean,
showMenu: Boolean,
_refreshTokens: Array,
};
}
connectedCallback() {
super.connectedCallback();
this._refreshRefreshTokens();
}
async _refreshRefreshTokens() {
this._refreshTokens = await this.hass.callWS({
type: 'auth/refresh_tokens'
});
}
_handleLogOut() {
this.fire('hass-logout');
}

View File

@@ -0,0 +1,106 @@
import '@polymer/paper-icon-button/paper-icon-button.js';
import '@polymer/paper-tooltip/paper-tooltip.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);
}
paper-icon-button[disabled] {
color: var(--disabled-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 three-line>
<span slot='heading'>[[_formatTitle(item.client_id)]]</span>
<div slot='description'>[[_formatCreatedAt(item.created_at)]]</div>
<div slot='description'>[[_formatLastUsed(item)]]</div>
<div>
<template is='dom-if' if='[[item.is_current]]'>
<paper-tooltip
position="left"
>[[localize('ui.panel.profile.refresh_tokens.current_token_tooltip')]]</paper-tooltip>
</template>
<paper-icon-button
icon="hass:delete"
on-click='_handleDelete'
disabled="[[item.is_current]]"
></paper-icon-button>
</div>
</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))
);
}
_formatLastUsed(item) {
return item.last_used_at ? this.localize(
'ui.panel.profile.refresh_tokens.last_used',
'date', formatDateTime(new Date(item.last_used_at)),
'location', item.last_used_ip
) : this.localize('ui.panel.profile.refresh_tokens.not_used');
}
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

@@ -22,7 +22,7 @@ class HaSettingsRow extends PolymerElement {
padding-right: 16px;
}
</style>
<paper-item-body two-line>
<paper-item-body two-line$='[[!threeLine]]' three-line$='[[threeLine]]'>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
@@ -35,6 +35,10 @@ class HaSettingsRow extends PolymerElement {
narrow: {
type: Boolean,
reflectToAttribute: true,
},
threeLine: {
type: Boolean,
value: false,
}
};
}

View File

@@ -746,6 +746,32 @@
"error_no_theme": "No themes available.",
"link_promo": "Learn about themes",
"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}",
"last_used": "Last used at {date} from {location}",
"not_used": "Has never been used",
"confirm_delete": "Are you sure you want to delete the refresh token for {name}?",
"delete_failed": "Failed to delete the refresh token.",
"current_token_tooltip": "Unable to delete current 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}",
"last_used": "Last used at {date} from {location}",
"not_used": "Has never been used",
"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": {

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

@@ -4,19 +4,17 @@ import canToggleDomain from '../../../src/common/entity/can_toggle_domain';
describe('canToggleDomain', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
lock: {
lock: null,
unlock: null,
},
sensor: {
custom_service: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
lock: {
lock: null,
unlock: null,
},
sensor: {
custom_service: null,
},
},
};

View File

@@ -4,12 +4,10 @@ import canToggleState from '../../../src/common/entity/can_toggle_state';
describe('canToggleState', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
},
};

View File

@@ -4,12 +4,10 @@ import stateCardType from '../../../src/common/entity/state_card_type.js';
describe('stateCardType', () => {
const hass = {
config: {
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
services: {
light: {
turn_on: null, // Service keys only need to be present for test
turn_off: null,
},
},
};

View File

@@ -1,18 +0,0 @@
import { assert } from 'chai';
import parseQuery from '../../../src/common/util/parse_query.js';
describe('parseQuery', () => {
it('works', () => {
assert.deepEqual(parseQuery('hello=world'), { hello: 'world' });
assert.deepEqual(parseQuery('hello=world&drink=soda'), {
hello: 'world',
drink: 'soda',
});
assert.deepEqual(parseQuery('hello=world&no_value&drink=soda'), {
hello: 'world',
no_value: undefined,
drink: 'soda',
});
});
});

View File

@@ -546,6 +546,58 @@
"link_promo": "Научете повече за темите",
"dropdown_label": "Тема"
}
},
"page-authorize": {
"form": {
"working": "Моля, изчакайте",
"unknown_error": "Ся си еба майката",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Потребителско име",
"password": "Парола"
}
}
},
"error": {
"invalid_auth": "Невалидно потребителско име или парола",
"invalid_code": "Невалиден код за аутентикация"
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Потребител"
},
"description": "Моля, изберете потребител, като който искате да влезете:"
}
}
},
"legacy_api_password": {
"error": {
"invalid_code": "Невалиден код за аутентикация"
}
}
}
}
},
"page-onboarding": {
"user": {
"intro": "Нека да започнете, като създадете потребителски акаунт.",
"required_field": "Задължително",
"data": {
"name": "Име",
"username": "Потребителско име",
"password": "Парола"
},
"create_account": "Създай акаунт",
"error": {
"required_fields": "Попълнете всички задължителни полета"
}
}
}
},
"sidebar": {

View File

@@ -545,6 +545,27 @@
"error_no_theme": "No hi ha temes disponibles.",
"link_promo": "Més informació sobre temes",
"dropdown_label": "Tema"
},
"refresh_tokens": {
"header": "Refresca els testimonis d'autenticació",
"description": "Cada testimoni d'autenticació d'actualització representa un inici de sessió diferent. Els testimonis d'autenticació d'actualització s'eliminaran automàticament quan tanqueu la sessió. A sota hi ha una llista de testimonis d'autenticació d'actualització que estan actius actualment al vostre compte.",
"token_title": "Refresca el testimoni d'autenticació del {clientId}",
"created_at": "Creat el {date}",
"confirm_delete": "Esteu segur que voleu suprimir el testimoni d'autenticació d'actualització per {name}?",
"delete_failed": "No s'ha pogut suprimir el testimoni d'autenticació d'actualització."
},
"long_lived_access_tokens": {
"header": "Testimonis d'autenticació d'accés de llarga durada",
"description": "Creeu testimonis d'autenticació d'accés de llarga durada per permetre als vostres programes (scripts) interactuar amb la vostra instància de Home Assistant. Cada testimoni d'autenticació serà vàlid durant deu anys després de la seva creació. Els següents testimoni d'autenticació d'accés de llarga durada estan actius actualment.",
"learn_auth_requests": "Apren a fer sol·licituds autenticades.",
"created_at": "Creat el {date}",
"confirm_delete": "Esteu segur que voleu suprimir el testimoni d'autenticació d'accés per {name}?",
"delete_failed": "No s'ha pogut suprimir el testimoni d'autenticació d'accés.",
"create": "Crea un testimoni d'autenticació",
"create_failed": "No s'ha pogut crear el testimoni d'autenticació d'accés.",
"prompt_name": "Nom?",
"prompt_copy_token": "Copieu el testimoni d'autenticació (token) d'accés. No es tornarà a mostrar més endavant.",
"empty_state": "Encara no tens testimonis d'autenticaciós d'accés de llarga durada."
}
},
"page-authorize": {

View File

@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Inicializuji",
"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",
"abort_intro": "Přihlášení bylo zrušeno",
"form": {

View File

@@ -389,12 +389,14 @@
},
"mqtt": {
"label": "MQTT",
"topic": "Emne"
"topic": "Emne",
"payload": "Indhold (valgfri)"
},
"numeric_state": {
"label": "Numerisk tilstand",
"above": "Over",
"below": "Under"
"below": "Under",
"value_template": "Værdi-skabelon"
},
"sun": {
"label": "Sol",
@@ -436,6 +438,7 @@
"state": "Tilstand"
},
"numeric_state": {
"label": "Numerisk stadie",
"above": "Over",
"below": "Under",
"value_template": "Værdi skabelon (ikke krævet)"
@@ -444,11 +447,14 @@
"label": "Sol",
"before": "Før:",
"after": "Efter:",
"before_offset": "Forskydning før",
"after_offset": "Forskydning efter",
"sunrise": "Solopgang",
"sunset": "Solnedgang"
},
"template": {
"label": "Skabelon"
"label": "Skabelon",
"value_template": "Værdi-skabelon"
},
"time": {
"label": "Tid",
@@ -470,6 +476,7 @@
"delete": "Slet",
"delete_confirm": "Er du sikker på du vil slette?",
"unsupported_action": "Ikke-understøttet handling: {action}",
"type_select": "Begivenhedstype",
"type": {
"service": {
"label": "Kald service",
@@ -481,12 +488,15 @@
},
"wait_template": {
"label": "Vent",
"wait_template": "Vente-skabelon",
"timeout": "Timeout (valgfri)"
},
"condition": {
"label": "Betingelse"
},
"event": {
"label": "Afsend hændelse",
"event": "Hændelse:",
"service_data": "Service data"
}
}
@@ -536,6 +546,95 @@
"link_promo": "Lær om temaer",
"dropdown_label": "Tema"
}
},
"page-authorize": {
"initializing": "Initialiserer",
"authorizing_client": "Du er ved at give {clientId} adgang til din Home Assistant-instans.",
"logging_in_with": "Log ind med **{authProviderName}**.",
"pick_auth_provider": "Eller log ind med",
"abort_intro": "Login afbrudt",
"form": {
"working": "Vent venligst",
"unknown_error": "Noget gik galt",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Brugernavn",
"password": "Password"
}
},
"mfa": {
"data": {
"code": "To-faktor godkendelseskode"
},
"description": "Åbn **{mfa_module_name}** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:"
}
},
"error": {
"invalid_auth": "Ugyldigt brugernavn eller password",
"invalid_code": "Ugyldig godkendelseskode"
},
"abort": {
"login_expired": "Session er udløbet, log ind igen."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API password"
},
"description": "Indtast venligst API-adgangskoden fra din http-konfiguration:"
},
"mfa": {
"data": {
"code": "To-faktor godkendelseskode"
},
"description": "Åbn **{mfa_module_name}** på din enhed for at se din to-faktor godkendelseskode og bekræft din identitet:"
}
},
"error": {
"invalid_auth": "Ugyldig API password",
"invalid_code": "Ugyldig godkendelseskode"
},
"abort": {
"no_api_password_set": "Du har ikke konfigureret en API-adgangskode.",
"login_expired": "Session er udløbet, log ind igen."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Bruger"
},
"description": "Vælg venligst den bruger, du vil logge ind som:"
}
},
"abort": {
"not_whitelisted": "Din computer er ikke whitelistet."
}
}
}
}
},
"page-onboarding": {
"intro": "Er du klar til at vække din hjem, genvinde dit privatliv og deltage i et verdensomspændende fællesskab af tinkerers?",
"user": {
"intro": "Lad os komme i gang ved at oprette en brugerkonto.",
"required_field": "Nødvendig",
"data": {
"name": "Navn",
"username": "Brugernavn",
"password": "Password"
},
"create_account": "Opret konto",
"error": {
"required_fields": "Udfyld alle obligatoriske felter"
}
}
}
},
"sidebar": {
@@ -622,16 +721,23 @@
"light": {
"brightness": "Lysstyrke",
"color_temperature": "Farvetemperatur",
"white_value": "Hvidværdi",
"effect": "Effekt"
},
"media_player": {
"text_to_speak": "Tekst til tale",
"source": "Kilde"
"source": "Kilde",
"sound_mode": "Lydtilstand"
},
"climate": {
"currently": "Lige nu",
"on_off": "On \/ off",
"fan_mode": "Ventilator tilstand"
"target_temperature": "Ønsket temperatur",
"target_humidity": "Ønsket luftfugtighed",
"operation": "Drift",
"fan_mode": "Ventilator tilstand",
"away_mode": "Ude af huset-modus",
"aux_heat": "Støtte-varme"
},
"lock": {
"code": "Kode",
@@ -692,6 +798,11 @@
"ask": "Vil du gemme dette login?",
"decline": "Nej tak",
"confirm": "Gem login"
},
"notification_drawer": {
"click_to_configure": "Klik på knappen for at konfigurere {entity}",
"empty": "Ingen notifikationer",
"title": "Notifikationer"
}
},
"domain": {

View File

@@ -545,6 +545,32 @@
"error_no_theme": "No themes available.",
"link_promo": "Learn about themes",
"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.",
"last_used": "Last used at {date} from {location}",
"not_used": "Has never been used",
"current_token_tooltip": "Unable to delete current 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.",
"last_used": "Last used at {date} from {location}",
"not_used": "Has never been used"
}
},
"page-authorize": {

View File

@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Inicializando",
"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",
"abort_intro": "Inicio de sesión cancelado",
"form": {
@@ -568,7 +568,8 @@
"mfa": {
"data": {
"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:"
}
},
"error": {
@@ -584,15 +585,23 @@
"init": {
"data": {
"password": "Contraseña API"
}
},
"description": "Por favor, introduzca la contraseña de la API en su configuración http:"
},
"mfa": {
"data": {
"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:"
}
},
"error": {
"invalid_auth": "Contraseña API inválida"
"invalid_auth": "Contraseña API inválida",
"invalid_code": "Código de autenticación inválido"
},
"abort": {
"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": {

View File

@@ -484,7 +484,7 @@
},
"delay": {
"label": "Retardo",
"delay": "Retardo"
"delay": "Demora"
},
"wait_template": {
"label": "Esperar",
@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Inicializando",
"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",
"abort_intro": "Inicio de sesión cancelado",
"form": {
@@ -564,10 +564,20 @@
"username": "Nombre de usuario",
"password": "Contraseña"
}
},
"mfa": {
"data": {
"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:"
}
},
"error": {
"invalid_auth": "Nombre de usuario o contraseña inválidos"
"invalid_auth": "Nombre de usuario o contraseña inválidos",
"invalid_code": "Código de autenticación inválido"
},
"abort": {
"login_expired": "La sesión expiró, por favor inicie sesión de nuevo."
}
},
"legacy_api_password": {
@@ -575,14 +585,23 @@
"init": {
"data": {
"password": "Contraseña de API"
}
},
"description": "Por favor, introduzca la contraseña de la API en su configuración http:"
},
"mfa": {
"data": {
"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:"
}
},
"error": {
"invalid_auth": "Contraseña de API inválida"
"invalid_auth": "Contraseña de API inválida",
"invalid_code": "Código de autenticación inválido"
},
"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": "La sesión expiró, por favor inicie sesión de nuevo."
}
},
"trusted_networks": {
@@ -590,7 +609,8 @@
"init": {
"data": {
"user": "Usuario"
}
},
"description": "Por favor, seleccione el usuario con el que desea iniciar sesión:"
}
},
"abort": {
@@ -601,6 +621,7 @@
}
},
"page-onboarding": {
"intro": "¿Estás listo para despertar tu casa, reclamar tu privacidad y unirte a una comunidad mundial de pensadores?",
"user": {
"intro": "Comencemos creando una cuenta de usuario.",
"required_field": "Obligatorio",
@@ -689,7 +710,8 @@
"trigger": "Desencadenar"
},
"cover": {
"position": "Posición"
"position": "Posición",
"tilt_position": "Posición inclinada"
},
"fan": {
"speed": "Velocidad",

View File

@@ -546,6 +546,95 @@
"link_promo": "Lisateave teemade kohta",
"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": {
@@ -710,6 +799,11 @@
"ask": "Kas soovid selle sisselogimise salvestada?",
"decline": "Tänan ei",
"confirm": "Salvesta sisselogimine"
},
"notification_drawer": {
"click_to_configure": "{entity} seadistamiseks klõpsa nuppu",
"empty": "Teavitusi pole",
"title": "Teavitused"
}
},
"domain": {

View File

@@ -259,6 +259,8 @@
"docked": "Sur la base",
"error": "Erreur",
"idle": "Inactif",
"off": "Off",
"on": "On",
"paused": "En pause",
"returning": "Retourne à la base"
}
@@ -528,14 +530,109 @@
"push_notifications": {
"header": "Notifications push",
"description": "Envoyer des notifications à cet appareil.",
"push_notifications": "Notifications push"
"error_load_platform": "Configurer notify.html5.",
"error_use_https": "Nécessite l'activation de SSL pour le frontend.",
"push_notifications": "Notifications push",
"link_promo": "En savoir plus"
},
"language": {
"header": "Langue",
"link_promo": "Aider à traduire",
"dropdown_label": "Langue"
},
"themes": {
"header": "Thème",
"error_no_theme": "Aucun thème disponible.",
"link_promo": "En savoir plus sur les thèmes",
"dropdown_label": "Thème"
}
},
"page-authorize": {
"initializing": "Initialisation",
"authorizing_client": "Vous êtes sur le point de donner accès à {clientId} à votre instance de Home Assistant.",
"logging_in_with": "Se connecter avec **{authProviderName}**.",
"pick_auth_provider": "Ou connectez-vous avec",
"abort_intro": "Connexion interrompue",
"form": {
"working": "Veuillez patienter",
"unknown_error": "Quelque chose a mal tourné",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Nom d'utilisateur",
"password": "Mot de passe"
}
},
"mfa": {
"data": {
"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é:"
}
},
"error": {
"invalid_auth": "Nom d'utilisateur ou mot de passe invalide",
"invalid_code": "Code d'authentification invalide"
},
"abort": {
"login_expired": "Session expirée, veuillez vous connecter à nouveau."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "Mot de passe API"
},
"description": "Veuillez saisir le mot de passe API dans votre configuration http:"
},
"mfa": {
"data": {
"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é:"
}
},
"error": {
"invalid_auth": "Mot de passe API invalide",
"invalid_code": "Code d'authentification invalide"
},
"abort": {
"no_api_password_set": "Vous n'avez pas de mot de passe API configuré.",
"login_expired": "Session expirée, veuillez vous connecter à nouveau."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Utilisateur"
},
"description": "Sélectionnez l'utilisateur avec lequel vous souhaitez vous connecter :"
}
},
"abort": {
"not_whitelisted": "Votre ordinateur n'est pas en liste blanche."
}
}
}
}
},
"page-onboarding": {
"intro": "Êtes-vous prêt à réveiller votre maison, à récupérer votre vie privée et à rejoindre une communauté mondiale de bricoleurs?",
"user": {
"intro": "Commençons par créer un compte utilisateur.",
"required_field": "Requis",
"data": {
"name": "Nom",
"username": "Nom d'utilisateur"
"username": "Nom d'utilisateur",
"password": "Mot de passe"
},
"create_account": "Créer un compte",
"error": {
"required_fields": "Remplissez tous les champs requis"
}
}
}
@@ -629,7 +726,8 @@
},
"media_player": {
"text_to_speak": "Texte à lire",
"source": "Source"
"source": "Source",
"sound_mode": "Mode sonore"
},
"climate": {
"currently": "Actuellement",
@@ -669,7 +767,14 @@
"relative_time": {
"past": "{time} auparavant",
"future": "Dans {time}",
"never": "Jamais"
"never": "Jamais",
"duration": {
"second": "{count} {count, plural,\none {seconde}\nother {secondes}\n}",
"minute": "{count} {count, plural,\none {minute}\nother {minutes}\n}",
"hour": "{count} {count, plural,\n one {hour}\n other {hours}\n}",
"day": "{count} {count, plural,\n one {day}\n other {days}\n}",
"week": "{count} {count, plural,\n one {week}\n other {weeks}\n}"
}
},
"history_charts": {
"loading_history": "Chargement de l'historique des valeurs ...",
@@ -686,8 +791,19 @@
"dialogs": {
"more_info_settings": {
"save": "Sauvegarder",
"name": "Nom"
"name": "Nom",
"entity_id": "ID de l'entité"
}
},
"auth_store": {
"ask": "Voulez-vous enregistrer cette connexion?",
"decline": "Non merci",
"confirm": "Enregistrer la connexion"
},
"notification_drawer": {
"click_to_configure": "Cliquez sur le bouton pour configurer {entity}",
"empty": "Aucune notification",
"title": "Notifications"
}
},
"domain": {
@@ -725,7 +841,8 @@
"switch": "Interrupteur",
"updater": "Mise à jour",
"weblink": "Lien",
"zwave": "Z-Wave"
"zwave": "Z-Wave",
"vacuum": "Aspirateur"
},
"attribute": {
"weather": {

View File

@@ -157,6 +157,19 @@
}
}
}
},
"page-authorize": {
"form": {
"providers": {
"trusted_networks": {
"step": {
"init": {
"description": "कृपया उस उपयोगकर्ता का चयन करें जिसके रूप में आप लॉगिन करना चाहते हैं:"
}
}
}
}
}
}
},
"sidebar": {

View File

@@ -283,7 +283,7 @@
"armed_custom_bypass": "Éles"
},
"device_tracker": {
"home": "0tthon",
"home": "Otthon",
"not_home": "Távol"
}
},
@@ -546,6 +546,95 @@
"link_promo": "Tudj meg többet a témákról",
"dropdown_label": "Téma"
}
},
"page-authorize": {
"initializing": "Inicializálás",
"authorizing_client": "Éppen a {clientId} számára készülsz hozzáférést biztosítani a Home Assistant példányodhoz.",
"logging_in_with": "Bejelentkezés **{authProviderName}** használatával.",
"pick_auth_provider": "Vagy válassz a következő bejelentkezési módok közül:",
"abort_intro": "Bejelentkezés megszakítva",
"form": {
"working": "Kérlek várj",
"unknown_error": "Valami hiba történt",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Felhasználónév",
"password": "Jelszó"
}
},
"mfa": {
"data": {
"code": "Kétfaktoros Hitelesítési Kód"
},
"description": "Nyisd meg a(z) **{mfa_module_name}** applikációt az eszközödön, hogy megtekintsd a kétfaktoros hitelesítési kódodat a személyazonosságod ellenőrzéséhez."
}
},
"error": {
"invalid_auth": "Érvénytelen felhasználónév vagy jelszó",
"invalid_code": "Érvénytelen hitelesítési kód"
},
"abort": {
"login_expired": "A munkamenet lejárt, jelentkezz be újra."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API Jelszó"
},
"description": "Kérlek, add meg az API jelszót a http konfigurációban:"
},
"mfa": {
"data": {
"code": "Kétfaktoros Hitelesítési Kód"
},
"description": "Nyisd meg a(z) **{mfa_module_name}** applikációt az eszközödön, hogy megtekintsd a kétfaktoros hitelesítési kódodat a személyazonosságod ellenőrzéséhez."
}
},
"error": {
"invalid_auth": "Érvénytelen API jelszó",
"invalid_code": "Érvénytelen hitelesítési kód"
},
"abort": {
"no_api_password_set": "Nincs megadva API jelszó a konfigurációban.",
"login_expired": "A munkamenet lejárt, jelentkezz be újra."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Felhasználó"
},
"description": "Kérlek, válassz egy felhasználót a bejelentkezéshez:"
}
},
"abort": {
"not_whitelisted": "A számítógéped nem engedélyezett."
}
}
}
}
},
"page-onboarding": {
"intro": "Készen állsz arra, hogy felébreszd az otthonod, visszaszerezed a magánéleted és csatlakozz egy világhálós közösséghez?",
"user": {
"intro": "Kezdjük a felhasználói fiók létrehozásával.",
"required_field": "Szükséges",
"data": {
"name": "Név",
"username": "Felhasználónév",
"password": "Jelszó"
},
"create_account": "Fiók Létrehozása",
"error": {
"required_fields": "Töltsd ki az összes szükséges mezőt"
}
}
}
},
"sidebar": {

View File

@@ -3,7 +3,7 @@
"config": "Konfigurasi",
"states": "Ikhtisar",
"map": "Peta",
"logbook": "Buku Catatan",
"logbook": "Catatan Log",
"history": "Riwayat",
"mailbox": "Kotak pesan",
"shopping_list": "Daftar belanja",
@@ -72,8 +72,8 @@
"on": "Tidak aman"
},
"presence": {
"off": "di luar",
"on": "di rumah"
"off": "Keluar",
"on": "Rumah"
},
"battery": {
"off": "Normal",
@@ -164,7 +164,7 @@
"locked": "Terkunci",
"unlocked": "Terbuka",
"ok": "OK",
"problem": "Malasah"
"problem": "Masalah"
},
"input_boolean": {
"off": "Off",
@@ -183,6 +183,7 @@
"on": "On",
"playing": "Memainkan",
"paused": "Jeda",
"idle": "Diam",
"standby": "Siaga"
},
"plant": {
@@ -231,6 +232,13 @@
"snowy-rainy": "Bersalju, hujan",
"sunny": "Cerah",
"windy": "Berangin"
},
"vacuum": {
"cleaning": "Membersihkan",
"error": "Kesalahan",
"idle": "Siaga",
"paused": "Berhenti",
"returning": "Kembali ke dock"
}
},
"state_badge": {
@@ -334,13 +342,15 @@
"to": "Ke"
},
"homeassistant": {
"label": "Home Assistant",
"event": "Event:",
"start": "Mulai",
"shutdown": "Matikan"
},
"mqtt": {
"label": "MQTT",
"topic": "Topik"
"topic": "Topik",
"payload": "Payload (opsional)"
},
"numeric_state": {
"label": "Status nomor",
@@ -460,6 +470,122 @@
"zwave": {
"caption": "Z-Wave",
"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"
},
"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": {
@@ -527,6 +698,9 @@
"service": "Layanan"
},
"relative_time": {
"past": "lalu",
"future": "dalam",
"never": "Tak pernah",
"duration": {
"second": "{count} {count, plural,\n one {detik}\n other {detik}\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}",
"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": {
"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 ..."
},
"dialogs": {
"more_info_settings": {
"save": "Simpan",
"name": "Nama",
"entity_id": "Entiti ID"
}
},
"auth_store": {
"ask": "Apakah Anda ingin menyimpan login ini?",
"decline": "Tidak, terima kasih",
"confirm": "Simpan login"
},
"notification_drawer": {
"click_to_configure": "Klik tombol untuk mengonfigurasi {entity}",
"empty": "Tidak ada pemberitahuan",
"title": "Pemberitahuan"
}
},
"domain": {
@@ -577,7 +771,8 @@
"sun": "Matahari",
"switch": "Sakelar",
"updater": "Updater",
"zwave": "Z-Wave"
"zwave": "Z-Wave",
"vacuum": "Vakum"
},
"attribute": {
"weather": {

View File

@@ -528,15 +528,113 @@
},
"profile": {
"push_notifications": {
"header": "Notifiche push"
"header": "Notifiche push",
"description": "Invia notifiche a questo dispositivo.",
"error_load_platform": "Configura notify.html5.",
"error_use_https": "Richiede SSL abilitato per il frontend.",
"push_notifications": "Notifiche push",
"link_promo": "Per saperne di più"
},
"language": {
"header": "Lingua"
"header": "Lingua",
"link_promo": "Aiuta a tradurre",
"dropdown_label": "Lingua"
},
"themes": {
"header": "Tema",
"error_no_theme": "Nessun tema disponibile.",
"link_promo": "Per saperne di più sui temi",
"dropdown_label": "Tema"
}
},
"page-authorize": {
"initializing": "Inizializzazione",
"authorizing_client": "Stai per dare accesso {clientId} alla tua istanza di Assistente Home.",
"logging_in_with": "Accesso con **{authProviderName}**.",
"pick_auth_provider": "Oppure accedi con",
"abort_intro": "Login interrotto",
"form": {
"working": "Attendere prego",
"unknown_error": "Qualcosa è andato storto",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Nome utente",
"password": "Password"
}
},
"mfa": {
"data": {
"code": "Codice di autenticazione a due fattori"
},
"description": "Apri il **{mfa_module_name}** sul tuo dispositivo per visualizzare il codice di autenticazione a due fattori e verificare la tua identità:"
}
},
"error": {
"invalid_auth": "Nome utente o password errati",
"invalid_code": "Codice di autenticazione non valido"
},
"abort": {
"login_expired": "Sessione scaduta, effettua nuovamente il login."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "Password API"
},
"description": "Inserisci l'API password nella configurazione http:"
},
"mfa": {
"data": {
"code": "Codice di autenticazione a due fattori"
},
"description": "Apri il **{mfa_module_name}** sul tuo dispositivo per visualizzare il codice di autenticazione a due fattori e verificare la tua identità:"
}
},
"error": {
"invalid_auth": "Password API non valida",
"invalid_code": "Codice di autenticazione non valido"
},
"abort": {
"no_api_password_set": "Non hai una password API configurata.",
"login_expired": "Sessione scaduta, effettua nuovamente il login."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Utente"
},
"description": "Perfavore, scegli l'utente con cui vuoi effettuare l'accesso:"
}
},
"abort": {
"not_whitelisted": "Il tuo computer non è nella whitelist."
}
}
}
}
},
"page-onboarding": {
"intro": "Sei pronto per risvegliare la tua casa, reclamare la tua privacy e far parte di una comunità mondiale di smanettoni?",
"user": {
"intro": "Cominciamo con la creazione di un account utente.",
"required_field": "Necessario",
"data": {
"name": "Nome",
"username": "Nome utente",
"password": "Password"
},
"create_account": "Crea un Account",
"error": {
"required_fields": "Compila tutti i campi richiesti"
}
}
}
},
"sidebar": {
@@ -696,6 +794,16 @@
"name": "Nome",
"entity_id": "ID Entità"
}
},
"auth_store": {
"ask": "Vuoi salvare questo login?",
"decline": "No grazie",
"confirm": "Salva login"
},
"notification_drawer": {
"click_to_configure": "Fare clic sul pulsante per configurare {entity}",
"empty": "Nessuna notifica",
"title": "Notifiche"
}
},
"domain": {

View File

@@ -257,7 +257,7 @@
"vacuum": {
"cleaning": "청소중",
"docked": "충전중",
"error": "에러",
"error": "작동 에러",
"idle": "대기중",
"off": "꺼짐",
"on": "켜짐",
@@ -531,7 +531,7 @@
"header": "푸시 알림",
"description": "이 기기에 알림 보내기",
"error_load_platform": "notify.html5 를 구성해주세요.",
"error_use_https": "프론트 엔드에는 SSL 사용이 필요합니다.",
"error_use_https": "SSL 을 통한 보안연결된 환경이 필요합니다.",
"push_notifications": "푸시 알림",
"link_promo": "더 알아보기"
},
@@ -569,7 +569,7 @@
"data": {
"code": "2 단계 인증 코드"
},
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 ** {mfa_module_name} ** 을(를) 열어주세요:"
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 **{mfa_module_name}** 을(를) 열어주세요:"
}
},
"error": {
@@ -586,13 +586,13 @@
"data": {
"password": "API 비밀번호"
},
"description": "http config 에 API password 를 입력해주세요:"
"description": "configuration.yaml 에 설정한 api_password 를 입력해주세요:"
},
"mfa": {
"data": {
"code": "2 단계 인증 코드"
},
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 ** {mfa_module_name} ** 을(를) 열어주세요:"
"description": "2 단계 인증 코드 및 신원을 확인하기 위해 기기에서 **{mfa_module_name}** 을(를) 열어주세요:"
}
},
"error": {
@@ -600,8 +600,8 @@
"invalid_code": "잘못된 인증 코드"
},
"abort": {
"no_api_password_set": "API 비밀번호를 구성하지 않았습니다.",
"login_expired": "세션이 만료되었습니다. 다시 로그인 해주세요"
"no_api_password_set": "API 비밀번호를 설정하지 않았습니다.",
"login_expired": "세션이 만료되었습니다. 다시 로그인 해주세요."
}
},
"trusted_networks": {
@@ -632,7 +632,7 @@
},
"create_account": "계정 만들기",
"error": {
"required_fields": "모든 필수 입력란을 채워주세요"
"required_fields": "필수 입력란을 모두 채워주세요"
}
}
}

View File

@@ -564,10 +564,20 @@
"username": "Benotzernumm",
"password": "Passwuert"
}
},
"mfa": {
"data": {
"code": "2-Faktor-Authentifikatiouns Code"
},
"description": "Maacht **{mfa_module_name}** op Ärem Apparat op, fir ären 2-Faktor-Authentifikatiouns Code ze kucken an Är Identitéit z'iwwerpréiwen:"
}
},
"error": {
"invalid_auth": "Ongëltege Benotzernumm oder Passwuert"
"invalid_auth": "Ongëltege Benotzernumm oder Passwuert",
"invalid_code": "Ongëlte Authentifikatiouns Code"
},
"abort": {
"login_expired": "Sessioun ofgelaaf, log dech rëm frësch an w.e.g."
}
},
"legacy_api_password": {
@@ -575,14 +585,23 @@
"init": {
"data": {
"password": "API Passwuert"
}
},
"description": "Gitt d'API Passwuert vun ärer http Konfiguratioun an:"
},
"mfa": {
"data": {
"code": "2-Faktor-Authentifikatiouns Code"
},
"description": "Maacht **{mfa_module_name}** op Ärem Apparat op, fir ären 2-Faktor-Authentifikatiouns Code ze kucken an Är Identitéit z'iwwerpréiwen:"
}
},
"error": {
"invalid_auth": "Ongëltegt API Passwuert"
"invalid_auth": "Ongëltegt API Passwuert",
"invalid_code": "Ongëlte Authentifikatiouns Code"
},
"abort": {
"no_api_password_set": "Dir hutt nach keen API Passwuert definéiert."
"no_api_password_set": "Dir hutt nach keen API Passwuert definéiert.",
"login_expired": "Sessioun ofgelaaf, log dech rëm frësch an w.e.g."
}
},
"trusted_networks": {
@@ -590,7 +609,8 @@
"init": {
"data": {
"user": "Benotzer"
}
},
"description": "Wielt den User aus mat deem dir iech wëllt aloggen:"
}
},
"abort": {

View File

@@ -64,9 +64,12 @@
"not_home": "Prom"
},
"media_player": {
"off": "Izslēgts",
"on": "Ieslēgts",
"playing": "Atskaņo",
"paused": "Apturēts",
"idle": "Dīkstāvē"
"idle": "Dīkstāvē",
"standby": "Gaidīšanas režīmā"
},
"plant": {
"ok": "Labi",
@@ -154,7 +157,7 @@
"introduction": "Izmaiņas konfigurācijā var būt nogurdinošs process. Mēs zinām. Šai sadaļai vajadzētu padarīt dzīvi mazliet vieglāku.",
"validation": {
"heading": "Konfigurācijas pārbaude",
"introduction": "Veiciet konfigurācijas pārbaudi, ja nesen esat veicis izmaiņas konfigurācijā un vēlaties pārliecināties, ka tā ir korekta",
"introduction": "Veiciet konfigurācijas pārbaudi, ja nesen esat veicis izmaiņas konfigurācijā un vēlaties pārliecināties, ka tā ir korekta.",
"check_config": "Pārbaudīt",
"valid": "Konfigurācija korekta!",
"invalid": "Konfigurācija kļūdaina"
@@ -191,6 +194,26 @@
"zwave": {
"caption": "Z-Wave"
}
},
"profile": {
"push_notifications": {
"header": "Pašpiegādes paziņojumi",
"description": "Sūtīt paziņojumus uz šo ierīci.",
"error_load_platform": "Konfigurēt notify.html5.",
"push_notifications": "Pašpiegādes paziņojumi",
"link_promo": "Uzziniet vairāk"
},
"language": {
"header": "Valoda",
"link_promo": "Palīdziet tulkot",
"dropdown_label": "Valoda"
},
"themes": {
"header": "Motīvs",
"error_no_theme": "Nav pieejams neviens motīvs",
"link_promo": "Uzziniet par tēmām",
"dropdown_label": "Motīvs"
}
}
},
"login-form": {

View File

@@ -312,11 +312,11 @@
"introduction": "Her er det mulig å konfigurere dine komponenter og Home Assistant. Ikke alt er mulig å konfigurere fra brukergrensesnittet enda, men vi jobber med det.",
"core": {
"caption": "Generelt",
"description": "Validere konfigurasjonsfilen din og kontroller serveren",
"description": "Valider konfigurasjonsfilen din og kontroller serveren",
"section": {
"core": {
"header": "Konfigurasjon og serverkontroll",
"introduction": "Endringer i konfigurasjonen kan være en slitsom prosess, vi vet det. Denne delen vil forsøke å gjøre livet ditt litt lettere.",
"introduction": "Endring av konfigurasjonen kan være en slitsom prosess, det vet vi. Denne delen vil forsøke å gjøre livet ditt litt lettere.",
"validation": {
"heading": "Konfigurasjonsvalidering",
"introduction": "Sjekk konfigurasjon din hvis du nylig har gjort noen endringer og ønsker å være sikker på at den er gyldig ved å kjøre en valideringstest",
@@ -334,7 +334,7 @@
},
"server_management": {
"heading": "Serveradministrasjon",
"introduction": "Kontroller Home Assistant fra Home Assistant.",
"introduction": "Kontroller din Home Assistant server...fra Home Assistant.",
"restart": "Start på nytt",
"stop": "Stopp"
}
@@ -359,7 +359,7 @@
"introduction": "Bruk automatiseringer for å få liv i hjemmet ditt",
"default_name": "Ny automatisering",
"save": "Lagre",
"unsaved_confirm": "Du har ulagrede endringer. Er du sikker på at du vil forlate?",
"unsaved_confirm": "Du har endringer som ikke er lagret. Er du sikker på at du vil forlate?",
"alias": "Navn",
"triggers": {
"header": "Utløsere",
@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Initialiserer",
"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",
"abort_intro": "Innlogging avbrutt",
"form": {
@@ -567,9 +567,9 @@
},
"mfa": {
"data": {
"code": "To-faktor Autentisering Kode"
"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": {
@@ -577,7 +577,7 @@
"invalid_code": "Ugyldig autentiseringskode"
},
"abort": {
"login_expired": "Sessionen utløpt, vennligst logg inn igjen."
"login_expired": "Økten er utløpt, vennligst logg inn på nytt"
}
},
"legacy_api_password": {
@@ -590,9 +590,9 @@
},
"mfa": {
"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": {
@@ -600,8 +600,8 @@
"invalid_code": "Ugyldig autentiseringskode"
},
"abort": {
"no_api_password_set": "Du har ikke et API-passord konfigurert.",
"login_expired": "Sesjonen har utløpt, vennligst logg inn igjen: "
"no_api_password_set": "Du har ikke konfigurert et API-passord.",
"login_expired": "Økten er utløpt, vennligst logg inn på nytt"
}
},
"trusted_networks": {
@@ -743,7 +743,7 @@
"lock": {
"code": "Kode",
"lock": "Lås",
"unlock": "Låse opp"
"unlock": "Lås opp"
},
"vacuum": {
"actions": {
@@ -778,7 +778,7 @@
},
"history_charts": {
"loading_history": "Laster statushistorikk...",
"no_history_found": "Ingen status history funnet."
"no_history_found": "Ingen statushistorikk funnet."
}
},
"notification_toast": {
@@ -840,7 +840,7 @@
"sun": "Sol",
"switch": "Bryter",
"updater": "Oppdateringer",
"weblink": "Nettlink",
"weblink": "Lenke",
"zwave": "Z-Wave",
"vacuum": "Støvsuger"
},

View File

@@ -18,7 +18,7 @@
"state": {
"default": {
"off": "wyłączony",
"on": "włączony",
"on": "Włączony",
"unknown": "nieznany",
"unavailable": "niedostępny"
},
@@ -41,7 +41,7 @@
"binary_sensor": {
"default": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"moisture": {
"off": "sucho",
@@ -122,7 +122,7 @@
},
"calendar": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"camera": {
"recording": "nagrywanie",
@@ -131,7 +131,7 @@
},
"climate": {
"off": "wyłączony",
"on": "włączony",
"on": "Włączony",
"heat": "ogrzewanie",
"cool": "chłodzenie",
"idle": "nieaktywny",
@@ -162,7 +162,7 @@
},
"fan": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"group": {
"off": "wyłączony",
@@ -181,7 +181,7 @@
},
"input_boolean": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"light": {
"off": "wyłączony",
@@ -193,7 +193,7 @@
},
"media_player": {
"off": "wyłączony",
"on": "włączony",
"on": "Włączony",
"playing": "odtwarzanie",
"paused": "pauza",
"idle": "nieaktywny",
@@ -205,37 +205,37 @@
},
"remote": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"scene": {
"scening": "sceny"
},
"script": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"sensor": {
"off": "wyłączony",
"on": "włączony"
"on": "Włączony"
},
"sun": {
"above_horizon": "powyżej horyzontu",
"below_horizon": "poniżej horyzontu"
"above_horizon": "Powyżej horyzontu",
"below_horizon": "Poniżej horyzontu"
},
"switch": {
"off": "wyłączony",
"on": "włączony"
"off": "Wyłączony",
"on": "Włączony"
},
"zwave": {
"default": {
"initializing": "inicjalizacja",
"dead": "martwy",
"sleeping": "uśpiony",
"ready": "gotowy"
"initializing": "Inicjalizacja",
"dead": "Nieaktywny",
"sleeping": "Uśpiony",
"ready": "Gotowy"
},
"query_stage": {
"initializing": "inicjalizacja ({query_stage})",
"dead": "martwy ({query_stage})"
"dead": "Nieaktywny ({query_stage})"
}
},
"weather": {
@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Inicjowanie",
"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ą",
"abort_intro": "Logowanie przerwane",
"form": {
@@ -569,7 +569,7 @@
"data": {
"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": {
@@ -592,7 +592,7 @@
"data": {
"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": {
@@ -610,7 +610,7 @@
"data": {
"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": {
@@ -621,12 +621,12 @@
}
},
"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": {
"intro": "Zacznijmy od utworzenia konta użytkownika.",
"required_field": "Wymagane",
"data": {
"name": "Nazwa",
"name": "Imię",
"username": "Nazwa użytkownika",
"password": "Hasło"
},

View File

@@ -546,6 +546,95 @@
"link_promo": "Aprenda sobre temas",
"dropdown_label": "Tema"
}
},
"page-authorize": {
"initializing": "Iniciando",
"authorizing_client": "Você está prestes a dar acesso pra {clientId} à sua instância do Home Assistant.",
"logging_in_with": "Fazendo login com **{authProviderName}**.",
"pick_auth_provider": "Ou entre com",
"abort_intro": "Login cancelado",
"form": {
"working": "Aguarde",
"unknown_error": "Alguma coisa saiu errada",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Usuário",
"password": "Senha"
}
},
"mfa": {
"data": {
"code": "Código de autenticação de dois fatores"
},
"description": "Abra o ** {mfa_module_name} ** no seu dispositivo para ver seu código de autenticação de dois fatores e confirmar sua identidade:"
}
},
"error": {
"invalid_auth": "Usuário ou senha inválidos",
"invalid_code": "Código de autenticação inválido"
},
"abort": {
"login_expired": "Sessão expirada, por favor fazer o login novamente."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "Senha de API"
},
"description": "Por favor insira a senha da API em sua configuração http:"
},
"mfa": {
"data": {
"code": "Código de autenticação de dois fatores"
},
"description": "Abra o ** {mfa_module_name} ** no seu dispositivo para ver seu código de autenticação de dois fatores e confirmar sua identidade:"
}
},
"error": {
"invalid_auth": "Senha de API inválida",
"invalid_code": "Código de autenticação inválido"
},
"abort": {
"no_api_password_set": "Você não tem uma senha de API configurada.",
"login_expired": "Sessão expirada, por favor fazer o login novamente."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Usuário"
},
"description": "Por favor, selecione o usuário que você quer fazer o login como:"
}
},
"abort": {
"not_whitelisted": "Seu computador não está na lista de permissões."
}
}
}
}
},
"page-onboarding": {
"intro": "Você está pronto para despertar sua casa, recuperar sua privacidade e se juntar a uma comunidade mundial de consertadores?",
"user": {
"intro": "Vamos começar criando uma conta de usuário.",
"required_field": "Obrigatório",
"data": {
"name": "Nome",
"username": "Usuário",
"password": "Senha"
},
"create_account": "Criar Conta",
"error": {
"required_fields": "Preencha todos os campos obrigatórios"
}
}
}
},
"sidebar": {

View File

@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "A inicializar",
"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",
"abort_intro": "Login abortado",
"form": {

View File

@@ -525,6 +525,114 @@
"delete_user": "Ștergeți utilizatorul"
}
}
},
"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": {
"initializing": "Inițializează",
"logging_in_with": "Conectare cu **{authProviderName}**.",
"pick_auth_provider": "Sau conectați-vă cu",
"abort_intro": "Conectare intrerupta",
"form": {
"working": "Te rog așteaptă",
"unknown_error": "Ceva n-a mers bine",
"providers": {
"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": {
"invalid_auth": "nume de utilizator sau parola incorecte",
"invalid_code": "Codul 'Two-factor Authentication' invalid"
},
"abort": {
"login_expired": "Sesiunea a expirat, va rugam logati-va din nou."
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "Parola API"
},
"description": "Introduceti parola API in http config:"
},
"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": {
"invalid_auth": "Parola API nevalidă",
"invalid_code": "Codul 'Two-factor Authentication' invalid"
},
"abort": {
"no_api_password_set": "Nu aveți o parolă API configurată.",
"login_expired": "Sesiunea a expirat, va rugam logati-va din nou."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Utilizator"
},
"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": {
@@ -684,6 +792,16 @@
"name": "Nume",
"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": {

View File

@@ -319,7 +319,7 @@
"introduction": "Изменение конфигурации может быть утомительным процессом. Мы знаем. Этот раздел попытается сделать вашу жизнь немного легче.",
"validation": {
"heading": "Проверка конфигурации",
"introduction": "Проверьте свою конфигурацию, если вы внесли в нее некоторые изменения и хотите убедиться, что она действительна",
"introduction": "Проверьте свою конфигурацию, если вы внесли в нее некоторые изменения и хотите убедиться в ее работоспособности",
"check_config": "Проверить конфигурацию",
"valid": "Конфигурация выполнена верно!",
"invalid": "Ошибка в конфигурации"
@@ -504,7 +504,7 @@
}
},
"script": {
"caption": "Скрипт",
"caption": "Скрипты",
"description": "Создавайте и редактируйте скрипты"
},
"zwave": {
@@ -531,7 +531,7 @@
"header": "Push-уведомления",
"description": "Отправлять уведомления на это устройство",
"error_load_platform": "Настроить notify.html5.",
"error_use_https": "Требуется SSL для интерфейса.",
"error_use_https": "Требуется SSL для веб-интерфейса.",
"push_notifications": "Push-уведомления",
"link_promo": "Узнать больше"
},
@@ -550,7 +550,7 @@
"page-authorize": {
"initializing": "Инициализация",
"authorizing_client": "Вы собираетесь предоставить доступ {clientId} к вашему Home Assistant.",
"logging_in_with": "Вход с помощью ** {authProviderName} **.",
"logging_in_with": "Вход с помощью **{authProviderName}**.",
"pick_auth_provider": "Или войти с помощью",
"abort_intro": "Вход прерван",
"form": {
@@ -569,7 +569,7 @@
"data": {
"code": "Код двухфакторной аутентификации"
},
"description": "Откройте ** {mfa_module_name} ** на вашем устройстве для просмотра кода двухфакторной аутентификации и подтвердите что это Вы:"
"description": "Откройте **{mfa_module_name}** на вашем устройстве для просмотра кода двухфакторной аутентификации и введите его для подтверждения:"
}
},
"error": {
@@ -586,13 +586,13 @@
"data": {
"password": "Пароль API"
},
"description": "Пожалуйста, введите пароль API в вашей конфигурации http:"
"description": "Пожалуйста, введите пароль API, указанный в Вашей конфигурации http:"
},
"mfa": {
"data": {
"code": "Код двухфакторной аутентификации"
},
"description": "Откройте ** {mfa_module_name} ** на вашем устройстве для просмотра кода двухфакторной аутентификации и подтвердите что это Вы:"
"description": "Откройте **{mfa_module_name}** на вашем устройстве для просмотра кода двухфакторной аутентификации и введите его для подтверждения:"
}
},
"error": {
@@ -782,8 +782,8 @@
}
},
"notification_toast": {
"entity_turned_on": "{entity} включение.",
"entity_turned_off": "{entity} выключение.",
"entity_turned_on": "{entity} включается",
"entity_turned_off": "{entity} выключается",
"service_called": "Вызов службы {service}.",
"service_call_failed": "Не удалось вызвать службу {service}.",
"connection_lost": "Соединение потеряно. Переподключение ..."

Some files were not shown because too many files have changed in this diff Show More