v0.14.21
This commit is contained in:
@@ -1664,7 +1664,7 @@ button.outside-pill:focus-visible {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.outdoor-history-links > div {
|
||||
.outdoor-history-links>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
@@ -2285,7 +2285,9 @@ button.outside-pill:focus-visible {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.custom-chart-edit-note strong { color: var(--text-soft); }
|
||||
.custom-chart-edit-note strong {
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.saved-chart-section {
|
||||
display: grid;
|
||||
@@ -2301,7 +2303,9 @@ button.outside-pill:focus-visible {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.saved-chart-heading small { color: var(--muted); }
|
||||
.saved-chart-heading small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.saved-chart-list {
|
||||
display: grid;
|
||||
@@ -2939,7 +2943,9 @@ button.outside-pill:focus-visible {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.history-network-controls label { min-width: 0; }
|
||||
.history-network-controls label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-jitter-toggle {
|
||||
min-height: 45px;
|
||||
@@ -4507,8 +4513,6 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Dashboard thermostat cards keep their intrinsic height when one zone shows
|
||||
a local/manual takeover panel. Other grids retain equal-height rows. */
|
||||
#dashboardZones {
|
||||
grid-auto-rows: auto;
|
||||
align-items: start;
|
||||
@@ -9029,13 +9033,25 @@ body.flow-editor-open {
|
||||
}
|
||||
|
||||
@keyframes cloud-command-pulse {
|
||||
from { opacity: .72; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloud-command-dot {
|
||||
from { transform: scale(.65); opacity: .45; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
from {
|
||||
transform: scale(.65);
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9367,7 +9383,7 @@ body.flow-editor-open {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
/* Notification delivery audit in Events. */
|
||||
|
||||
.log-row .kind .log-notification-badge {
|
||||
margin-left: .45rem;
|
||||
padding: 3px 7px;
|
||||
@@ -9380,7 +9396,6 @@ body.flow-editor-open {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Keep the Flow footer compact before the full mobile layout kicks in. */
|
||||
@media (min-width:761px) and (max-width:1200px) {
|
||||
.flow-natural-preview {
|
||||
display: block;
|
||||
@@ -9450,14 +9465,13 @@ body.flow-editor-open {
|
||||
}
|
||||
}
|
||||
|
||||
/* GREE Cloud runtime summary */
|
||||
.cloud-runtime-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cloud-runtime-summary > div {
|
||||
.cloud-runtime-summary>div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
@@ -9519,7 +9533,6 @@ body.flow-editor-open {
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared alerts and split/multisplit installation configuration */
|
||||
.inline-alert {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
@@ -9550,18 +9563,30 @@ body.flow-editor-open {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.inline-alert.error strong { color: var(--danger); }
|
||||
.inline-alert.error strong {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.inline-alert.success {
|
||||
border-color: color-mix(in srgb, var(--accent) 42%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||
}
|
||||
|
||||
.inline-alert.info {
|
||||
border-color: color-mix(in srgb, var(--info) 38%, var(--line));
|
||||
background: color-mix(in srgb, var(--info) 9%, var(--surface));
|
||||
}
|
||||
.inline-alert.wide { grid-column: 1 / -1; }
|
||||
|
||||
.device-installation-summary { display: grid; gap: 8px; margin-bottom: 12px; }
|
||||
.inline-alert.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.device-installation-summary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.installation-summary-card,
|
||||
.installation-list-row {
|
||||
display: grid;
|
||||
@@ -9573,36 +9598,106 @@ body.flow-editor-open {
|
||||
border-radius: 12px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
.installation-summary-card > div,
|
||||
.installation-list-row > button:first-child { display: grid; gap: 3px; }
|
||||
|
||||
.installation-summary-card>div,
|
||||
.installation-list-row>button:first-child {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.installation-summary-card strong,
|
||||
.installation-list-row strong { color: var(--text); }
|
||||
.installation-list-row strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.installation-summary-card small,
|
||||
.installation-list-row small { color: var(--muted); }
|
||||
.installation-list { display: grid; gap: 8px; }
|
||||
.installation-list-row > button:first-child { border: 0; background: transparent; padding: 0; text-align: left; }
|
||||
.installation-list-row > div { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
||||
.installation-form { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
|
||||
.installation-device-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin-top: 6px; }
|
||||
.installation-device-choices .check { margin: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); }
|
||||
.installation-device-choices .check.disabled { opacity: .55; }
|
||||
.installation-list-row small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.installation-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.installation-list-row>button:first-child {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.installation-list-row>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.installation-form {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.installation-device-choices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.installation-device-choices .check {
|
||||
margin: 0;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.installation-device-choices .check.disabled {
|
||||
opacity: .55;
|
||||
}
|
||||
|
||||
.installation-energy-metric b {
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
|
||||
.history-context-controls.energy-context-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 1.5fr) repeat(3, minmax(132px, .8fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.history-energy-targets { display: grid; gap: 7px; min-width: 0; color: var(--muted); font-size: 12px; font-weight: 650; }
|
||||
|
||||
.history-energy-targets {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.history-energy-targets small,
|
||||
.history-range-control small { display: block; color: var(--muted); font-size: 10px; font-weight: 550; line-height: 1.35; }
|
||||
.history-energy-picker { position: relative; min-width: 0; }
|
||||
.history-energy-picker > summary {
|
||||
.history-range-control small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 550;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.history-energy-picker {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-energy-picker>summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
@@ -9616,9 +9711,18 @@ body.flow-editor-open {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.history-energy-picker > summary::-webkit-details-marker { display: none; }
|
||||
.history-energy-picker > summary span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.history-energy-picker > summary b {
|
||||
|
||||
.history-energy-picker>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.history-energy-picker>summary span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-energy-picker>summary b {
|
||||
min-width: 34px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 999px;
|
||||
@@ -9627,7 +9731,13 @@ body.flow-editor-open {
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.history-energy-picker[open] > summary { border-color: var(--accent); outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); outline-offset: 1px; }
|
||||
|
||||
.history-energy-picker[open]>summary {
|
||||
border-color: var(--accent);
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.history-energy-options {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
@@ -9642,6 +9752,7 @@ body.flow-editor-open {
|
||||
background: var(--surface);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, .22);
|
||||
}
|
||||
|
||||
.history-energy-option {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
@@ -9655,28 +9766,67 @@ body.flow-editor-open {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.history-energy-option:hover { background: var(--surface-muted); }
|
||||
.history-energy-option input { width: 17px; min-height: 17px; margin: 0; padding: 0; accent-color: var(--accent); }
|
||||
.history-energy-option span { overflow-wrap: anywhere; }
|
||||
.history-range-control.energy-period-control { min-width: 0; }
|
||||
|
||||
.history-energy-option:hover {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.history-energy-option input {
|
||||
width: 17px;
|
||||
min-height: 17px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.history-energy-option span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history-range-control.energy-period-control {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
|
||||
.installation-summary-card,
|
||||
.installation-list-row { grid-template-columns: 1fr; }
|
||||
.installation-list-row > div { justify-content: flex-start; }
|
||||
.installation-device-choices { grid-template-columns: 1fr; }
|
||||
.history-context-controls.energy-context-controls { grid-template-columns: 1fr; }
|
||||
.history-toolbar-panel .chart-toolbar.energy-toolbar .history-refresh-button { margin-top: 0; }
|
||||
.history-energy-options { width: min(420px, calc(100vw - 40px)); max-height: 46vh; }
|
||||
.installation-list-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.installation-list-row>div {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.installation-device-choices {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-context-controls.energy-context-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-toolbar-panel .chart-toolbar.energy-toolbar .history-refresh-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.history-energy-options {
|
||||
width: min(420px, calc(100vw - 40px));
|
||||
max-height: 46vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.history-network-controls { grid-template-columns: 1fr; }
|
||||
.history-jitter-toggle { width: 100%; }
|
||||
.history-network-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-jitter-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unified custom selects -------------------------------------------------- */
|
||||
.select-native-proxy {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
@@ -9865,8 +10015,8 @@ body.flow-editor-open {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.has-error > .custom-select .custom-select-trigger,
|
||||
select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-trigger {
|
||||
.has-error>.custom-select .custom-select-trigger,
|
||||
select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-trigger {
|
||||
border-color: color-mix(in srgb, var(--danger) 68%, var(--line));
|
||||
outline: 2px solid color-mix(in srgb, var(--danger) 13%, transparent);
|
||||
}
|
||||
@@ -9877,13 +10027,26 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px), (orientation: landscape) and (max-height: 700px) {
|
||||
.flow-fit-button-icon { display: inline-grid; place-items: center; }
|
||||
.flow-fit-button-label { display: none; }
|
||||
@media (max-width: 900px),
|
||||
(orientation: landscape) and (max-height: 700px) {
|
||||
.flow-fit-button-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.flow-fit-button-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 700px) and (min-width: 500px) {
|
||||
.flow-fit-button-icon { display: none; }
|
||||
.flow-fit-button-label { display: inline; }
|
||||
.flow-fit-button-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flow-fit-button-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.ha-supervisor-status {
|
||||
@@ -9896,11 +10059,27 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.ha-supervisor-status strong { font-size: .92rem; }
|
||||
.ha-supervisor-status span { color: var(--muted); font-size: .82rem; line-height: 1.45; }
|
||||
.ha-supervisor-status.success { border-left-color: var(--teal); }
|
||||
.ha-supervisor-status.warning { border-left-color: var(--accent); }
|
||||
.ha-supervisor-status.error { border-left-color: var(--danger); }
|
||||
.ha-supervisor-status strong {
|
||||
font-size: .92rem;
|
||||
}
|
||||
|
||||
.ha-supervisor-status span {
|
||||
color: var(--muted);
|
||||
font-size: .82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ha-supervisor-status.success {
|
||||
border-left-color: var(--teal);
|
||||
}
|
||||
|
||||
.ha-supervisor-status.warning {
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.ha-supervisor-status.error {
|
||||
border-left-color: var(--danger);
|
||||
}
|
||||
|
||||
.ha-entity-picker {
|
||||
display: grid;
|
||||
@@ -9908,8 +10087,13 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ha-entity-picker > label { margin: 0; }
|
||||
.ha-entity-picker > .field-note { margin: 0; }
|
||||
.ha-entity-picker>label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ha-entity-picker>.field-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ha-entity-results {
|
||||
display: grid;
|
||||
@@ -9942,7 +10126,7 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.ha-entity-option > span {
|
||||
.ha-entity-option>span {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
@@ -9962,15 +10146,26 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
font-size: .76rem;
|
||||
}
|
||||
|
||||
.ha-entity-option small { flex: 0 0 auto; }
|
||||
.ha-entity-empty { padding: 9px 10px; }
|
||||
.ha-entity-option small {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Shared Flow HA entity picker: keep search and selected entity visually distinct. */
|
||||
.ha-entity-search-field input[type="search"] { width: 100%; }
|
||||
.ha-entity-selected-field { margin-top: 2px; }
|
||||
.ha-entity-selected-field input { font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); }
|
||||
.ha-entity-empty {
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.ha-entity-search-field input[type="search"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ha-entity-selected-field {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.ha-entity-selected-field input {
|
||||
font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||
}
|
||||
|
||||
/* Standalone public custom chart */
|
||||
.public-custom-chart-page {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
@@ -10003,7 +10198,9 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.public-chart-heading { min-width: 0; }
|
||||
.public-chart-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.public-chart-range-control {
|
||||
display: grid;
|
||||
@@ -10013,7 +10210,9 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.public-chart-range-control select { min-width: 126px; }
|
||||
.public-chart-range-control select {
|
||||
min-width: 126px;
|
||||
}
|
||||
|
||||
.public-custom-chart-wrap {
|
||||
flex: 1 1 0;
|
||||
@@ -10037,7 +10236,9 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.public-custom-chart-card .legend::-webkit-scrollbar { display: none; }
|
||||
.public-custom-chart-card .legend::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.public-chart-legend-item {
|
||||
cursor: default;
|
||||
@@ -10056,9 +10257,23 @@ select.select-native-proxy[aria-invalid="true"] + .custom-select .custom-select-
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.public-custom-chart-shell { padding: 8px; }
|
||||
.public-custom-chart-card { padding: 12px; }
|
||||
.public-chart-title-row { align-items: center; }
|
||||
.public-chart-range-control select { min-width: 108px; }
|
||||
.public-custom-chart-wrap { min-height: 140px; }
|
||||
}
|
||||
.public-custom-chart-shell {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.public-custom-chart-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.public-chart-title-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.public-chart-range-control select {
|
||||
min-width: 108px;
|
||||
}
|
||||
|
||||
.public-custom-chart-wrap {
|
||||
min-height: 140px;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
@@ -8,8 +9,10 @@
|
||||
<link rel="icon" href="__GREE_BASE_PATH__/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="__GREE_STYLES_ASSET__">
|
||||
<script src="__GREE_THEME_INIT_ASSET__"></script>
|
||||
<script src="__GREE_LANG_INIT_ASSET__"></script>
|
||||
<title>GREE Controller · Custom chart</title>
|
||||
</head>
|
||||
|
||||
<body class="public-custom-chart-page">
|
||||
<main class="public-custom-chart-shell">
|
||||
<section class="panel chart-panel chart-card public-custom-chart-card" aria-live="polite">
|
||||
@@ -41,4 +44,5 @@
|
||||
</main>
|
||||
<script src="__GREE_CUSTOM_CHART_ASSET__"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
+280
-127
@@ -31,7 +31,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-picker" data-i18n-title="controls.theme" title="Theme">
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" data-ui-icon="system" aria-hidden="true"></span>
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" data-ui-icon="system"
|
||||
aria-hidden="true"></span>
|
||||
<span class="toolbar-picker-chevron" data-ui-icon="chevron-down" aria-hidden="true"></span>
|
||||
<select class="toolbar-picker-select" id="themeSelect" data-i18n-aria="controls.theme" aria-label="Theme">
|
||||
<option value="system" data-i18n="theme.system">System</option>
|
||||
@@ -146,7 +147,11 @@
|
||||
<div><span class="eyebrow" data-i18n="devices.localCloud">Local / GREE Cloud</span>
|
||||
<h1 data-i18n="nav.devices">Devices</h1>
|
||||
</div>
|
||||
<div class="section-heading-actions"><button class="primary compact" id="discoverButton" data-i18n="devices.discoverLocal">Discover Local</button><button class="secondary" type="button" id="cloudDiscoverButton">GREE Cloud</button><button class="secondary" type="button" id="deviceGroupsButton" data-i18n="devices.installations">Split / multisplit installations</button><button class="secondary" data-open="deviceDialog" data-i18n="devices.addLocalManual">Add Local manually</button></div>
|
||||
<div class="section-heading-actions"><button class="primary compact" id="discoverButton"
|
||||
data-i18n="devices.discoverLocal">Discover Local</button><button class="secondary" type="button"
|
||||
id="cloudDiscoverButton">GREE Cloud</button><button class="secondary" type="button" id="deviceGroupsButton"
|
||||
data-i18n="devices.installations">Split / multisplit installations</button><button class="secondary"
|
||||
data-open="deviceDialog" data-i18n="devices.addLocalManual">Add Local manually</button></div>
|
||||
</div>
|
||||
<p class="lead" data-i18n="devices.technicalDescription">Technical configuration and diagnostics only. Current
|
||||
operating settings are available in Manual control.</p>
|
||||
@@ -334,12 +339,13 @@
|
||||
<div class="panel chart-panel history-toolbar-panel">
|
||||
<div class="chart-toolbar">
|
||||
<div id="historyContextControls" class="history-context-controls"></div>
|
||||
<label class="history-range-control" id="historyRangeControl"><span data-i18n="history.range">Range</span><select id="historyHours"
|
||||
data-i18n-aria="history.range" aria-label="History range">
|
||||
<label class="history-range-control" id="historyRangeControl"><span
|
||||
data-i18n="history.range">Range</span><select id="historyHours" data-i18n-aria="history.range"
|
||||
aria-label="History range">
|
||||
<option value="6" data-i18n="history.6h">6 hours</option>
|
||||
<option value="24" selected data-i18n="history.24h">24 hours</option>
|
||||
<option value="24" data-i18n="history.24h">24 hours</option>
|
||||
<option value="168" data-i18n="history.7d">7 days</option>
|
||||
<option value="720" data-i18n="history.30d">30 days</option>
|
||||
<option value="720" selected data-i18n="history.30d">30 days</option>
|
||||
<option value="2160" data-i18n="history.90d">90 days</option>
|
||||
<option value="8760" data-i18n="history.1y">1 year</option>
|
||||
</select><small id="historyRangeHint" hidden></small></label>
|
||||
@@ -414,19 +420,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div id="haSupervisorStatus" class="ha-supervisor-status wide" role="status" aria-live="polite" hidden></div>
|
||||
<div id="haSupervisorStatus" class="ha-supervisor-status wide" role="status" aria-live="polite" hidden>
|
||||
</div>
|
||||
<label class="wide" data-ha-manual-field><span>URL</span><input type="url" name="ha_url"
|
||||
placeholder="http://homeassistant.local:8123"></label>
|
||||
<label class="wide" data-ha-manual-field><span data-i18n="settings.haLongLivedToken">Long-Lived Access Token</span><input
|
||||
type="password" name="ha_token" autocomplete="new-password" data-i18n-placeholder="settings.haTokenKeep"
|
||||
placeholder="Leave empty to keep the saved token"></label>
|
||||
<label class="wide" data-ha-manual-field><span data-i18n="settings.haLongLivedToken">Long-Lived Access
|
||||
Token</span><input type="password" name="ha_token" autocomplete="new-password"
|
||||
data-i18n-placeholder="settings.haTokenKeep" placeholder="Leave empty to keep the saved token"></label>
|
||||
<label class="check wide" data-ha-manual-field><input type="checkbox" name="ha_allow_invalid_tls"> <span
|
||||
data-i18n="settings.allowInvalidTls">Allow invalid/self-signed HTTPS certificate</span></label>
|
||||
<p class="field-note wide warning-note" data-ha-manual-field data-i18n="settings.allowInvalidTlsHint">Use only for a trusted
|
||||
<p class="field-note wide warning-note" data-ha-manual-field data-i18n="settings.allowInvalidTlsHint">Use
|
||||
only for a trusted
|
||||
local Home Assistant server.</p>
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="haUseSupervisor"
|
||||
data-i18n="settings.haUseSupervisor" hidden>Use Supervisor automatically</button><button type="button" class="secondary" id="haTest"
|
||||
data-i18n="settings.testHa">Test HA</button></div>
|
||||
data-i18n="settings.haUseSupervisor" hidden>Use Supervisor automatically</button><button type="button"
|
||||
class="secondary" id="haTest" data-i18n="settings.testHa">Test HA</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -439,9 +447,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="wide"><span data-i18n="settings.outdoorEntity">Global outdoor temperature entity_id</span><input
|
||||
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
|
||||
<p class="field-note wide" data-i18n="settings.outdoorEntityHint">Global Home Assistant outdoor temperature sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.</p>
|
||||
<label class="wide"><span data-i18n="settings.outdoorEntity">Global outdoor temperature
|
||||
entity_id</span><input name="ha_outdoor_entity_id" list="haEntitySuggestions"
|
||||
placeholder="sensor.outdoor_temperature"></label>
|
||||
<p class="field-note wide" data-i18n="settings.outdoorEntityHint">Global Home Assistant outdoor temperature
|
||||
sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.</p>
|
||||
<label class="wide"><span data-i18n="settings.haSensorStaleAfterMinutes">Maximum HA sensor reading age
|
||||
(min)</span><input type="number" name="ha_sensor_stale_after_minutes" min="1" max="1440" step="1"
|
||||
value="5"></label>
|
||||
@@ -475,9 +485,10 @@
|
||||
</div>
|
||||
<div class="sensor-alias-manager">
|
||||
<div id="sensorAliasList" class="sensor-alias-list"></div>
|
||||
<div class="sensor-alias-add"><input id="sensorAliasEntity" list="haEntitySuggestions" placeholder="sensor.gabinet_temperature"><input
|
||||
id="sensorAliasName" data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button
|
||||
type="button" class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
|
||||
<div class="sensor-alias-add"><input id="sensorAliasEntity" list="haEntitySuggestions"
|
||||
placeholder="sensor.gabinet_temperature"><input id="sensorAliasName"
|
||||
data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button type="button"
|
||||
class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -587,7 +598,8 @@
|
||||
<label class="check"><input type="checkbox" name="notification_alert_automation"> <span
|
||||
data-i18n="notifications.alertAutomation">Automation errors and conflicts</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_sensor_discrepancy"> <span
|
||||
data-i18n="notifications.alertSensorDiscrepancy">GREE vs Home Assistant temperature difference</span></label>
|
||||
data-i18n="notifications.alertSensorDiscrepancy">GREE vs Home Assistant temperature
|
||||
difference</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_control_errors"> <span
|
||||
data-i18n="notifications.alertControlErrors">Thermostat and group control errors</span></label>
|
||||
<label class="check"><input type="checkbox" name="notification_alert_important_events"> <span
|
||||
@@ -652,6 +664,9 @@
|
||||
autocomplete="new-password" data-i18n-placeholder="settings.secretKeep"
|
||||
placeholder="Leave empty to keep saved secret"></label>
|
||||
</div>
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="testInfluxDbConnection"
|
||||
data-i18n="settings.influxTest">Test InfluxDB connection</button></div>
|
||||
<div id="influxDbTestResult" class="inline-alert wide" role="status" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel form-section">
|
||||
@@ -714,14 +729,19 @@
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.connectivityTitle">Connectivity metrics</h3>
|
||||
<p data-i18n="settings.connectivityHint">Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.</p>
|
||||
<p data-i18n="settings.connectivityHint">Periodic GREE UDP round-trip measurements for Local/LAN units.
|
||||
Results are stored in History.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="ping_metrics_enabled"> <span data-i18n="settings.connectivityEnable">Enable local unit ping measurements</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="ping_interval_seconds" min="10" max="3600" value="60"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="ping_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.connectivityLocalOnly">Only Local/LAN installations are probed. Cloud devices are not pinged directly.</p>
|
||||
<label class="check wide"><input type="checkbox" name="ping_metrics_enabled"> <span
|
||||
data-i18n="settings.connectivityEnable">Enable local unit ping measurements</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number"
|
||||
name="ping_interval_seconds" min="10" max="3600" value="60"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number"
|
||||
name="ping_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.connectivityLocalOnly">Only Local/LAN installations are
|
||||
probed. Cloud devices are not pinged directly.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel form-section">
|
||||
@@ -757,33 +777,70 @@
|
||||
</div>
|
||||
<div class="settings-pane" data-settings-pane="cloud" hidden>
|
||||
<section class="panel form-section">
|
||||
<div class="form-section-head"><div><h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3><p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE service. The password is never returned by the API after it is saved.</p></div></div>
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.cloudAccountTitle">GREE Cloud account</h3>
|
||||
<p data-i18n="settings.cloudAccountHint">REST login and device discovery use the selected regional GREE
|
||||
service. The password is never returned by the API after it is saved.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_enabled"> <span data-i18n="settings.cloudEnable">Enable GREE Cloud</span></label>
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_enabled"> <span
|
||||
data-i18n="settings.cloudEnable">Enable GREE Cloud</span></label>
|
||||
<label><span data-i18n="settings.cloudRegion">Region</span><select name="gree_cloud_region">
|
||||
<option>Australia</option><option>China Mainland</option><option>East South Asia</option><option selected>Europe</option><option>India</option><option>Latin American</option><option>Middle East</option><option>North American</option><option>Russia</option><option>South American</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="settings.cloudPolling">Cloud fallback polling interval (s)</span><input type="number" name="gree_cloud_polling_interval_seconds" min="30" max="3600" value="60"></label>
|
||||
<label class="wide"><span data-i18n="settings.cloudLogin">Login / email</span><input name="gree_cloud_username" autocomplete="username"></label>
|
||||
<label class="wide"><span data-i18n="settings.cloudPassword">Password</span><input type="password" name="gree_cloud_password" autocomplete="new-password" data-i18n-placeholder="settings.cloudPasswordPlaceholder" placeholder="Leave empty to keep saved secret"></label>
|
||||
<option>Australia</option>
|
||||
<option>China Mainland</option>
|
||||
<option>East South Asia</option>
|
||||
<option selected>Europe</option>
|
||||
<option>India</option>
|
||||
<option>Latin American</option>
|
||||
<option>Middle East</option>
|
||||
<option>North American</option>
|
||||
<option>Russia</option>
|
||||
<option>South American</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="settings.cloudPolling">Cloud fallback polling interval (s)</span><input
|
||||
type="number" name="gree_cloud_polling_interval_seconds" min="30" max="3600" value="60"></label>
|
||||
<label class="wide"><span data-i18n="settings.cloudLogin">Login / email</span><input
|
||||
name="gree_cloud_username" autocomplete="username"></label>
|
||||
<label class="wide"><span data-i18n="settings.cloudPassword">Password</span><input type="password"
|
||||
name="gree_cloud_password" autocomplete="new-password"
|
||||
data-i18n-placeholder="settings.cloudPasswordPlaceholder"
|
||||
placeholder="Leave empty to keep saved secret"></label>
|
||||
<div class="wide cloud-runtime-summary" aria-live="polite" hidden>
|
||||
<div><span data-i18n="settings.cloudAccountStatus">Account status</span><strong id="greeCloudAccountStatus">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudMqttStatus">MQTT status</span><strong id="greeCloudMqttStatus">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudDevicesOnline">Devices online</span><strong id="greeCloudDevicesOnline">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudRestResponseTime">Last REST response time</span><strong id="greeCloudRestResponseTime">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudResponseTime">Last device response time</span><strong id="greeCloudResponseTime">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudLastDeviceResponse">Last device response</span><strong id="greeCloudLastDeviceResponse">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudLastMqttMessage">Last MQTT activity</span><strong id="greeCloudLastMqttMessage">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudConnectedSince">MQTT connected since</span><strong id="greeCloudConnectedSince">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudAccountStatus">Account status</span><strong
|
||||
id="greeCloudAccountStatus">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudMqttStatus">MQTT status</span><strong
|
||||
id="greeCloudMqttStatus">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudDevicesOnline">Devices online</span><strong
|
||||
id="greeCloudDevicesOnline">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudRestResponseTime">REST response time</span><strong
|
||||
id="greeCloudRestResponseTime">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudResponseTime">Device response time</span><strong
|
||||
id="greeCloudResponseTime">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudLastDeviceResponse">Last device response</span><strong
|
||||
id="greeCloudLastDeviceResponse">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudLastMqttMessage">Last MQTT activity</span><strong
|
||||
id="greeCloudLastMqttMessage">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudConnectedSince">MQTT connected since</span><strong
|
||||
id="greeCloudConnectedSince">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudBroker">Broker</span><strong id="greeCloudBroker">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudTraffic">Requests / responses / timeouts</span><strong id="greeCloudTraffic">—</strong></div>
|
||||
<div><span data-i18n="settings.cloudTraffic">Requests / responses / timeouts</span><strong
|
||||
id="greeCloudTraffic">—</strong></div>
|
||||
</div>
|
||||
<div class="wide cloud-account-meta">
|
||||
<small hidden><span data-i18n="settings.cloudLastContact">Last successful REST contact</span>: <strong id="greeCloudLastContact">—</strong></small>
|
||||
<small><span data-i18n="settings.cloudInstallationId">Installation ID</span>: <code id="greeCloudInstanceId">—</code></small>
|
||||
<small hidden><span data-i18n="settings.cloudLastContact">Last successful REST contact</span>: <strong
|
||||
id="greeCloudLastContact">—</strong></small>
|
||||
<small><span data-i18n="settings.cloudInstallationId">Installation ID</span>: <code
|
||||
id="greeCloudInstanceId">—</code></small>
|
||||
</div>
|
||||
<p class="field-note wide" data-i18n="settings.cloudPushHint">MQTT push is primary. Polling is used only for initial state, recovery and periodic verification.</p>
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="greeCloudTestButton" data-i18n="settings.cloudTest">Test connection</button><button type="button" class="secondary" id="greeCloudRefreshButton" data-i18n="settings.cloudRefreshDevices">Refresh devices</button><button type="button" class="secondary" id="greeCloudReconnectButton" data-i18n="settings.cloudReconnect">Reconnect</button></div>
|
||||
<p class="field-note wide" data-i18n="settings.cloudPushHint">MQTT push is primary. Polling is used only
|
||||
for initial state, recovery and periodic verification.</p>
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="greeCloudTestButton"
|
||||
data-i18n="settings.cloudTest">Test connection</button><button type="button" class="secondary"
|
||||
id="greeCloudRefreshButton" data-i18n="settings.cloudRefreshDevices">Refresh devices</button><button
|
||||
type="button" class="secondary" id="greeCloudReconnectButton"
|
||||
data-i18n="settings.cloudReconnect">Reconnect</button></div>
|
||||
<div id="greeCloudTestResult" class="inline-alert wide" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -791,14 +848,19 @@
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.cloudConnectivityTitle">Cloud connectivity metrics</h3>
|
||||
<p data-i18n="settings.cloudConnectivityHint">Optional. Disabled by default. Measures REST login response and MQTT PINGRESP round-trip.</p>
|
||||
<p data-i18n="settings.cloudConnectivityHint">Optional. Disabled by default. Measures REST login
|
||||
response and MQTT PINGRESP round-trip.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_connectivity_metrics_enabled"> <span data-i18n="settings.cloudConnectivityEnable">Measure Cloud REST/MQTT connectivity</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="gree_cloud_connectivity_metrics_interval_seconds" min="30" max="3600" value="300"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="gree_cloud_connectivity_metrics_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.cloudConnectivityHistoryHint">REST and MQTT measurements are stored in History → Pings together with Local/LAN measurements.</p>
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_connectivity_metrics_enabled"> <span
|
||||
data-i18n="settings.cloudConnectivityEnable">Measure Cloud REST/MQTT connectivity</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number"
|
||||
name="gree_cloud_connectivity_metrics_interval_seconds" min="30" max="3600" value="300"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number"
|
||||
name="gree_cloud_connectivity_metrics_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.cloudConnectivityHistoryHint">REST and MQTT measurements
|
||||
are stored in History → Pings together with Local/LAN measurements.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -853,9 +915,9 @@
|
||||
type="button" data-action="edit-flow-name" data-i18n-title="flow.editName"
|
||||
title="Edit Flow name"></button><button class="flow-name-edit" type="button"
|
||||
data-action="edit-flow-name" data-i18n-title="flow.editName" data-i18n-aria="flow.editName"
|
||||
title="Edit Flow name" aria-label="Edit Flow name" data-ui-icon="edit"></button></div><input id="flowName" maxlength="100"
|
||||
data-i18n-placeholder="flow.namePlaceholder" data-i18n-title="flow.nameAliasHint"
|
||||
placeholder="My automation">
|
||||
title="Edit Flow name" aria-label="Edit Flow name" data-ui-icon="edit"></button></div><input
|
||||
id="flowName" maxlength="100" data-i18n-placeholder="flow.namePlaceholder"
|
||||
data-i18n-title="flow.nameAliasHint" placeholder="My automation">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -931,7 +993,8 @@
|
||||
<div class="flow-workspace-wrap">
|
||||
<div class="flow-workspace-toolbar">
|
||||
<div class="flow-workspace-primary-actions"><button type="button" class="primary flow-add-block-button"
|
||||
data-action="flow-add-block"><span data-ui-icon="plus" aria-hidden="true"></span><span data-i18n="flow.addBlock">Add
|
||||
data-action="flow-add-block"><span data-ui-icon="plus" aria-hidden="true"></span><span
|
||||
data-i18n="flow.addBlock">Add
|
||||
block</span></button><button type="button" class="secondary flow-selection-action"
|
||||
data-action="flow-select-all" data-i18n="flow.selectAll">Zaznacz wszystko</button><button type="button"
|
||||
class="secondary flow-selection-action" data-action="flow-clear-selection"
|
||||
@@ -941,10 +1004,11 @@
|
||||
<button type="button" class="secondary icon-button" data-action="flow-zoom-out"
|
||||
data-i18n-title="flow.zoomOut" title="Zoom out" data-ui-icon="minus"></button><span id="flowZoomLabel"
|
||||
aria-live="polite">100%</span><button type="button" class="secondary icon-button"
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in" data-ui-icon="plus"></button><button type="button"
|
||||
class="secondary flow-fit-button" data-action="flow-fit" data-i18n-title="flow.fitView" title="Fit"><span
|
||||
class="flow-fit-button-icon" data-ui-icon="fit" aria-hidden="true"></span><span
|
||||
class="flow-fit-button-label" data-i18n="flow.fitView">Fit</span></button>
|
||||
data-action="flow-zoom-in" data-i18n-title="flow.zoomIn" title="Zoom in"
|
||||
data-ui-icon="plus"></button><button type="button" class="secondary flow-fit-button"
|
||||
data-action="flow-fit" data-i18n-title="flow.fitView" title="Fit"><span class="flow-fit-button-icon"
|
||||
data-ui-icon="fit" aria-hidden="true"></span><span class="flow-fit-button-label"
|
||||
data-i18n="flow.fitView">Fit</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="flowWorkspace" class="flow-workspace" tabindex="0">
|
||||
@@ -991,13 +1055,16 @@
|
||||
<h2 data-i18n="flow.actions">Actions</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<button type="button" data-action="flow-templates"><span
|
||||
data-i18n="flow.templates">Templates</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-dry-run"><span
|
||||
data-i18n="flow.dryRun">Dry-run</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-templates"><span data-i18n="flow.templates">Templates</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="import-flow"><span data-i18n="flow.import">Import</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="export-flow"><span data-i18n="flow.export">Export</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-dry-run"><span data-i18n="flow.dryRun">Dry-run</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button type="button" data-action="flow-logs"><span data-i18n="flow.logs">Logs</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1084,26 +1151,36 @@
|
||||
|
||||
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button class="active" data-nav="dashboard"><span data-ui-icon="home" aria-hidden="true"></span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span data-ui-icon="zones" aria-hidden="true"></span><b data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span data-ui-icon="groups" aria-hidden="true"></span><b data-i18n="nav.groups">Groups</b></button>
|
||||
<button class="active" data-nav="dashboard"><span data-ui-icon="home" aria-hidden="true"></span><b
|
||||
data-i18n="nav.dashboard">Dashboard</b></button>
|
||||
<button data-nav="zones"><span data-ui-icon="zones" aria-hidden="true"></span><b
|
||||
data-i18n="nav.zones">Zones</b></button>
|
||||
<button class="desktop-nav-only" data-nav="groups"><span data-ui-icon="groups" aria-hidden="true"></span><b
|
||||
data-i18n="nav.groups">Groups</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-nav="flows"><span data-ui-icon="flow" aria-hidden="true"></span><b data-i18n="nav.flows">Flow</b></button>
|
||||
<button data-nav="flows"><span data-ui-icon="flow" aria-hidden="true"></span><b
|
||||
data-i18n="nav.flows">Flow</b></button>
|
||||
<button class="desktop-nav-only" data-nav="schedules"><span data-ui-icon="schedule" aria-hidden="true"></span><b
|
||||
data-i18n="nav.schedules">Schedules</b></button>
|
||||
<button class="desktop-nav-only" data-nav="automations"><span data-ui-icon="automation" aria-hidden="true"></span><b
|
||||
data-i18n="nav.automations">Automations</b></button>
|
||||
<span class="nav-section-label desktop-nav-only" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-nav="history"><span data-ui-icon="history" aria-hidden="true"></span><b data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span data-ui-icon="devices" aria-hidden="true"></span><b data-i18n="nav.devices">Devices</b></button>
|
||||
<button data-nav="history"><span data-ui-icon="history" aria-hidden="true"></span><b
|
||||
data-i18n="nav.history">History</b></button>
|
||||
<button class="desktop-nav-only" data-nav="devices"><span data-ui-icon="devices" aria-hidden="true"></span><b
|
||||
data-i18n="nav.devices">Devices</b></button>
|
||||
<button class="desktop-nav-only" data-nav="homeassistant"><span>HA</span><b data-i18n="nav.homeAssistantShort">Home
|
||||
Assistant</b></button>
|
||||
<button class="desktop-nav-only" data-nav="simulation"><span data-ui-icon="play" aria-hidden="true"></span><b
|
||||
data-i18n="nav.simulation">Simulator</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span data-ui-icon="moon" aria-hidden="true"></span><b data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span data-ui-icon="events" aria-hidden="true"></span><b data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span data-ui-icon="settings" aria-hidden="true"></span><b data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span data-ui-icon="more-horizontal" aria-hidden="true"></span><b data-i18n="nav.more">More</b></button>
|
||||
<button class="desktop-nav-only" data-nav="night"><span data-ui-icon="moon" aria-hidden="true"></span><b
|
||||
data-i18n="nav.nightMode">Night mode</b></button>
|
||||
<button class="desktop-nav-only" data-nav="logs"><span data-ui-icon="events" aria-hidden="true"></span><b
|
||||
data-i18n="nav.logs">Events</b></button>
|
||||
<button class="desktop-nav-only" data-nav="settings"><span data-ui-icon="settings" aria-hidden="true"></span><b
|
||||
data-i18n="nav.settings">Settings</b></button>
|
||||
<button class="mobile-nav-only" data-nav="more"><span data-ui-icon="more-horizontal" aria-hidden="true"></span><b
|
||||
data-i18n="nav.more">More</b></button>
|
||||
</nav>
|
||||
|
||||
<dialog id="moreDialog" class="sheet">
|
||||
@@ -1112,18 +1189,26 @@
|
||||
</div>
|
||||
<div class="menu-list more-menu-list">
|
||||
<span class="menu-section-label" data-i18n="nav.sectionControl">Sterowanie</span>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="devices"><span data-i18n="nav.devices">Devices</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<button data-go="groups"><span data-i18n="nav.groups">Groups</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionAutomation">Automatyzacja</span>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<span class="menu-section-label" data-i18n="nav.sectionData">Dane i system</span>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="simulation"><span data-i18n="nav.simulation">Simulator</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<button data-go="night"><span data-i18n="nav.nightMode">Night mode</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<button data-go="homeassistant"><span data-i18n="nav.homeAssistant">Home Assistant /
|
||||
Sensors</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span data-ui-icon="chevron-right"
|
||||
aria-hidden="true"></span></button>
|
||||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span
|
||||
data-ui-icon="chevron-right" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1157,17 +1242,25 @@
|
||||
|
||||
<dialog id="cloudDiscoveryDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiscoveryHint">Devices are loaded from the saved GREE Cloud account. Nothing is added automatically; choose the units you want to register.</p>
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow">GREE Cloud</span>
|
||||
<h2 data-i18n="devices.cloudDiscoveryTitle">Devices on account</h2>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiscoveryHint">Devices are loaded from the saved GREE Cloud account.
|
||||
Nothing is added automatically; choose the units you want to register.</p>
|
||||
<div id="cloudDiscoveryList" class="discovery-name-list"></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiscoveryRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiscoveryRefresh"
|
||||
data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close
|
||||
data-i18n="actions.done">Done</button></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="discoverDialog">
|
||||
<form method="dialog" id="discoverForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
<h2 data-i18n="discovery.title">Discover GREE devices</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.help">Auto searches both AES-ECB and AES-GCM devices. Multiple passes
|
||||
improve discovery when several Wi-Fi modules answer the same broadcast.</p>
|
||||
@@ -1188,7 +1281,8 @@
|
||||
<dialog id="discoveryNamesDialog">
|
||||
<form method="dialog" id="discoveryNamesForm" class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
<h2 data-i18n="discovery.nameDevices">Name discovered devices</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="discovery.nameDevicesHelp">Give each new unit a friendly room name. The technical
|
||||
model and MAC remain available in diagnostics.</p>
|
||||
@@ -1203,7 +1297,8 @@
|
||||
<form method="dialog" id="renameDeviceForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
<h2 data-i18n="devices.technicalConfig">Technical configuration</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="two"><label><span data-i18n="devices.ipAddress">IP address</span><input name="ip"
|
||||
@@ -1228,56 +1323,101 @@
|
||||
<form method="dialog" id="deviceDetailsForm" class="dialog-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="devices.cloudDetails">Cloud details</span><h2 id="deviceDetailsTitle">Device details</h2></div>
|
||||
<div><span class="eyebrow" data-i18n="devices.cloudDetails">Cloud details</span>
|
||||
<h2 id="deviceDetailsTitle">Device details</h2>
|
||||
</div>
|
||||
<button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<label><span data-i18n="common.name">Name</span><input name="name" required maxlength="80"></label>
|
||||
<div class="technical-device-grid" id="deviceDetailsMeta"></div>
|
||||
<section class="form-section">
|
||||
<div class="form-section-head"><div><h3 data-i18n="energy.title">Energy</h3><p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p></div></div>
|
||||
<div class="form-section-head">
|
||||
<div>
|
||||
<h3 data-i18n="energy.title">Energy</h3>
|
||||
<p data-i18n="energy.sourceHint">Choose the cumulative energy source used for charts and period totals.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="energy.source">Energy source</span><select name="energy_source">
|
||||
<option value="auto" data-i18n="energy.auto">Auto</option>
|
||||
<option value="gree_cloud">GREE Cloud</option>
|
||||
<option value="home_assistant">Home Assistant</option>
|
||||
</select></label>
|
||||
<label class="wide"><span data-i18n="energy.haSensor">Home Assistant energy sensor</span><select name="ha_energy_entity_id"><option value="">—</option></select></label>
|
||||
<option value="auto" data-i18n="energy.auto">Auto</option>
|
||||
<option value="gree_cloud">GREE Cloud</option>
|
||||
<option value="home_assistant">Home Assistant</option>
|
||||
</select></label>
|
||||
<label class="wide"><span data-i18n="energy.haSensor">Home Assistant energy sensor</span><select
|
||||
name="ha_energy_entity_id">
|
||||
<option value="">—</option>
|
||||
</select></label>
|
||||
<p class="field-note wide" id="deviceEnergySensorMeta"></p>
|
||||
<div class="inline-alert info wide" id="deviceEnergyGroupNote" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="form-actions"><button type="button" class="secondary" data-close data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" data-close
|
||||
data-i18n="actions.cancel">Cancel</button><button class="primary" type="submit"
|
||||
data-i18n="actions.save">Save</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="deviceGroupsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow" data-i18n="devices.installation">Installation</span><h2 data-i18n="devices.installations">Split / multisplit installations</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.installationsHint">Group indoor units that share one outdoor unit. Energy and outdoor temperature can use one shared source.</p>
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="devices.installation">Installation</span>
|
||||
<h2 data-i18n="devices.installations">Split / multisplit installations</h2>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="devices.installationsHint">Group indoor units that share one outdoor unit. Energy
|
||||
and outdoor temperature can use one shared source.</p>
|
||||
<div id="deviceGroupsList" class="installation-list"></div>
|
||||
<form id="deviceGroupForm" class="installation-form">
|
||||
<input type="hidden" name="id">
|
||||
<div class="form-grid">
|
||||
<label><span data-i18n="devices.installationName">Installation name</span><input name="name" maxlength="80" required></label>
|
||||
<label><span data-i18n="devices.installationType">Installation type</span><select name="kind"><option value="split" data-i18n="devices.split">Split</option><option value="multisplit" data-i18n="devices.multisplit">Multisplit</option></select></label>
|
||||
<div class="wide"><span class="field-label" data-i18n="devices.installationDevices">Indoor units</span><div id="deviceGroupDeviceChoices" class="installation-device-choices"></div></div>
|
||||
<label><span data-i18n="energy.source">Energy source</span><select name="energy_source"><option value="auto" data-i18n="energy.auto">Auto</option><option value="gree_cloud">GREE Cloud</option><option value="home_assistant">Home Assistant</option></select></label>
|
||||
<label><span data-i18n="devices.energySourceDevice">GREE Cloud energy source</span><select name="energy_device_id"><option value="">—</option></select></label>
|
||||
<label class="wide"><span data-i18n="energy.haSensor">Home Assistant energy sensor</span><select name="ha_energy_entity_id"><option value="">—</option></select></label>
|
||||
<label class="wide"><span data-i18n="devices.outdoorSourceDevice">Outdoor temperature source</span><select name="outdoor_temperature_device_id"><option value="" data-i18n="devices.noSharedOutdoor">No shared source</option></select></label>
|
||||
<label><span data-i18n="devices.installationName">Installation name</span><input name="name" maxlength="80"
|
||||
required></label>
|
||||
<label><span data-i18n="devices.installationType">Installation type</span><select name="kind">
|
||||
<option value="split" data-i18n="devices.split">Split</option>
|
||||
<option value="multisplit" data-i18n="devices.multisplit">Multisplit</option>
|
||||
</select></label>
|
||||
<div class="wide"><span class="field-label" data-i18n="devices.installationDevices">Indoor units</span>
|
||||
<div id="deviceGroupDeviceChoices" class="installation-device-choices"></div>
|
||||
</div>
|
||||
<label><span data-i18n="energy.source">Energy source</span><select name="energy_source">
|
||||
<option value="auto" data-i18n="energy.auto">Auto</option>
|
||||
<option value="gree_cloud">GREE Cloud</option>
|
||||
<option value="home_assistant">Home Assistant</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.energySourceDevice">GREE Cloud energy source</span><select
|
||||
name="energy_device_id">
|
||||
<option value="">—</option>
|
||||
</select></label>
|
||||
<label class="wide"><span data-i18n="energy.haSensor">Home Assistant energy sensor</span><select
|
||||
name="ha_energy_entity_id">
|
||||
<option value="">—</option>
|
||||
</select></label>
|
||||
<label class="wide"><span data-i18n="devices.outdoorSourceDevice">Outdoor temperature source</span><select
|
||||
name="outdoor_temperature_device_id">
|
||||
<option value="" data-i18n="devices.noSharedOutdoor">No shared source</option>
|
||||
</select></label>
|
||||
<p class="field-note wide" id="deviceGroupEnergySensorMeta"></p>
|
||||
</div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="deviceGroupNew" data-i18n="devices.newInstallation">New installation</button><button type="submit" class="primary" data-i18n="actions.save">Save</button></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="deviceGroupNew"
|
||||
data-i18n="devices.newInstallation">New installation</button><button type="submit" class="primary"
|
||||
data-i18n="actions.save">Save</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="cloudDiagnosticsDialog" class="dialog-wide">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head"><div><span class="eyebrow">GREE Cloud</span><h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2></div><button type="button" data-close data-ui-icon="close"></button></div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiagnosticsHint">Decrypted and sanitized protocol data. Passwords, tokens and cipher keys are never included.</p>
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow">GREE Cloud</span>
|
||||
<h2 data-i18n="devices.cloudDiagnostics">Cloud diagnostics</h2>
|
||||
</div><button type="button" data-close data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="devices.cloudDiagnosticsHint">Decrypted and sanitized protocol data. Passwords,
|
||||
tokens and cipher keys are never included.</p>
|
||||
<pre id="cloudDiagnosticsPayload" class="mono cloud-diagnostics-payload">—</pre>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiagnosticsRefresh" data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||||
<div class="form-actions"><button type="button" class="secondary" id="cloudDiagnosticsRefresh"
|
||||
data-i18n="actions.refresh">Refresh</button><button type="button" class="primary" data-close
|
||||
data-i18n="actions.done">Done</button></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1359,17 +1499,20 @@
|
||||
°C</span><input type="text" inputmode="decimal" step="0.1" min="0.1" max="5" name="heat_hysteresis"
|
||||
value="0.6"></label></div>
|
||||
<div id="externalSensorFields">
|
||||
<label><span data-i18n="zones.roomSensorEntity">Room sensor entity_id</span><input name="ha_entity_id" list="haEntitySuggestions"
|
||||
placeholder="sensor.living_room_temperature"></label>
|
||||
<label><span data-i18n="zones.roomSensorEntity">Room sensor entity_id</span><input name="ha_entity_id"
|
||||
list="haEntitySuggestions" placeholder="sensor.living_room_temperature"></label>
|
||||
<div class="two"><label><span data-i18n="zones.roomSensorWeight">Room sensor weight %</span><input type="number"
|
||||
step="5" min="0" max="100" name="external_sensor_weight_percent" value="40"></label><label><span
|
||||
data-i18n="zones.maxDifference">Max. sensor difference °C</span><input type="text" inputmode="decimal"
|
||||
step="0.1" min="0.1" max="20" name="max_sensor_difference" value="3.0"></label></div>
|
||||
<p class="field-note" data-i18n="zones.sensorHelp">This room sensor is required when the temperature strategy uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.</p>
|
||||
<p class="field-note" data-i18n="zones.sensorHelp">This room sensor is required when the temperature strategy
|
||||
uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.</p>
|
||||
</div>
|
||||
<label><span data-i18n="zones.outdoorSensorEntity">Outdoor temperature entity_id override</span><input name="ha_outdoor_entity_id" list="haEntitySuggestions"
|
||||
placeholder="sensor.outdoor_temperature"></label>
|
||||
<p class="field-note" data-i18n="zones.outdoorSensorHelp">Leave empty to use the global Outdoor temperature entity_id. Set another entity_id to use a different outdoor sensor only for this zone. It is used by outdoor-temperature assist and recorded in history/metrics.</p>
|
||||
<label><span data-i18n="zones.outdoorSensorEntity">Outdoor temperature entity_id override</span><input
|
||||
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
|
||||
<p class="field-note" data-i18n="zones.outdoorSensorHelp">Leave empty to use the global Outdoor temperature
|
||||
entity_id. Set another entity_id to use a different outdoor sensor only for this zone. It is used by
|
||||
outdoor-temperature assist and recorded in history/metrics.</p>
|
||||
<div class="two"><label><span data-i18n="zones.minAdjust">Minimum adjustment interval (s)</span><input
|
||||
type="number" min="15" name="min_adjust_seconds" value="120"></label><label><span
|
||||
data-i18n="zones.standbyOffset">Standby setpoint offset °C</span><input type="text" inputmode="decimal"
|
||||
@@ -1641,7 +1784,8 @@
|
||||
<dialog id="generatedTokenDialog" class="auth-dialog">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close data-ui-icon="close"></button>
|
||||
<h2 data-i18n="settings.tokenCreated">Token created</h2><button type="button" data-close
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<p class="field-note" data-i18n="settings.tokenCreatedHint">Copy this token now. It will not be shown again.</p>
|
||||
<label><span data-i18n="auth.token">Access token</span><input id="generatedAccessToken" class="mono"
|
||||
@@ -1655,18 +1799,27 @@
|
||||
<dialog id="outdoorHistoryDialog" class="dialog-wide outdoor-history-dialog">
|
||||
<div class="dialog-form">
|
||||
<div class="dialog-head">
|
||||
<div><span class="eyebrow" data-i18n="history.24h">24 hours</span><h2 data-i18n="history.allOutdoor">Outdoor temperatures</h2></div>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close" data-ui-icon="close"></button>
|
||||
<div><span class="eyebrow" data-i18n="history.24h">24 hours</span>
|
||||
<h2 data-i18n="history.allOutdoor">Outdoor temperatures</h2>
|
||||
</div>
|
||||
<button type="button" data-close data-i18n-aria="actions.close" aria-label="Close"
|
||||
data-ui-icon="close"></button>
|
||||
</div>
|
||||
<div class="outdoor-history-current"><small data-i18n="house.outdoor">Outdoor</small><strong id="outdoorHistoryCurrent">--</strong></div>
|
||||
<div class="outdoor-history-current"><small data-i18n="house.outdoor">Outdoor</small><strong
|
||||
id="outdoorHistoryCurrent">--</strong></div>
|
||||
<div id="outdoorHistoryChartHost"></div>
|
||||
<div class="outdoor-history-links" data-i18n-aria="house.outdoorHistoryMore" aria-label="More metrics and ranges in history">
|
||||
<div class="outdoor-history-links" data-i18n-aria="house.outdoorHistoryMore"
|
||||
aria-label="More metrics and ranges in history">
|
||||
<span data-i18n="house.outdoorHistoryMore">More in history</span>
|
||||
<div>
|
||||
<a href="/history/overview?hours=168" data-history-route data-history-hours="168" data-i18n="history.range7d">7 days</a>
|
||||
<a href="/history/overview?hours=720" data-history-route data-history-hours="720" data-i18n="history.range30d">30 days</a>
|
||||
<a href="/history/overview?hours=8760" data-history-route data-history-hours="8760" data-i18n="history.range1y">1 year</a>
|
||||
<a href="/history/overview" data-history-route class="outdoor-history-all" data-i18n="house.outdoorHistoryFull">Full history</a>
|
||||
<a href="/history/overview?hours=168" data-history-route data-history-hours="168"
|
||||
data-i18n="history.range7d">7 days</a>
|
||||
<a href="/history/overview?hours=720" data-history-route data-history-hours="720"
|
||||
data-i18n="history.range30d">30 days</a>
|
||||
<a href="/history/overview?hours=8760" data-history-route data-history-hours="8760"
|
||||
data-i18n="history.range1y">1 year</a>
|
||||
<a href="/history/overview" data-history-route class="outdoor-history-all"
|
||||
data-i18n="house.outdoorHistoryFull">Full history</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1695,8 +1848,8 @@
|
||||
data-i18n="debug.requests">Requests</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="gree"
|
||||
data-i18n="debug.gree">GREE</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="cloud"
|
||||
data-i18n="debug.cloud">Cloud API</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="cloud" data-i18n="debug.cloud">Cloud
|
||||
API</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="mqtt"
|
||||
data-i18n="debug.mqtt">MQTT</button>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@ function normalizeEnergyHistoryTargets() {
|
||||
const ids = new Set(available.map(item => item.id));
|
||||
app.historyEnergyTargets = (app.historyEnergyTargets || []).filter(id => ids.has(id));
|
||||
if (!app.historyEnergyTargets.length && app.historyEnergyDevice && ids.has(app.historyEnergyDevice)) app.historyEnergyTargets = [app.historyEnergyDevice];
|
||||
if (!app.historyEnergyTargets.length && available.length) app.historyEnergyTargets = [available[0].id];
|
||||
if (!app.historyEnergyTargets.length && available.length) app.historyEnergyTargets = available.slice(0, 8).map(item => item.id);
|
||||
app.historyEnergyTargets = app.historyEnergyTargets.slice(0, 8);
|
||||
app.historyEnergyDevice = app.historyEnergyTargets[0] || '';
|
||||
return available;
|
||||
@@ -20,7 +20,7 @@ const setCookie = (name, value) => {
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=31536000; Path=${APP_BASE || '/'}; SameSite=Lax`;
|
||||
};
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
const DEFAULT_LANGUAGE = String(document.documentElement.lang || '').trim();
|
||||
const preferredLanguage = getCookie('gree_controller_language') || DEFAULT_LANGUAGE;
|
||||
const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_controller_theme')) ? getCookie('gree_controller_theme') : 'system';
|
||||
|
||||
@@ -28,9 +28,9 @@ const app = {
|
||||
devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
|
||||
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {},
|
||||
languages: [], translations: {}, locales: {}, defaultLanguage: DEFAULT_LANGUAGE,
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetworkShowJitter: true, historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetworkShowJitter: true, historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyHours: '720', historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
customChartSeries: [], savedCharts: [], customChartEditingId: null, customChartNameDraft: null, chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
haManualFallbackVisible: false, haSupervisorTestState: null, haEntityCatalog: [], haEntityCatalogConfigured: false, haEntityCatalogLoadedAt: 0, haEntityCatalogLoading: false,
|
||||
@@ -97,9 +97,9 @@ function hydrateUiIcons(root = document) {
|
||||
node.innerHTML = uiIcon(name, className);
|
||||
});
|
||||
}
|
||||
const locale = () => app.locales[app.language] || app.locales[DEFAULT_LANGUAGE] || 'en-GB';
|
||||
const locale = () => app.locales[app.language] || app.locales[app.defaultLanguage] || app.language || app.defaultLanguage || DEFAULT_LANGUAGE;
|
||||
const tr = (key, params = {}) => {
|
||||
const template = app.translations[app.language]?.[key] ?? app.translations[DEFAULT_LANGUAGE]?.[key] ?? key;
|
||||
const template = app.translations[app.language]?.[key] ?? app.translations[app.defaultLanguage]?.[key] ?? key;
|
||||
return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => params[name] ?? `{${name}}`);
|
||||
};
|
||||
const fmtTemp = value => value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value)) ? `${Number(value).toFixed(1)}°C` : '--';
|
||||
@@ -275,7 +275,7 @@ function updateConnectionIndicator(status) {
|
||||
|
||||
function updateToolbarControls() {
|
||||
const languageLabel = $('#languageLabel');
|
||||
if (languageLabel) languageLabel.textContent = String(app.language || DEFAULT_LANGUAGE).toUpperCase();
|
||||
if (languageLabel) languageLabel.textContent = String(app.language || app.defaultLanguage || DEFAULT_LANGUAGE).toUpperCase();
|
||||
const themeSelect = $('#themeSelect');
|
||||
const themeIcon = $('#themeIcon');
|
||||
const iconName = { system: 'system', light: 'sun', dark: 'moon' }[app.theme] || 'system';
|
||||
@@ -328,7 +328,7 @@ async function loadLanguagePack(language) {
|
||||
|
||||
async function setLanguage(language) {
|
||||
const available = app.languages.some(item => item.code === language);
|
||||
const nextLanguage = available ? language : DEFAULT_LANGUAGE;
|
||||
const nextLanguage = available ? language : (app.defaultLanguage || app.languages[0]?.code || DEFAULT_LANGUAGE);
|
||||
const previousLanguage = app.language;
|
||||
const select = $('#languageSelect');
|
||||
if (nextLanguage === previousLanguage && app.translations[nextLanguage]) return;
|
||||
@@ -364,29 +364,33 @@ async function loadLanguages() {
|
||||
if (!response.ok) throw new Error(`Language index HTTP ${response.status}`);
|
||||
const manifest = await response.json();
|
||||
const languages = Array.isArray(manifest.languages) ? manifest.languages : [];
|
||||
if (!languages.some(item => item.code === DEFAULT_LANGUAGE)) throw new Error('Default English language pack is missing');
|
||||
if (!languages.length) throw new Error('Language manifest is empty');
|
||||
app.languages = languages;
|
||||
app.defaultLanguage = app.languages.some(item => item.code === manifest.default)
|
||||
? manifest.default
|
||||
: app.languages[0].code;
|
||||
app.translations = {};
|
||||
app.locales = Object.fromEntries(languages.map(item => [item.code, item.locale || item.code]));
|
||||
app.language = app.languages.some(item => item.code === preferredLanguage)
|
||||
? preferredLanguage
|
||||
: (manifest.default || DEFAULT_LANGUAGE);
|
||||
if (!app.languages.some(item => item.code === app.language)) app.language = DEFAULT_LANGUAGE;
|
||||
: app.defaultLanguage;
|
||||
if (!app.languages.some(item => item.code === app.language)) app.language = app.defaultLanguage;
|
||||
try {
|
||||
await loadLanguagePack(app.language);
|
||||
} catch (error) {
|
||||
if (app.language === DEFAULT_LANGUAGE) throw error;
|
||||
if (app.language === app.defaultLanguage) throw error;
|
||||
console.error(`Unable to load preferred language ${app.language}:`, error);
|
||||
app.language = DEFAULT_LANGUAGE;
|
||||
await loadLanguagePack(DEFAULT_LANGUAGE);
|
||||
app.language = app.defaultLanguage;
|
||||
await loadLanguagePack(app.defaultLanguage);
|
||||
}
|
||||
renderLanguageOptions();
|
||||
} catch (error) {
|
||||
console.error('Unable to load language packs:', error);
|
||||
app.languages = [{ code: DEFAULT_LANGUAGE, name: 'English', native_name: 'English', locale: 'en-GB' }];
|
||||
app.translations = { [DEFAULT_LANGUAGE]: {} };
|
||||
app.locales = { [DEFAULT_LANGUAGE]: 'en-GB' };
|
||||
app.language = DEFAULT_LANGUAGE;
|
||||
const fallbackLanguage = app.defaultLanguage || DEFAULT_LANGUAGE;
|
||||
app.languages = fallbackLanguage ? [{ code: fallbackLanguage, name: fallbackLanguage.toUpperCase(), native_name: fallbackLanguage.toUpperCase(), locale: fallbackLanguage }] : [];
|
||||
app.translations = fallbackLanguage ? { [fallbackLanguage]: {} } : {};
|
||||
app.locales = fallbackLanguage ? { [fallbackLanguage]: fallbackLanguage } : {};
|
||||
app.language = fallbackLanguage;
|
||||
renderLanguageOptions();
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ $('#cloudDiscoverButton')?.addEventListener('click', async event => {
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
$('#historyRefresh').addEventListener('click', loadHistory);
|
||||
$('#historyHours').addEventListener('change', () => { updateBrowserUrl(currentHistoryPath(), true); loadHistory(); });
|
||||
$('#historyHours').addEventListener('change', () => { if (app.historyTab === 'energy') app.historyEnergyHours = $('#historyHours').value || '720'; updateBrowserUrl(currentHistoryPath(), true); loadHistory(); });
|
||||
$('#logsRefresh').addEventListener('click', loadLogs);
|
||||
$('#languageSelect').addEventListener('change', event => setLanguage(event.target.value));
|
||||
$('#themeSelect')?.addEventListener('change', event => setTheme(event.target.value));
|
||||
@@ -728,7 +728,7 @@ function markFlowPresetRecent(id) {
|
||||
function flowPresetText(value) {
|
||||
if (typeof value === 'string') return value;
|
||||
if (!value || typeof value !== 'object') return '';
|
||||
return value[app.language] || value.en || value.pl || Object.values(value).find(item => typeof item === 'string') || '';
|
||||
return value[app.language] || value[app.defaultLanguage] || Object.values(value).find(item => typeof item === 'string') || '';
|
||||
}
|
||||
|
||||
async function loadFlowPresets({ force = false } = {}) {
|
||||
@@ -741,11 +741,11 @@ async function loadFlowPresets({ force = false } = {}) {
|
||||
const entries = Array.isArray(manifest?.presets) ? manifest.presets : [];
|
||||
const presets = await Promise.all(entries.map(async entry => {
|
||||
const filename = String(entry?.file || '');
|
||||
if (!filename || !/^[a-zA-Z0-9_-]+\.json$/.test(filename)) throw new Error('Invalid preset filename');
|
||||
if (!filename || !/^[a-zA-Z0-9_-]+\.json$/.test(filename)) throw new Error(tr('flow.invalidPresetFilename'));
|
||||
const response = await fetch(withBase(`/presets/${encodeURIComponent(filename)}`), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`${filename}: HTTP ${response.status}`);
|
||||
const preset = await response.json();
|
||||
if (!preset?.id || !preset?.flow || !Array.isArray(preset.flow.nodes) || !Array.isArray(preset.flow.edges)) throw new Error(`${filename}: invalid preset`);
|
||||
if (!preset?.id || !preset?.flow || !Array.isArray(preset.flow.nodes) || !Array.isArray(preset.flow.edges)) throw new Error(tr('flow.invalidPresetFile', { filename }));
|
||||
return preset;
|
||||
}));
|
||||
app.flowPresets = presets;
|
||||
@@ -923,7 +923,7 @@ async function openFlowTemplates() {
|
||||
renderFlowPresetBrowser(flowPresetActiveCategory);
|
||||
} catch (error) {
|
||||
if (tabs) tabs.innerHTML = '';
|
||||
host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.templatesLoadFailed') || 'Unable to load presets')}</strong><span>${esc(error.message)}</span></div>`;
|
||||
host.innerHTML = `<div class="empty compact"><strong>${esc(tr('flow.templatesLoadFailed'))}</strong><span>${esc(error.message)}</span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,7 +936,7 @@ function previewFlowTemplate(key) {
|
||||
function applyFlowTemplate(key) {
|
||||
if (!app.flowDraft) return;
|
||||
const preset = (app.flowPresets || []).find(item => item.id === key);
|
||||
if (!preset) return toast(tr('flow.templatesLoadFailed') || 'Preset not found', true);
|
||||
if (!preset) return toast(tr('flow.presetNotFound'), true);
|
||||
if (app.flowDraft.nodes.length && !confirm(tr('flow.templateReplaceConfirm'))) return;
|
||||
const graph = materializeFlowPreset(preset);
|
||||
app.flowDraft.nodes = graph.nodes; app.flowDraft.edges = graph.edges;
|
||||
@@ -392,7 +392,7 @@ async function handleHistoryAction(button) {
|
||||
title: $('#customChartName')?.value.trim() || tr('history.customChart'),
|
||||
series: [...app.customChartSeries],
|
||||
hours: Number($('#historyHours')?.value || 24),
|
||||
lang: app.language === 'pl' ? 'pl' : 'en',
|
||||
lang: app.language,
|
||||
},
|
||||
});
|
||||
const link = publicCustomChartUrl(share.path);
|
||||
@@ -29,7 +29,10 @@ function showView(name, { push = true, scroll = true } = {}) {
|
||||
}
|
||||
|
||||
function showHistoryTab(tab, { push = true, load = true } = {}) {
|
||||
app.historyTab = HISTORY_TABS.includes(tab) ? tab : 'overview';
|
||||
const nextTab = HISTORY_TABS.includes(tab) ? tab : 'overview';
|
||||
if (app.historyTab === 'energy' && $('#historyHours')) app.historyEnergyHours = $('#historyHours').value || app.historyEnergyHours || '720';
|
||||
app.historyTab = nextTab;
|
||||
if (nextTab === 'energy' && $('#historyHours')) $('#historyHours').value = app.historyEnergyHours || '720';
|
||||
renderHistoryNavigation();
|
||||
if (push) updateBrowserUrl(currentHistoryPath());
|
||||
if (load) loadHistory();
|
||||
@@ -85,7 +85,13 @@ function applyRouteFromLocation() {
|
||||
app.historyNetworkTarget = params.get('target') || 'all';
|
||||
app.historySensor = params.get('sensor') || 'all';
|
||||
const hours = params.get('hours');
|
||||
if (hours && ['6', '24', '168', '720', '2160', '8760'].includes(hours) && $('#historyHours')) $('#historyHours').value = hours;
|
||||
const validHours = hours && ['6', '24', '168', '720', '2160', '8760'].includes(hours) ? hours : null;
|
||||
if (app.historyTab === 'energy') {
|
||||
app.historyEnergyHours = validHours || app.historyEnergyHours || '720';
|
||||
if ($('#historyHours')) $('#historyHours').value = app.historyEnergyHours;
|
||||
} else if (validHours && $('#historyHours')) {
|
||||
$('#historyHours').value = validHours;
|
||||
}
|
||||
if (app.historyTab === 'custom' && params.get('chart')) app.customChartSeries = decodeChartSpec(params.get('chart'));
|
||||
if (!showView('history', { push: false, scroll: false })) restoreCurrentRoute(previousView);
|
||||
return;
|
||||
@@ -345,6 +345,38 @@ $('#haUseSupervisor')?.addEventListener('click', async event => {
|
||||
} finally { button.disabled = false; button.textContent = idle; }
|
||||
});
|
||||
|
||||
$('#testInfluxDbConnection')?.addEventListener('click', async event => {
|
||||
const form = $('#settingsForm');
|
||||
const button = event.currentTarget;
|
||||
const resultBox = $('#influxDbTestResult');
|
||||
if (!form) return;
|
||||
const idle = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = tr('settings.influxTesting');
|
||||
clearFormErrors(form);
|
||||
if (resultBox) { resultBox.hidden = true; resultBox.classList.remove('success', 'error'); }
|
||||
try {
|
||||
const result = await api('/api/integrations/influxdb/test', { method: 'POST', body: influxDbSettingsBodyFromForm(form) });
|
||||
const message = tr('settings.influxTestSuccess', { ms: result.response_time_ms ?? 0 });
|
||||
if (resultBox) {
|
||||
resultBox.hidden = false;
|
||||
resultBox.classList.add('success');
|
||||
resultBox.innerHTML = `<span>${esc(tr('status.connected'))}</span><strong>${esc(message)}</strong>`;
|
||||
}
|
||||
toast(message);
|
||||
} catch (error) {
|
||||
if (resultBox) {
|
||||
resultBox.hidden = false;
|
||||
resultBox.classList.add('error');
|
||||
resultBox.innerHTML = `<span>${esc(tr('devices.lastError'))}</span><strong>${esc(error.message)}</strong>`;
|
||||
}
|
||||
toast(error.message, true);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = idle;
|
||||
}
|
||||
});
|
||||
|
||||
$('#exportSettings').addEventListener('click', async () => {
|
||||
try {
|
||||
const data = await api('/api/configuration/export');
|
||||
@@ -509,7 +541,7 @@ $('#greeCloudTestButton')?.addEventListener('click', async event => {
|
||||
if (resultBox) {
|
||||
resultBox.hidden = false;
|
||||
resultBox.classList.add(result.ok ? 'success' : 'error');
|
||||
const message = result.ok ? `Connected. ${Number(result.device_count || 0)} device(s) found.` : (result.message || result.status || 'Connection failed');
|
||||
const message = result.ok ? tr('settings.cloudConnected', { count: Number(result.device_count || 0) }) : (result.message || result.status || tr('settings.cloudConnectionFailed'));
|
||||
resultBox.innerHTML = `<span>${esc(result.ok ? tr('status.online') : tr('devices.lastError'))}</span><strong>${esc(message)}</strong>`;
|
||||
}
|
||||
if (result.ok) {
|
||||
@@ -16,16 +16,55 @@ function basePathFromScript() {
|
||||
|
||||
const PUBLIC_BASE = basePathFromScript();
|
||||
const withBase = path => `${PUBLIC_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const PUBLIC_DEFAULT_LANGUAGE = String(document.documentElement.lang || '').trim();
|
||||
const PUBLIC_RANGE_KEYS = ['history.6h', 'history.24h', 'history.7d', 'history.30d', 'history.90d', 'history.1y'];
|
||||
let publicLanguageManifest = null;
|
||||
let publicChartLanguage = PUBLIC_DEFAULT_LANGUAGE;
|
||||
let publicChartTranslations = {};
|
||||
let publicChartLocale = PUBLIC_DEFAULT_LANGUAGE;
|
||||
|
||||
function publicCookie(name) {
|
||||
const row = document.cookie.split('; ').find(item => item.startsWith(`${name}=`));
|
||||
return row ? decodeURIComponent(row.split('=').slice(1).join('=')) : '';
|
||||
}
|
||||
|
||||
function publicTr(key, params = {}) {
|
||||
const template = publicChartTranslations[key] ?? key;
|
||||
return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => params[name] ?? `{${name}}`);
|
||||
}
|
||||
|
||||
async function loadPublicLanguageManifest() {
|
||||
if (publicLanguageManifest) return publicLanguageManifest;
|
||||
const response = await fetch(withBase('/lang/index.json'), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`language index HTTP ${response.status}`);
|
||||
publicLanguageManifest = await response.json();
|
||||
return publicLanguageManifest;
|
||||
}
|
||||
|
||||
async function setPublicChartLanguage(language) {
|
||||
const manifest = await loadPublicLanguageManifest();
|
||||
const languages = Array.isArray(manifest.languages) ? manifest.languages : [];
|
||||
const fallbackCode = manifest.default || PUBLIC_DEFAULT_LANGUAGE;
|
||||
const item = languages.find(entry => entry.code === language)
|
||||
|| languages.find(entry => entry.code === fallbackCode)
|
||||
|| languages[0];
|
||||
if (!item) throw new Error('language pack unavailable');
|
||||
const response = await fetch(withBase(item.path || `/lang/${encodeURIComponent(item.code)}.json`), { headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) throw new Error(`language ${item.code} HTTP ${response.status}`);
|
||||
const pack = await response.json();
|
||||
publicChartLanguage = item.code;
|
||||
publicChartTranslations = pack.translations || {};
|
||||
publicChartLocale = pack.meta?.locale || item.locale || item.code;
|
||||
document.documentElement.lang = item.code;
|
||||
document.title = `GREE Controller · ${publicTr('publicChart.title')}`;
|
||||
setPublicRangeLanguage();
|
||||
}
|
||||
|
||||
function cssColor(name, fallback) {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function localeFor(lang) {
|
||||
return lang === 'pl' ? 'pl-PL' : 'en-GB';
|
||||
}
|
||||
|
||||
function timeLabel(timestamp, hours, locale) {
|
||||
const options = Number(hours) > 48
|
||||
? { day: '2-digit', month: '2-digit', hour: '2-digit' }
|
||||
@@ -142,7 +181,7 @@ function drawChart(canvas, payload, locale) {
|
||||
renderLegend(series);
|
||||
const allPoints = series.flatMap(item => item.points.map(point => ({ ...point, value: Number(point.value) })))
|
||||
.filter(point => Number.isFinite(point.value) && Number.isFinite(new Date(point.timestamp).getTime()));
|
||||
if (!allPoints.length) return drawEmpty(canvas, payload.no_data_label || 'No data');
|
||||
if (!allPoints.length) return drawEmpty(canvas, payload.no_data_label || publicTr('publicChart.noData'));
|
||||
|
||||
const { ctx, width, height } = prepareCanvas(canvas);
|
||||
const text = cssColor('--muted', '#888');
|
||||
@@ -196,7 +235,6 @@ function drawChart(canvas, payload, locale) {
|
||||
}
|
||||
|
||||
let publicChartPayload = null;
|
||||
let publicChartLocale = 'en-GB';
|
||||
let publicChartResizeFrame = 0;
|
||||
|
||||
function publicChartToken() {
|
||||
@@ -208,17 +246,16 @@ function requestedPublicChartHours() {
|
||||
return Number.isInteger(value) && value >= 1 && value <= 87600 ? value : null;
|
||||
}
|
||||
|
||||
function setPublicRangeLanguage(lang) {
|
||||
const pl = lang === 'pl';
|
||||
function setPublicRangeLanguage() {
|
||||
const label = $('#publicChartRangeLabel');
|
||||
const select = $('#publicChartHours');
|
||||
if (label) label.textContent = pl ? 'Zakres' : 'Range';
|
||||
if (label) label.textContent = publicTr('history.range');
|
||||
if (!select) return;
|
||||
const labels = pl
|
||||
? ['6 godzin', '24 godziny', '7 dni', '30 dni', '90 dni', '1 rok']
|
||||
: ['6 hours', '24 hours', '7 days', '30 days', '90 days', '1 year'];
|
||||
[...select.options].forEach((option, index) => { if (labels[index]) option.textContent = labels[index]; });
|
||||
select.setAttribute('aria-label', pl ? 'Zakres wykresu' : 'Chart range');
|
||||
[...select.options].forEach((option, index) => {
|
||||
const key = PUBLIC_RANGE_KEYS[index];
|
||||
if (key) option.textContent = publicTr(key);
|
||||
});
|
||||
select.setAttribute('aria-label', publicTr('publicChart.rangeAria'));
|
||||
}
|
||||
|
||||
function syncPublicRangeControl(hours) {
|
||||
@@ -228,7 +265,7 @@ function syncPublicRangeControl(hours) {
|
||||
if (![...select.options].some(option => option.value === value)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = `${value} h`;
|
||||
option.textContent = publicTr('publicChart.hoursShort', { hours: value });
|
||||
select.append(option);
|
||||
}
|
||||
select.value = value;
|
||||
@@ -255,15 +292,16 @@ async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
const range = $('#publicChartHours');
|
||||
|
||||
if (!token.startsWith('chart_')) {
|
||||
title.textContent = 'Custom chart';
|
||||
title.textContent = publicTr('publicChart.title');
|
||||
hint.textContent = '';
|
||||
error.hidden = false;
|
||||
error.textContent = 'Invalid chart link.';
|
||||
error.textContent = publicTr('publicChart.invalidLink');
|
||||
drawEmpty(canvas, error.textContent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (range) range.disabled = true;
|
||||
if (hint) hint.textContent = publicTr('publicChart.loading');
|
||||
error.hidden = true;
|
||||
const response = await fetch(withBase(publicChartApiPath(token, hours)), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
@@ -272,17 +310,17 @@ async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
throw new Error(message);
|
||||
}
|
||||
const payload = await response.json();
|
||||
const lang = payload.lang === 'pl' ? 'pl' : 'en';
|
||||
const locale = localeFor(lang);
|
||||
document.documentElement.lang = payload.lang || lang;
|
||||
title.textContent = payload.title || (lang === 'pl' ? 'Wykres niestandardowy' : 'Custom chart');
|
||||
hint.textContent = payload.hint || '';
|
||||
await setPublicChartLanguage(payload.lang || publicChartLanguage);
|
||||
title.textContent = payload.title || publicTr('publicChart.title');
|
||||
hint.textContent = payload.hint || publicTr('publicChart.periodHint', { hours: payload.hours });
|
||||
canvas.setAttribute('aria-label', title.textContent);
|
||||
setPublicRangeLanguage(lang);
|
||||
syncPublicRangeControl(payload.hours);
|
||||
publicChartPayload = payload;
|
||||
publicChartLocale = locale;
|
||||
drawChart(canvas, payload, locale);
|
||||
const localizedSeries = (payload.series || []).map(item => ({
|
||||
...item,
|
||||
label: item.label_key ? `${item.label} · ${publicTr(item.label_key)}` : item.label,
|
||||
}));
|
||||
publicChartPayload = { ...payload, series: localizedSeries, no_data_label: payload.no_data_label || publicTr('publicChart.noData') };
|
||||
drawChart(canvas, publicChartPayload, publicChartLocale);
|
||||
if (range) range.disabled = false;
|
||||
}
|
||||
|
||||
@@ -297,7 +335,7 @@ $('#publicChartHours')?.addEventListener('change', async event => {
|
||||
} catch (error) {
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = `${document.documentElement.lang === 'pl' ? 'Nie udało się wczytać wykresu' : 'Unable to load chart'}: ${error.message}`;
|
||||
host.textContent = publicTr('publicChart.loadFailed', { error: error.message });
|
||||
event.currentTarget.disabled = false;
|
||||
}
|
||||
});
|
||||
@@ -307,11 +345,24 @@ window.addEventListener('resize', () => {
|
||||
publicChartResizeFrame = requestAnimationFrame(redrawPublicChart);
|
||||
});
|
||||
|
||||
loadPublicChart().catch(error => {
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = `Unable to load chart: ${error.message}`;
|
||||
$('#publicChartHint').textContent = '';
|
||||
$('#publicChartHours').disabled = false;
|
||||
drawEmpty($('#publicCustomChart'), host.textContent);
|
||||
});
|
||||
async function initPublicChart() {
|
||||
try {
|
||||
await setPublicChartLanguage(publicCookie('gree_controller_language') || PUBLIC_DEFAULT_LANGUAGE);
|
||||
} catch (error) {
|
||||
console.error('Public chart language initialization failed:', error);
|
||||
}
|
||||
try {
|
||||
await loadPublicChart();
|
||||
} catch (error) {
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = publicChartTranslations['publicChart.loadFailed']
|
||||
? publicTr('publicChart.loadFailed', { error: error.message })
|
||||
: error.message;
|
||||
$('#publicChartHint').textContent = '';
|
||||
$('#publicChartHours').disabled = false;
|
||||
drawEmpty($('#publicCustomChart'), host.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
initPublicChart();
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
(() => {
|
||||
const row = document.cookie.split('; ').find(item => item.startsWith('gree_controller_language='));
|
||||
const language = row ? decodeURIComponent(row.split('=').slice(1).join('=')) : 'en';
|
||||
document.documentElement.lang = language || 'en';
|
||||
const language = row ? decodeURIComponent(row.split('=').slice(1).join('=')) : document.documentElement.lang;
|
||||
if (language) document.documentElement.lang = language;
|
||||
document.documentElement.classList.add('i18n-loading');
|
||||
})();
|
||||
Reference in New Issue
Block a user