mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-09 04:59:37 +00:00
Compare commits
40 Commits
20180829.1
...
20180912.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd3d079dfb | ||
![]() |
fe776191b7 | ||
![]() |
c546d8787d | ||
![]() |
a672b84b88 | ||
![]() |
e3a137c675 | ||
![]() |
10aa99abdc | ||
![]() |
34567d451f | ||
![]() |
494e3dc62c | ||
![]() |
0997274f29 | ||
![]() |
76161329b6 | ||
![]() |
8505750958 | ||
![]() |
4077105db1 | ||
![]() |
3f31d83a55 | ||
![]() |
d729e3c567 | ||
![]() |
9af75f9a43 | ||
![]() |
d32d334a2e | ||
![]() |
94006a843c | ||
![]() |
4790590327 | ||
![]() |
7cf7763e21 | ||
![]() |
0d7979a72f | ||
![]() |
300425e698 | ||
![]() |
59010baf89 | ||
![]() |
47fcb122a2 | ||
![]() |
bbb50b1397 | ||
![]() |
ae8724d699 | ||
![]() |
2169f6979d | ||
![]() |
9cc577e9c7 | ||
![]() |
6ead58f62f | ||
![]() |
ec3118227c | ||
![]() |
0d3d9bc78a | ||
![]() |
e16b3db0d4 | ||
![]() |
cdab874b5b | ||
![]() |
bf40995b16 | ||
![]() |
68b3a4fbb7 | ||
![]() |
c38bfa1101 | ||
![]() |
af7a85eeb7 | ||
![]() |
2bd5dc21a8 | ||
![]() |
18a151c8e8 | ||
![]() |
da19a1a9c6 | ||
![]() |
45cdb5a3e4 |
28
Dockerfile
28
Dockerfile
@@ -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" ]
|
||||
|
12
README.md
12
README.md
@@ -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
1
hassio/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hassio-icons.html
|
10
package.json
10
package.json
@@ -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",
|
||||
|
14
script/docker_entrypoint.sh
Normal file
14
script/docker_entrypoint.sh
Normal 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
103
script/docker_run.sh
Executable 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
|
2
setup.py
2
setup.py
@@ -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',
|
||||
|
@@ -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, "loading")]]">
|
||||
[[localize('ui.panel.page-authorize.form.working')]]:
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "error")]]">
|
||||
[[localize('ui.panel.page-authorize.form.unknown_error')]]:
|
||||
<div class='error'>Error: [[_errorMsg]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "step")]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, "abort")]]">
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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] || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
70
src/common/auth/external_auth.js
Normal file
70
src/common/auth/external_auth.js
Normal 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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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') {
|
||||
|
@@ -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;
|
||||
}
|
@@ -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} %`;
|
||||
|
@@ -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 {
|
||||
|
@@ -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 '';
|
||||
|
@@ -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]}`);
|
||||
|
@@ -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) {
|
||||
|
@@ -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
10
src/data/ws-panels.js
Normal 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
20
src/data/ws-themes.js
Normal 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
10
src/data/ws-user.js
Normal 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
|
||||
);
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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] || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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({
|
||||
|
@@ -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');
|
||||
|
@@ -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 = '/';
|
||||
}
|
||||
};
|
||||
|
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
|
48
src/layouts/ha-init-page.js
Normal file
48
src/layouts/ha-init-page.js
Normal 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);
|
@@ -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);
|
@@ -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
|
||||
|
@@ -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">
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
@@ -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'>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
158
src/panels/mailbox/ha-dialog-show-audio-message.js
Normal file
158
src/panels/mailbox/ha-dialog-show-audio-message.js
Normal 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);
|
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
136
src/panels/profile/ha-long-lived-access-tokens-card.js
Normal file
136
src/panels/profile/ha-long-lived-access-tokens-card.js
Normal 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);
|
@@ -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');
|
||||
}
|
||||
|
106
src/panels/profile/ha-refresh-tokens-card.js
Normal file
106
src/panels/profile/ha-refresh-tokens-card.js
Normal 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);
|
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
9
src/util/fetch-with-auth.js
Normal file
9
src/util/fetch-with-auth.js
Normal 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);
|
||||
};
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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",
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -157,6 +157,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"page-authorize": {
|
||||
"form": {
|
||||
"providers": {
|
||||
"trusted_networks": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "कृपया उस उपयोगकर्ता का चयन करें जिसके रूप में आप लॉगिन करना चाहते हैं:"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": "필수 입력란을 모두 채워주세요"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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"
|
||||
},
|
||||
|
@@ -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"
|
||||
},
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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
Reference in New Issue
Block a user