Add persistent per-screen brightness, standby and clock settings
This commit is contained in:
@@ -10,6 +10,38 @@ REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
|
||||
REFS = {'cyd': 'main', 'guition': 'main'}
|
||||
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement'.split())
|
||||
|
||||
# Additive schema 1 extension. An absent object retains old firmware/YAML defaults.
|
||||
SETTING_RULES = {
|
||||
'standby_enabled': (True, None, None),
|
||||
'standby_seconds': (600, 60, 86400),
|
||||
'brightness': (100, 5, 100),
|
||||
'standby_brightness': (20, 0, 100),
|
||||
'night_enabled': (True, None, None),
|
||||
'night_start': (1320, 0, 1439),
|
||||
'night_end': (420, 0, 1439),
|
||||
'night_brightness': (10, 0, 100),
|
||||
'show_clock': (True, None, None),
|
||||
'clock_24h': (True, None, None),
|
||||
'home_on_standby': (False, None, None),
|
||||
}
|
||||
|
||||
def validate_settings(data):
|
||||
if not isinstance(data, dict) or set(data) - SETTING_RULES.keys():
|
||||
raise ValueError('Onbekende scherminstellingen; vernieuw de beheerpagina.')
|
||||
clean = {}
|
||||
for key, (default, minimum, maximum) in SETTING_RULES.items():
|
||||
value = data.get(key, default)
|
||||
if minimum is None:
|
||||
valid = type(value) is bool
|
||||
else:
|
||||
valid = type(value) is int and minimum <= value <= maximum
|
||||
if not valid:
|
||||
raise ValueError(f'Ongeldige waarde voor {key}.')
|
||||
clean[key] = value
|
||||
if max(clean['standby_brightness'], clean['night_brightness']) > clean['brightness']:
|
||||
raise ValueError('Helderheid in standby en nacht mag niet hoger zijn dan normaal.')
|
||||
return clean
|
||||
|
||||
def entity_id(value):
|
||||
return isinstance(value, str) and len(value) <= 120 and re.fullmatch(r'[a-z0-9_]+\.[a-z0-9_]+', value) and value.split('.')[0] in DOMAINS
|
||||
|
||||
@@ -35,7 +67,10 @@ def validate_layout(data):
|
||||
raise ValueError('Een tegelnaam mag maximaal 80 bytes bevatten.')
|
||||
seen.add(tile['entity'])
|
||||
clean.append({'entity': tile['entity'], 'name': name.strip()})
|
||||
return {'title': title.strip(), 'tiles': clean}
|
||||
result = {'title': title.strip(), 'tiles': clean}
|
||||
if 'settings' in data:
|
||||
result['settings'] = validate_settings(data['settings'])
|
||||
return result
|
||||
|
||||
def state_message(index, tile, states):
|
||||
state = states.get(tile['entity'], {})
|
||||
|
||||
@@ -130,6 +130,9 @@ class Manager:
|
||||
if inbox not in {s['id'] for s in self.inventory()[0]}:
|
||||
raise ValueError('Dit is geen gekoppeld ESP-scherm. Vernieuw het overzicht.')
|
||||
layout = validate_layout(data)
|
||||
# A still-open older UI may save tiles without the new optional settings.
|
||||
if 'settings' not in layout and 'settings' in self.layouts.get(inbox, {}):
|
||||
layout['settings'] = self.layouts[inbox]['settings'].copy()
|
||||
known = {e['id'] for e in self.inventory()[1]}
|
||||
if any(t['entity'] not in known for t in layout['tiles']):
|
||||
raise ValueError('Een gekozen entiteit bestaat niet meer. Zoek de nieuwe entiteit op.')
|
||||
@@ -149,6 +152,8 @@ class Manager:
|
||||
|
||||
async def sync_one(self, inbox, layout, force=False):
|
||||
messages = [{'v': 1, 'op': 'layout', 'title': layout['title'], 'entities': [t['entity'] for t in layout['tiles']]}]
|
||||
if 'settings' in layout:
|
||||
messages[0]['settings'] = layout['settings']
|
||||
messages += [state_message(i, tile, self.ha.states) for i, tile in enumerate(layout['tiles'])]
|
||||
previous = self.sent.get(inbox, [])
|
||||
force = force or not previous or messages[0] != previous[0]
|
||||
|
||||
@@ -65,6 +65,99 @@ function select(id) {
|
||||
renderScreens();
|
||||
renderTiles();
|
||||
renderResults();
|
||||
renderSettings();
|
||||
}
|
||||
const settingDefinitions = [
|
||||
["standby_enabled", "Automatisch standby", "check", true],
|
||||
["standby_seconds", "Standby na", "minutes", 600, 1, 1440],
|
||||
["brightness", "Helderheid normaal", "range", 100, 5, 100],
|
||||
["standby_brightness", "Helderheid in standby", "range", 20, 0, 100],
|
||||
["night_enabled", "Nachtstand gebruiken", "check", true],
|
||||
["night_brightness", "Helderheid in nachtstand", "range", 10, 0, 100],
|
||||
["night_start", "Nachtstand vanaf", "time", 1320],
|
||||
["night_end", "Nachtstand tot", "time", 420],
|
||||
["show_clock", "Klok tonen", "check", true],
|
||||
["clock_24h", "24-uursklok (uit = 12 uur)", "check", true],
|
||||
["home_on_standby", "Na standby terug naar pagina 1", "check", false],
|
||||
];
|
||||
function renderSettings() {
|
||||
const values = {
|
||||
...Object.fromEntries(
|
||||
settingDefinitions.map(([key, , , value]) => [key, value]),
|
||||
),
|
||||
...layout.settings,
|
||||
};
|
||||
const container = $("#settings-fields");
|
||||
container.replaceChildren();
|
||||
for (const [key, title, kind, , min, max] of settingDefinitions) {
|
||||
const label = node(
|
||||
"label",
|
||||
undefined,
|
||||
`setting ${kind === "check" ? "setting-check" : ""}`,
|
||||
);
|
||||
const caption = node("span", title),
|
||||
input = node("input"),
|
||||
output = node("output");
|
||||
input.id = `setting-${key}`;
|
||||
input.type =
|
||||
kind === "check" ? "checkbox" : kind === "minutes" ? "number" : kind;
|
||||
input.setAttribute("aria-label", title);
|
||||
input.required = kind === "minutes" || kind === "time";
|
||||
if (kind === "check") input.checked = values[key];
|
||||
else if (kind === "time")
|
||||
input.value = `${String(Math.floor(values[key] / 60)).padStart(2, "0")}:${String(values[key] % 60).padStart(2, "0")}`;
|
||||
else {
|
||||
input.min = min;
|
||||
input.max = max;
|
||||
input.step = 1;
|
||||
input.value = kind === "minutes" ? values[key] / 60 : values[key];
|
||||
}
|
||||
const updateOutput = () =>
|
||||
(output.textContent =
|
||||
kind === "range"
|
||||
? `${input.value}%`
|
||||
: kind === "minutes"
|
||||
? "minuten"
|
||||
: "");
|
||||
updateOutput();
|
||||
input.oninput = () => {
|
||||
if (!input.checkValidity() || input.value === "") return;
|
||||
let value = kind === "check" ? input.checked : Number(input.value);
|
||||
if (kind === "time") {
|
||||
const [h, m] = input.value.split(":").map(Number);
|
||||
value = h * 60 + m;
|
||||
}
|
||||
if (kind === "minutes") value *= 60;
|
||||
layout.settings = { ...values, ...layout.settings, [key]: value };
|
||||
// A lower normal brightness also lowers any higher standby levels.
|
||||
if (key === "brightness")
|
||||
for (const dim of ["standby_brightness", "night_brightness"]) {
|
||||
layout.settings[dim] = Math.min(layout.settings[dim], value);
|
||||
const other = $(`#setting-${dim}`);
|
||||
other.value = layout.settings[dim];
|
||||
other.parentElement.querySelector("output").textContent =
|
||||
`${other.value}%`;
|
||||
}
|
||||
updateOutput();
|
||||
markDirty();
|
||||
};
|
||||
label.append(caption, input, output);
|
||||
container.append(label);
|
||||
}
|
||||
renderSettingsSupport();
|
||||
}
|
||||
function renderSettingsSupport() {
|
||||
const version =
|
||||
inventory.screens.find((s) => s.id === selected)?.firmware || "";
|
||||
const parts = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(version);
|
||||
const supported =
|
||||
parts &&
|
||||
(Number(parts[1]) > 0 ||
|
||||
Number(parts[2]) > 1 ||
|
||||
(Number(parts[2]) === 1 && Number(parts[3]) >= 2));
|
||||
$("#settings-support").textContent = supported
|
||||
? "Opslaan stuurt je instellingen direct naar dit scherm. Ze blijven ook na een herstart bewaard."
|
||||
: "Eenmalig firmware 0.1.2 of nieuwer installeren via ESPHome. Je kunt de instellingen alvast bewaren; oudere firmware gebruikt ze nog niet.";
|
||||
}
|
||||
function renderScreens() {
|
||||
$("#screens").replaceChildren();
|
||||
@@ -220,6 +313,7 @@ async function refresh() {
|
||||
: "Verbinding met Home Assistant herstellen…";
|
||||
$("#connection").classList.toggle("online", inventory.connected);
|
||||
renderScreens();
|
||||
if (selected) renderSettingsSupport();
|
||||
if (!selected && inventory.screens.length) select(inventory.screens[0].id);
|
||||
} catch {
|
||||
$("#connection").textContent =
|
||||
@@ -228,6 +322,9 @@ async function refresh() {
|
||||
}
|
||||
$("#save").onclick = async () => {
|
||||
if (busy) return;
|
||||
for (const input of document.querySelectorAll("#settings-fields input")) {
|
||||
if (!input.reportValidity()) return;
|
||||
}
|
||||
busy = true;
|
||||
$("#save").disabled = true;
|
||||
try {
|
||||
|
||||
@@ -86,6 +86,19 @@
|
||||
<div id="results" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="screen-settings" open>
|
||||
<summary>
|
||||
Scherminstellingen
|
||||
<span class="hint">Helderheid, rustmomenten en weergave</span>
|
||||
</summary>
|
||||
<p class="hint" id="settings-support"></p>
|
||||
<div class="settings-grid" id="settings-fields"></div>
|
||||
<p class="hint">
|
||||
Nachturen gelden voor de helderheid tijdens standby. De eerste
|
||||
aanraking maakt het scherm wakker; de volgende bedient je huis.
|
||||
Gelijke begin- en eindtijd schakelen het nachtvenster uit.
|
||||
</p>
|
||||
</details>
|
||||
<footer class="savebar">
|
||||
<div>
|
||||
<strong id="dirty">Alles opgeslagen</strong
|
||||
|
||||
@@ -563,3 +563,56 @@ textarea {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-settings {
|
||||
margin: 28px 0 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 20px;
|
||||
}
|
||||
.screen-settings summary {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px 28px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.setting {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.setting output {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.setting input[type="range"] {
|
||||
padding: 0;
|
||||
accent-color: var(--accent);
|
||||
min-height: 32px;
|
||||
}
|
||||
.setting-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.setting-check input {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.setting-check output {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user