mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
Merge pull request #6214 from home-assistant/dev
This commit is contained in:
commit
5268afabdb
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200622.0",
|
||||
version="20200623.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -37,6 +37,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"fan",
|
||||
"group",
|
||||
"history_graph",
|
||||
"humidifier",
|
||||
"input_datetime",
|
||||
"light",
|
||||
"lock",
|
||||
@ -79,6 +80,7 @@ export const DOMAINS_TOGGLE = new Set([
|
||||
"switch",
|
||||
"group",
|
||||
"automation",
|
||||
"humidifier",
|
||||
]);
|
||||
|
||||
/** Temperature units. */
|
||||
|
@ -55,6 +55,12 @@ export const computeStateDisplay = (
|
||||
return formatDateTime(date, language);
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
if (stateObj.state === "on" && stateObj.attributes.humidity) {
|
||||
return `${stateObj.attributes.humidity}%`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
|
@ -22,6 +22,7 @@ const fixedIcons = {
|
||||
history_graph: "hass:chart-line",
|
||||
homeassistant: "hass:home-assistant",
|
||||
homekit: "hass:home-automation",
|
||||
humidifier: "hass:air-humidifier",
|
||||
image_processing: "hass:image-filter-frames",
|
||||
input_boolean: "hass:toggle-switch-outline",
|
||||
input_datetime: "hass:calendar-clock",
|
||||
|
@ -8,6 +8,7 @@ export const iconColorCSS = css`
|
||||
ha-icon[data-domain="camera"][data-state="streaming"],
|
||||
ha-icon[data-domain="cover"][data-state="open"],
|
||||
ha-icon[data-domain="fan"][data-state="on"],
|
||||
ha-icon[data-domain="humidifier"][data-state="on"],
|
||||
ha-icon[data-domain="light"][data-state="on"],
|
||||
ha-icon[data-domain="input_boolean"][data-state="on"],
|
||||
ha-icon[data-domain="lock"][data-state="unlocked"],
|
||||
|
@ -22,7 +22,7 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!UNAVAILABLE_STATES.includes(stateObj.state);
|
||||
|
||||
class HaEntityToggle extends LitElement {
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
|
@ -262,6 +262,28 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
pushData(new Date(state.last_changed), series);
|
||||
}
|
||||
});
|
||||
} else if (domain === "humidifier") {
|
||||
addColumn(
|
||||
`${this.hass.localize(
|
||||
"ui.card.humidifier.target_humidity_entity",
|
||||
"name",
|
||||
name
|
||||
)}`,
|
||||
true
|
||||
);
|
||||
addColumn(
|
||||
`${this.hass.localize("ui.card.humidifier.on_entity", "name", name)}`,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
states.states.forEach((state) => {
|
||||
if (!state.attributes) return;
|
||||
const target = safeParseFloat(state.attributes.humidity);
|
||||
const series = [target];
|
||||
series.push(state.state === "on" ? target : null);
|
||||
pushData(new Date(state.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
// Only disable interpolation for sensors
|
||||
const isStep = domain === "sensor";
|
||||
|
@ -5,13 +5,15 @@ import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "water_heater"];
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||
"temperature",
|
||||
"current_temperature",
|
||||
"target_temp_low",
|
||||
"target_temp_high",
|
||||
"hvac_action",
|
||||
"humidity",
|
||||
"mode",
|
||||
];
|
||||
|
||||
export interface LineChartState {
|
||||
@ -224,6 +226,8 @@ export const computeHistory = (
|
||||
unit = hass.config.unit_system.temperature;
|
||||
} else if (computeStateDomain(stateInfo[0]) === "water_heater") {
|
||||
unit = hass.config.unit_system.temperature;
|
||||
} else if (computeStateDomain(stateInfo[0]) === "humidifier") {
|
||||
unit = "%";
|
||||
}
|
||||
|
||||
if (!unit) {
|
||||
|
19
src/data/humidifier.ts
Normal file
19
src/data/humidifier.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export type HumidifierEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
humidity?: number;
|
||||
min_humidity?: number;
|
||||
max_humidity?: number;
|
||||
mode?: string;
|
||||
available_modes?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export const HUMIDIFIER_SUPPORT_MODES = 1;
|
||||
|
||||
export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier";
|
||||
export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier";
|
@ -14,6 +14,7 @@ import "./more-info-default";
|
||||
import "./more-info-fan";
|
||||
import "./more-info-group";
|
||||
import "./more-info-history_graph";
|
||||
import "./more-info-humidifier";
|
||||
import "./more-info-input_datetime";
|
||||
import "./more-info-light";
|
||||
import "./more-info-lock";
|
||||
|
218
src/dialogs/more-info/controls/more-info-humidifier.ts
Normal file
218
src/dialogs/more-info/controls/more-info-humidifier.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-paper-slider";
|
||||
import "../../../components/ha-switch";
|
||||
import {
|
||||
HumidifierEntity,
|
||||
HUMIDIFIER_SUPPORT_MODES,
|
||||
} from "../../../data/humidifier";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
class MoreInfoHumidifier extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HumidifierEntity;
|
||||
|
||||
private _resizeDebounce?: number;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const hass = this.hass;
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
const supportModes = supportsFeature(stateObj, HUMIDIFIER_SUPPORT_MODES);
|
||||
|
||||
const rtlDirection = computeRTLDirection(hass);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"has-modes": supportModes,
|
||||
})}
|
||||
>
|
||||
<div class="container-humidity">
|
||||
<div>${hass.localize("ui.card.humidifier.humidity")}</div>
|
||||
<div class="single-row">
|
||||
<div class="target-humidity">
|
||||
${stateObj.attributes.humidity} %
|
||||
</div>
|
||||
<ha-paper-slider
|
||||
class="humidity"
|
||||
step="1"
|
||||
pin
|
||||
ignore-bar-touch
|
||||
dir=${rtlDirection}
|
||||
.min=${stateObj.attributes.min_humidity}
|
||||
.max=${stateObj.attributes.max_humidity}
|
||||
.secondaryProgress=${stateObj.attributes.max_humidity}
|
||||
.value=${stateObj.attributes.humidity}
|
||||
@change=${this._targetHumiditySliderChanged}
|
||||
>
|
||||
</ha-paper-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${supportModes
|
||||
? html`
|
||||
<div class="container-modes">
|
||||
<ha-paper-dropdown-menu
|
||||
label-float
|
||||
dynamic-align
|
||||
.label=${hass.localize("ui.card.humidifier.mode")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-name"
|
||||
.selected=${stateObj.attributes.mode}
|
||||
@selected-changed=${this._handleModeChanged}
|
||||
>
|
||||
${stateObj.attributes.available_modes!.map(
|
||||
(mode) => html`
|
||||
<paper-item item-name=${mode}>
|
||||
${hass.localize(
|
||||
`state_attributes.humidifier.mode.${mode}`
|
||||
) || mode}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!changedProps.has("stateObj") || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._resizeDebounce) {
|
||||
clearTimeout(this._resizeDebounce);
|
||||
}
|
||||
this._resizeDebounce = window.setTimeout(() => {
|
||||
fireEvent(this, "iron-resize");
|
||||
this._resizeDebounce = undefined;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _targetHumiditySliderChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.humidity,
|
||||
newVal,
|
||||
"set_humidity",
|
||||
{ humidity: newVal }
|
||||
);
|
||||
}
|
||||
|
||||
private _handleModeChanged(ev) {
|
||||
const newVal = ev.detail.value || null;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.mode,
|
||||
newVal,
|
||||
"set_mode",
|
||||
{ mode: newVal }
|
||||
);
|
||||
}
|
||||
|
||||
private async _callServiceHelper(
|
||||
oldVal: unknown,
|
||||
newVal: unknown,
|
||||
service: string,
|
||||
data: {
|
||||
entity_id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
) {
|
||||
if (oldVal === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.entity_id = this.stateObj!.entity_id;
|
||||
const curState = this.stateObj;
|
||||
|
||||
await this.hass.callService("humidifier", service, data);
|
||||
|
||||
// We reset stateObj to re-sync the inputs with the state. It will be out
|
||||
// of sync if our service call did not result in the entity to be turned
|
||||
// on. Since the state is not changing, the resync is not called automatic.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// No need to resync if we received a new state.
|
||||
if (this.stateObj !== curState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stateObj = undefined;
|
||||
await this.updateComplete;
|
||||
// Only restore if not set yet by a state change
|
||||
if (this.stateObj === undefined) {
|
||||
this.stateObj = curState;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-paper-dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-paper-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container-humidity .single-row {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.target-humidity {
|
||||
width: 90px;
|
||||
font-size: 200%;
|
||||
margin: auto;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.humidity {
|
||||
--paper-slider-active-color: var(--paper-blue-400);
|
||||
--paper-slider-secondary-color: var(--paper-blue-400);
|
||||
}
|
||||
|
||||
.single-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-humidifier", MoreInfoHumidifier);
|
@ -22,6 +22,10 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
|
||||
private _haVersion?: string;
|
||||
|
||||
private _hiddenTimeout?: number;
|
||||
|
||||
private _visiblePromiseResolve?: () => void;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
||||
@ -71,6 +75,12 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
super.hassConnected();
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state");
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => this.__handleVisibilityChange(),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
protected hassReconnected() {
|
||||
@ -137,6 +147,33 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
? route.path.substr(1)
|
||||
: route.path.substr(1, dividerPos - 1);
|
||||
}
|
||||
|
||||
private __handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// If the document is hidden, we will prevent reconnects until we are visible again
|
||||
this.hass!.connection.suspendReconnectUntil(
|
||||
new Promise((resolve) => {
|
||||
this._visiblePromiseResolve = resolve;
|
||||
})
|
||||
);
|
||||
// We close the connection to Home Assistant after being hidden for 5 minutes
|
||||
this._hiddenTimeout = window.setTimeout(() => {
|
||||
this._hiddenTimeout = undefined;
|
||||
this.hass!.connection.suspend();
|
||||
}, 300000);
|
||||
} else {
|
||||
// Clear timer to close the connection
|
||||
if (this._hiddenTimeout) {
|
||||
clearTimeout(this._hiddenTimeout);
|
||||
this._hiddenTimeout = undefined;
|
||||
}
|
||||
// Unsuspend the reconnect
|
||||
if (this._visiblePromiseResolve) {
|
||||
this._visiblePromiseResolve();
|
||||
this._visiblePromiseResolve = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
392
src/panels/lovelace/cards/hui-humidifier-card.ts
Normal file
392
src/panels/lovelace/cards/hui-humidifier-card.ts
Normal file
@ -0,0 +1,392 @@
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@thomasloven/round-slider";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
svg,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-card";
|
||||
import { HumidifierEntity } from "../../../data/humidifier";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { HumidifierCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-humidifier-card")
|
||||
export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import(
|
||||
/* webpackChunkName: "hui-humidifier-card-editor" */ "../editor/config-elements/hui-humidifier-card-editor"
|
||||
);
|
||||
return document.createElement("hui-humidifier-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): HumidifierCardConfig {
|
||||
const includeDomains = ["humidifier"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { type: "humidifier", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _config?: HumidifierCardConfig;
|
||||
|
||||
@property() private _setHum?: number;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
public setConfig(config: HumidifierCardConfig): void {
|
||||
if (!config.entity || config.entity.split(".")[0] !== "humidifier") {
|
||||
throw new Error("Specify an entity from within the humidifier domain.");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as HumidifierEntity;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.entity)}
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
const name =
|
||||
this._config!.name ||
|
||||
computeStateName(this.hass!.states[this._config!.entity]);
|
||||
const targetHumidity =
|
||||
stateObj.attributes.humidity !== null &&
|
||||
Number.isFinite(Number(stateObj.attributes.humidity))
|
||||
? stateObj.attributes.humidity
|
||||
: stateObj.attributes.min_humidity;
|
||||
|
||||
const rtlDirection = computeRTLDirection(this.hass);
|
||||
|
||||
const slider = UNAVAILABLE_STATES.includes(stateObj.state)
|
||||
? html` <round-slider disabled="true"></round-slider> `
|
||||
: html`
|
||||
<round-slider
|
||||
.value=${targetHumidity}
|
||||
.min=${stateObj.attributes.min_humidity}
|
||||
.max=${stateObj.attributes.max_humidity}
|
||||
.rtl=${rtlDirection === "rtl"}
|
||||
.step="1"
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setHumidity}
|
||||
></round-slider>
|
||||
`;
|
||||
|
||||
const setValues = svg`
|
||||
<svg viewBox="0 0 40 20">
|
||||
<text
|
||||
x="50%"
|
||||
dx="1"
|
||||
y="60%"
|
||||
text-anchor="middle"
|
||||
style="font-size: 13px;"
|
||||
class="set-value"
|
||||
>
|
||||
${
|
||||
UNAVAILABLE_STATES.includes(stateObj.state) ||
|
||||
this._setHum === undefined ||
|
||||
this._setHum === null
|
||||
? ""
|
||||
: svg`
|
||||
${this._setHum.toFixed()}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
%
|
||||
</tspan>
|
||||
`
|
||||
}
|
||||
</text>
|
||||
</svg>
|
||||
<svg id="set-values">
|
||||
<g>
|
||||
<text
|
||||
dy="22"
|
||||
text-anchor="middle"
|
||||
id="set-mode"
|
||||
>
|
||||
${this.hass!.localize(`state.default.${stateObj.state}`)}
|
||||
${
|
||||
stateObj.attributes.mode &&
|
||||
!UNAVAILABLE_STATES.includes(stateObj.state)
|
||||
? html`
|
||||
-
|
||||
${this.hass!.localize(
|
||||
`state_attributes.humidifier.mode.${stateObj.attributes.mode}`
|
||||
) || stateObj.attributes.mode}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
class="more-info"
|
||||
@click=${this._handleMoreInfo}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
|
||||
<div class="content">
|
||||
<div id="controls">
|
||||
<div id="slider">
|
||||
${slider}
|
||||
<div id="slider-center">
|
||||
<div id="humidity">
|
||||
${setValues}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="info">
|
||||
${name}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
(!changedProps.has("hass") && !changedProps.has("_config"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| HumidifierCardConfig
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
!oldConfig ||
|
||||
oldHass.themes !== this.hass.themes ||
|
||||
oldConfig.theme !== this._config.theme
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oldHass || oldHass.states[this._config.entity] !== stateObj) {
|
||||
this._setHum = this._getSetHum(stateObj);
|
||||
this._rescale_svg();
|
||||
}
|
||||
}
|
||||
|
||||
private _rescale_svg() {
|
||||
// Set the viewbox of the SVG containing the set humidity to perfectly
|
||||
// fit the text
|
||||
// That way it will auto-scale correctly
|
||||
// This is not done to the SVG containing the current humidity, because
|
||||
// it should not be centered on the text, but only on the value
|
||||
if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) {
|
||||
(this.shadowRoot.querySelector(
|
||||
"ha-card"
|
||||
) as LitElement).updateComplete.then(() => {
|
||||
const svgRoot = this.shadowRoot!.querySelector("#set-values");
|
||||
const box = svgRoot!.querySelector("g")!.getBBox();
|
||||
svgRoot!.setAttribute(
|
||||
"viewBox",
|
||||
`${box!.x} ${box!.y} ${box!.width} ${box!.height}`
|
||||
);
|
||||
svgRoot!.setAttribute("width", `${box!.width}`);
|
||||
svgRoot!.setAttribute("height", `${box!.height}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _getSetHum(stateObj: HassEntity): undefined | number {
|
||||
if (UNAVAILABLE_STATES.includes(stateObj.state)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return stateObj.attributes.humidity;
|
||||
}
|
||||
|
||||
private _dragEvent(e): void {
|
||||
this._setHum = e.detail.value;
|
||||
}
|
||||
|
||||
private _setHumidity(e): void {
|
||||
this.hass!.callService("humidifier", "set_humidity", {
|
||||
entity_id: this._config!.entity,
|
||||
humidity: e.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMoreInfo() {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--name-font-size: 1.2rem;
|
||||
--brightness-font-size: 1.2rem;
|
||||
--rail-border-color: transparent;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
color: var(--secondary-text-color);
|
||||
z-index: 25;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#slider {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-width: 250px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
round-slider {
|
||||
--round-slider-path-color: var(--disabled-text-color);
|
||||
--round-slider-bar-color: var(--mode-color);
|
||||
padding-bottom: 10%;
|
||||
}
|
||||
|
||||
#slider-center {
|
||||
position: absolute;
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 100%;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
text-align: center;
|
||||
overflow-wrap: break-word;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#humidity {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#set-values {
|
||||
max-width: 80%;
|
||||
transform: translate(0, -50%);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#set-mode {
|
||||
fill: var(--secondary-text-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#info {
|
||||
display: flex-vertical;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
margin-top: -60px;
|
||||
font-size: var(--name-font-size);
|
||||
}
|
||||
|
||||
#modes > * {
|
||||
color: var(--disabled-text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#modes .selected-icon {
|
||||
color: var(--mode-color);
|
||||
}
|
||||
|
||||
text {
|
||||
fill: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-humidifier-card": HuiHumidifierCard;
|
||||
}
|
||||
}
|
@ -133,6 +133,12 @@ export interface GlanceCardConfig extends LovelaceCardConfig {
|
||||
state_color?: boolean;
|
||||
}
|
||||
|
||||
export interface HumidifierCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
theme?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IframeCardConfig extends LovelaceCardConfig {
|
||||
aspect_ratio?: string;
|
||||
title?: string;
|
||||
|
@ -37,6 +37,7 @@ import { GroupEntity, HomeAssistant } from "../../../types";
|
||||
import {
|
||||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
HumidifierCardConfig,
|
||||
LightCardConfig,
|
||||
PictureEntityCardConfig,
|
||||
ThermostatCardConfig,
|
||||
@ -150,6 +151,12 @@ export const computeCards = (
|
||||
refresh_interval: stateObj.attributes.refresh,
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "humidifier") {
|
||||
const cardConfig: HumidifierCardConfig = {
|
||||
type: "humidifier",
|
||||
entity: entityId,
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "light" && single) {
|
||||
const cardConfig: LightCardConfig = {
|
||||
type: "light",
|
||||
|
@ -38,6 +38,7 @@ const LAZY_LOAD_TYPES = {
|
||||
"empty-state": () => import("../cards/hui-empty-state-card"),
|
||||
starting: () => import("../cards/hui-starting-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
humidifier: () => import("../cards/hui-humidifier-card"),
|
||||
"media-control": () => import("../cards/hui-media-control-card"),
|
||||
"picture-elements": () => import("../cards/hui-picture-elements-card"),
|
||||
"picture-entity": () => import("../cards/hui-picture-entity-card"),
|
||||
|
@ -51,6 +51,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
cover: "cover",
|
||||
fan: "toggle",
|
||||
group: "group",
|
||||
humidifier: "toggle",
|
||||
input_boolean: "toggle",
|
||||
input_number: "input-number",
|
||||
input_select: "input-select",
|
||||
|
@ -0,0 +1,117 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HumidifierCardConfig } from "../../cards/types";
|
||||
import { struct } from "../../common/structs/struct";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const cardConfigStruct = struct({
|
||||
type: "string",
|
||||
entity: "string",
|
||||
name: "string?",
|
||||
theme: "string?",
|
||||
});
|
||||
|
||||
const includeDomains = ["humidifier"];
|
||||
|
||||
@customElement("hui-humidifier-card-editor")
|
||||
export class HuiHumidifierCardEditor extends LitElement
|
||||
implements LovelaceCardEditor {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _config?: HumidifierCardConfig;
|
||||
|
||||
public setConfig(config: HumidifierCardConfig): void {
|
||||
config = cardConfigStruct(config);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
get _entity(): string {
|
||||
return this._config!.entity || "";
|
||||
}
|
||||
|
||||
get _name(): string {
|
||||
return this._config!.name || "";
|
||||
}
|
||||
|
||||
get _theme(): string {
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${configElementStyle}
|
||||
<div class="card-config">
|
||||
<ha-entity-picker
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.entity"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.value="${this._entity}"
|
||||
.configValue=${"entity"}
|
||||
.includeDomains=${includeDomains}
|
||||
@change="${this._valueChanged}"
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.name"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value="${this._name}"
|
||||
.configValue="${"name"}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></paper-input>
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value="${this._theme}"
|
||||
.configValue="${"theme"}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></hui-theme-select-editor>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: EntitiesEditorEvent): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
const target = ev.target! as EditorTarget;
|
||||
|
||||
if (this[`_${target.configValue}`] === target.value) {
|
||||
return;
|
||||
}
|
||||
if (target.configValue) {
|
||||
if (target.value === "") {
|
||||
delete this._config[target.configValue!];
|
||||
} else {
|
||||
this._config = { ...this._config, [target.configValue!]: target.value };
|
||||
}
|
||||
}
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-humidifier-card-editor": HuiHumidifierCardEditor;
|
||||
}
|
||||
}
|
@ -29,6 +29,10 @@ export const coreCards: Card[] = [
|
||||
type: "history-graph",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "humidifier",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "light",
|
||||
showElement: true,
|
||||
|
@ -12,7 +12,6 @@ import SidebarMixin from "./sidebar-mixin";
|
||||
import ThemesMixin from "./themes-mixin";
|
||||
import TranslationsMixin from "./translations-mixin";
|
||||
import { urlSyncMixin } from "./url-sync-mixin";
|
||||
import { suspendMixin } from "./suspend-mixin";
|
||||
|
||||
const ext = <T extends Constructor>(baseClass: T, mixins): T =>
|
||||
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
|
||||
@ -25,7 +24,6 @@ export class HassElement extends ext(HassBaseEl, [
|
||||
SidebarMixin,
|
||||
DisconnectToastMixin,
|
||||
connectionMixin,
|
||||
suspendMixin,
|
||||
NotificationMixin,
|
||||
dialogManagerMixin,
|
||||
urlSyncMixin,
|
||||
|
@ -1,48 +0,0 @@
|
||||
import { Constructor } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export const suspendMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
private __hiddenTimeout?: number;
|
||||
|
||||
private __visiblePromiseResolve?: () => void;
|
||||
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => this.__handleVisibilityChange(),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private __handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
// If the document is hidden, we will prevent reconnects until we are visible again
|
||||
this.hass!.connection.suspendReconnectUntil(
|
||||
new Promise((resolve) => {
|
||||
this.__visiblePromiseResolve = resolve;
|
||||
})
|
||||
);
|
||||
// We close the connection to Home Assistant after being hidden for 5 minutes
|
||||
this.__hiddenTimeout = window.setTimeout(() => {
|
||||
this.__hiddenTimeout = undefined;
|
||||
this.hass!.connection.suspend();
|
||||
}, 300000);
|
||||
} else {
|
||||
// Clear timer to close the connection
|
||||
if (this.__hiddenTimeout) {
|
||||
clearTimeout(this.__hiddenTimeout);
|
||||
this.__hiddenTimeout = undefined;
|
||||
}
|
||||
// Unsuspend the reconnect
|
||||
if (this.__visiblePromiseResolve) {
|
||||
this.__visiblePromiseResolve();
|
||||
this.__visiblePromiseResolve = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -44,6 +44,19 @@
|
||||
"idle": "Idle",
|
||||
"fan": "Fan"
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"mode": {
|
||||
"normal": "Normal",
|
||||
"eco": "Eco",
|
||||
"away": "Away",
|
||||
"boost": "Boost",
|
||||
"comfort": "Comfort",
|
||||
"home": "Home",
|
||||
"sleep": "Sleep",
|
||||
"auto": "Auto",
|
||||
"baby": "Baby"
|
||||
}
|
||||
}
|
||||
},
|
||||
"state_badge": {
|
||||
@ -146,6 +159,12 @@
|
||||
"forward": "Forward",
|
||||
"reverse": "Reverse"
|
||||
},
|
||||
"humidifier": {
|
||||
"humidity": "Target humidity",
|
||||
"mode": "Mode",
|
||||
"target_humidity_entity": "{name} target humidity",
|
||||
"on_entity": "{name} on"
|
||||
},
|
||||
"light": {
|
||||
"brightness": "Brightness",
|
||||
"color_temperature": "Color temperature",
|
||||
@ -1935,6 +1954,10 @@
|
||||
"name": "Horizontal Stack",
|
||||
"description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column."
|
||||
},
|
||||
"humidifier": {
|
||||
"name": "Humidifier",
|
||||
"description": "The Humidifier card gives control of your humidifier entity. Allowing you to change the humidity and mode of the entity."
|
||||
},
|
||||
"iframe": {
|
||||
"name": "Webpage",
|
||||
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant."
|
||||
|
@ -37,6 +37,7 @@ hassAttributeUtil.DOMAIN_DEVICE_CLASS = {
|
||||
"shutter",
|
||||
"window",
|
||||
],
|
||||
humidifier: ["dehumidifier", "humidifier"],
|
||||
sensor: [
|
||||
"battery",
|
||||
"humidity",
|
||||
@ -89,7 +90,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU
|
||||
type: "array",
|
||||
options: hassAttributeUtil.DOMAIN_DEVICE_CLASS,
|
||||
description: "Device class",
|
||||
domains: ["binary_sensor", "cover", "sensor", "switch"],
|
||||
domains: ["binary_sensor", "cover", "humidifier", "sensor", "switch"],
|
||||
},
|
||||
hidden: { type: "boolean", description: "Hide from UI" },
|
||||
assumed_state: {
|
||||
@ -100,6 +101,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU
|
||||
"cover",
|
||||
"climate",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"group",
|
||||
"water_heater",
|
||||
],
|
||||
|
@ -506,6 +506,11 @@
|
||||
"clear": "Ryd",
|
||||
"show_areas": "Vis områder"
|
||||
},
|
||||
"date-range-picker": {
|
||||
"end_date": "Slutdato",
|
||||
"select": "Vælg",
|
||||
"start_date": "Startdato"
|
||||
},
|
||||
"device-picker": {
|
||||
"clear": "Ryd",
|
||||
"device": "Enhed",
|
||||
@ -695,6 +700,7 @@
|
||||
"zha_device_info": {
|
||||
"buttons": {
|
||||
"add": "Tilføj enheder",
|
||||
"clusters": "Administrer klynger",
|
||||
"reconfigure": "Genkonfigurer enhed",
|
||||
"remove": "Fjern enhed",
|
||||
"zigbee_information": "Zigbee-oplysninger"
|
||||
@ -1558,6 +1564,7 @@
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"button": "Konfigurer",
|
||||
"description_listen": "Lyt til et emne",
|
||||
"description_publish": "Udsend en pakke",
|
||||
"listening_to": "Lytter til",
|
||||
@ -2008,11 +2015,23 @@
|
||||
},
|
||||
"history": {
|
||||
"period": "Periode",
|
||||
"ranges": {
|
||||
"last_week": "Sidste uge",
|
||||
"this_week": "Denne uge",
|
||||
"today": "I dag",
|
||||
"yesterday": "I går"
|
||||
},
|
||||
"showing_entries": "Viser poster for"
|
||||
},
|
||||
"logbook": {
|
||||
"entries_not_found": "Der blev ikke fundet nogen logbogsposter.",
|
||||
"period": "Periode",
|
||||
"ranges": {
|
||||
"last_week": "Sidste uge",
|
||||
"this_week": "Denne uge",
|
||||
"today": "I dag",
|
||||
"yesterday": "I går"
|
||||
},
|
||||
"showing_entries": "Viser poster for"
|
||||
},
|
||||
"lovelace": {
|
||||
|
@ -506,6 +506,11 @@
|
||||
"clear": "Tøm",
|
||||
"show_areas": "Vis områder"
|
||||
},
|
||||
"date-range-picker": {
|
||||
"end_date": "Sluttdato",
|
||||
"select": "Velg",
|
||||
"start_date": "Startdato"
|
||||
},
|
||||
"device-picker": {
|
||||
"clear": "Tøm",
|
||||
"device": "Enhet",
|
||||
@ -695,6 +700,7 @@
|
||||
"zha_device_info": {
|
||||
"buttons": {
|
||||
"add": "Legg til enheter via denne enheten",
|
||||
"clusters": "Behandle Clusters",
|
||||
"reconfigure": "Rekonfigurer enhet",
|
||||
"remove": "Fjern enhet",
|
||||
"zigbee_information": "Zigbee-enhetssignatur"
|
||||
@ -1558,6 +1564,7 @@
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"button": "Konfigurer",
|
||||
"description_listen": "Lytt til et emne",
|
||||
"description_publish": "Publiser en pakke",
|
||||
"listening_to": "Lytter til",
|
||||
@ -1678,11 +1685,11 @@
|
||||
"core": "Last inn lokasjon og spesialtilpassinger på nytt",
|
||||
"group": "Last inn grupper på nytt",
|
||||
"heading": "YAML -Konfigurasjon lastes på nytt",
|
||||
"input_boolean": "Last input booleans på nytt",
|
||||
"input_datetime": "Last input date på nytt",
|
||||
"input_number": "Las input numbers på nytt",
|
||||
"input_select": "Last input selects på nytt ",
|
||||
"input_text": "Last input texts på nytt",
|
||||
"input_boolean": "Last inn bolsk inndata på nytt",
|
||||
"input_datetime": "Last inn dato inndata på nytt",
|
||||
"input_number": "Last inn nummer inndata på nytt",
|
||||
"input_select": "Last inn valg inndata på nytt ",
|
||||
"input_text": "Last inn tekst inndata på nytt",
|
||||
"introduction": "Noen deler av Home Assistant kan laste inn uten å kreve omstart. Hvis du trykker last på nytt, vil du bytte den nåværende konfigurasjonen med den nye.",
|
||||
"person": "Last inn personer på nytt",
|
||||
"scene": "Last inn scener på nytt",
|
||||
@ -1742,14 +1749,14 @@
|
||||
"system": ""
|
||||
}
|
||||
},
|
||||
"users_privileges_note": "Brukere-gruppen er et pågående arbeid. Brukeren kan ikke administrere forekomsten via brukergrensesnittet. Vi overvåker fortsatt alle API-endepunkter for administrasjonsadministrasjon for å sikre at de begrenser tilgangen til administratorer på riktig måte."
|
||||
"users_privileges_note": "Brukere-gruppen er et pågående arbeid. Brukeren kan ikke administrere forekomsten via brukergrensesnittet. Vi reviderer fortsatt alle API-endepunkter for å sikre at de begrenser tilgangen til administratorer på riktig måte."
|
||||
},
|
||||
"zha": {
|
||||
"add_device_page": {
|
||||
"discovered_text": "Enheter vises her når de er oppdaget.",
|
||||
"discovery_text": "Oppdagede enheter vises her. Følg instruksjonene for enheten(e) og sett enheten(e) i paringsmodus.",
|
||||
"header": "Zigbee Home Automation - Legg til enheter",
|
||||
"no_devices_found": "Ingen enheter er funnet, sørg for at de er i paringsmodus og holde dem våken mens du oppdager kjører.",
|
||||
"no_devices_found": "Ingen enheter ble funnet, sørg for at de er i paringsmodus og holde dem våken mens oppdagelse pågår.",
|
||||
"pairing_mode": "Kontroller at enhetene er i paringsmodus. Sjekk instruksjonene til enheten om hvordan du gjør dette.",
|
||||
"search_again": "Søk på nytt",
|
||||
"spinner": "Søker etter ZHA Zigbee-enheter..."
|
||||
@ -2035,11 +2042,23 @@
|
||||
},
|
||||
"history": {
|
||||
"period": "Periode",
|
||||
"ranges": {
|
||||
"last_week": "Forrige uke",
|
||||
"this_week": "Denne uken",
|
||||
"today": "I dag",
|
||||
"yesterday": "I går"
|
||||
},
|
||||
"showing_entries": "Viser oppføringer for"
|
||||
},
|
||||
"logbook": {
|
||||
"entries_not_found": "Finner ingen loggbokoppføringer.",
|
||||
"period": "Periode",
|
||||
"ranges": {
|
||||
"last_week": "Forrige uke",
|
||||
"this_week": "Denne uken",
|
||||
"today": "I dag",
|
||||
"yesterday": "I går"
|
||||
},
|
||||
"showing_entries": "Viser oppføringer for"
|
||||
},
|
||||
"lovelace": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user