Files
location_managemet/app/templates/sonoff/gateway.html
ske087 30806560a6 Fix SonoffLAN signing key: hardcode pre-computed HMAC key
The previous _compute_sign_key() function indexed into a base64 string
derived from the full SonoffLAN REGIONS dict (~243 entries). Our partial
dict only produced a 7876-char a string but needed index 7872+, so the
function must use the full dict.

Solution: pre-compute the key once from the full dict and hardcode the
resulting 32-byte ASCII key. This is deterministic — the SonoffLAN
algorithm always produces the same output regardless of when it runs.

The sonoff_ewelink driver now loads cleanly alongside all other drivers.
2026-02-26 20:13:07 +02:00

261 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ board.name }} Sonoff Gateway{% endblock %}
{% block content %}
<div class="container-fluid py-3">
{# ── Page header ──────────────────────────────────────────────────── #}
<div class="d-flex align-items-center justify-content-between mb-4 gap-2 flex-wrap">
<div class="d-flex align-items-center gap-3">
<a href="{{ url_for('boards.list_boards') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i>
</a>
<div>
<h4 class="mb-0 fw-bold">
<i class="bi bi-cloud-lightning-fill text-warning me-2"></i>{{ board.name }}
</h4>
<small class="text-muted">Sonoff eWeLink Gateway
{% if board.is_online %}
<span class="badge bg-success ms-2">Online</span>
{% else %}
<span class="badge bg-secondary ms-2">Offline</span>
{% endif %}
</small>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
{% if current_user.is_admin() %}
<form method="post" action="{{ url_for('sonoff.sync_devices', board_id=board.id) }}">
<button class="btn btn-primary btn-sm">
<i class="bi bi-arrow-repeat me-1"></i>Sync Devices
</button>
</form>
<a href="{{ url_for('sonoff.auth_settings', board_id=board.id) }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-key me-1"></i>eWeLink Settings
</a>
{% endif %}
</div>
</div>
{# ── Flash messages ───────────────────────────────────────────────── #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show py-2" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{# ── No credentials warning ───────────────────────────────────────── #}
{% if not has_credentials %}
<div class="alert alert-warning d-flex align-items-center gap-3" role="alert">
<i class="bi bi-exclamation-triangle-fill fs-4"></i>
<div>
<strong>eWeLink credentials not configured.</strong>
<a href="{{ url_for('sonoff.auth_settings', board_id=board.id) }}" class="alert-link">
Set up your account
</a> to discover and control devices.
</div>
</div>
{% endif %}
{# ── Device grid ──────────────────────────────────────────────────── #}
{% if devices %}
<div class="row g-3">
{% for dev in devices %}
<div class="col-12 col-md-6 col-xl-4" id="sonoff-card-{{ dev.device_id }}">
<div class="card bg-dark border-secondary h-100">
<div class="card-header d-flex align-items-center justify-content-between py-2">
<div class="d-flex align-items-center gap-2">
{# Kind icon #}
{% set kind_icon = {
'switch': 'bi-toggles', 'light': 'bi-lightbulb-fill',
'fan': 'bi-fan', 'sensor': 'bi-thermometer-half',
'remote': 'bi-broadcast', 'cover': 'bi-door-open'
}.get(dev.kind, 'bi-plug') %}
<i class="bi {{ kind_icon }} text-warning"></i>
<span class="fw-semibold text-truncate" style="max-width:160px"
title="{{ dev.name }}">{{ dev.name or dev.device_id }}</span>
</div>
<div class="d-flex align-items-center gap-2">
{% if dev.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
<a href="{{ url_for('sonoff.device_detail', board_id=board.id, device_id=dev.device_id) }}"
class="btn btn-outline-secondary btn-sm px-2 py-0">
<i class="bi bi-sliders"></i>
</a>
</div>
</div>
<div class="card-body py-2">
<div class="text-muted small mb-2">
{{ dev.device_label }}
{% if dev.firmware %}<span class="ms-2 opacity-50">v{{ dev.firmware }}</span>{% endif %}
</div>
{# ── Switch/relay device ────────────────────────────────────── #}
{% if dev.kind in ('switch', 'light', 'fan') and dev.num_channels > 0 %}
<div class="d-flex flex-wrap gap-2">
{% for ch in dev.all_channel_states() %}
{% set ch_on = ch.state %}
<div class="d-flex align-items-center gap-2 border border-secondary rounded px-2 py-1"
id="ch-{{ dev.device_id }}-{{ ch.channel }}"
style="min-width:min-content">
<span class="small text-muted">{{ ch.label }}</span>
<div class="d-flex gap-1">
<button
class="btn btn-sm px-2 py-0 sonoff-ch-btn {{ 'btn-success' if ch_on else 'btn-outline-success' }}"
data-url="{{ url_for('sonoff.control_channel', board_id=board.id, device_id=dev.device_id, channel=ch.channel, action='on') }}"
data-device="{{ dev.device_id }}" data-ch="{{ ch.channel }}" data-target="on"
title="Turn ON"
>ON</button>
<button
class="btn btn-sm px-2 py-0 sonoff-ch-btn {{ 'btn-danger' if not ch_on else 'btn-outline-danger' }}"
data-url="{{ url_for('sonoff.control_channel', board_id=board.id, device_id=dev.device_id, channel=ch.channel, action='off') }}"
data-device="{{ dev.device_id }}" data-ch="{{ ch.channel }}" data-target="off"
title="Turn OFF"
>OFF</button>
</div>
</div>
{% endfor %}
</div>
{# ── Sensor device ──────────────────────────────────────────── #}
{% elif dev.kind == 'sensor' %}
<div class="d-flex flex-wrap gap-3">
{% if dev.temperature is not none %}
<div class="text-center">
<div class="fs-4 fw-bold text-info">{{ dev.temperature }}°C</div>
<div class="small text-muted">Temperature</div>
</div>
{% endif %}
{% if dev.humidity is not none %}
<div class="text-center">
<div class="fs-4 fw-bold text-primary">{{ dev.humidity }}%</div>
<div class="small text-muted">Humidity</div>
</div>
{% endif %}
</div>
{# ── Power meter ────────────────────────────────────────────── #}
{% if dev.has_power_meter %}
<div class="d-flex flex-wrap gap-3 mt-2">
{% if dev.power is not none %}
<div class="text-center">
<div class="fs-5 fw-bold text-warning">{{ dev.power }}W</div>
<div class="small text-muted">Power</div>
</div>
{% endif %}
{% if dev.voltage is not none %}
<div class="text-center">
<div class="fs-5 fw-bold text-success">{{ dev.voltage }}V</div>
<div class="small text-muted">Voltage</div>
</div>
{% endif %}
{% if dev.current is not none %}
<div class="text-center">
<div class="fs-5 fw-bold text-danger">{{ dev.current }}A</div>
<div class="small text-muted">Current</div>
</div>
{% endif %}
</div>
{% endif %}
{# ── RF Bridge / Remote ─────────────────────────────────────── #}
{% elif dev.kind == 'remote' %}
<div class="text-muted small"><i class="bi bi-broadcast me-1"></i>RF Bridge cloud only</div>
{% endif %}
<div class="text-muted mt-2" style="font-size:.7rem">
ID: {{ dev.device_id }}
{% if dev.ip_address %}
&nbsp;·&nbsp;<i class="bi bi-wifi"></i> {{ dev.ip_address }}
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-cloud-slash fs-1 d-block mb-3"></i>
<p class="fs-5">No devices found.</p>
{% if has_credentials %}
<form method="post" action="{{ url_for('sonoff.sync_devices', board_id=board.id) }}">
<button class="btn btn-primary">
<i class="bi bi-arrow-repeat me-1"></i>Sync from eWeLink
</button>
</form>
{% else %}
<a href="{{ url_for('sonoff.auth_settings', board_id=board.id) }}" class="btn btn-warning">
<i class="bi bi-key me-1"></i>Configure eWeLink Account
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
const _socket = io();
// ── AJAX channel control ──────────────────────────────────────────────────────
document.querySelectorAll(".sonoff-ch-btn").forEach(btn => {
btn.addEventListener("click", async function() {
const url = this.dataset.url;
const did = this.dataset.device;
const ch = parseInt(this.dataset.ch);
const tgt = this.dataset.target; // "on" or "off"
this.disabled = true;
try {
const resp = await fetch(url, {
method: "POST",
headers: { "Accept": "application/json",
"X-Requested-With": "XMLHttpRequest" },
});
const data = await resp.json();
if (data.ok) {
_applyChannelState(did, ch, data.state);
}
} catch(e) { console.error(e); }
finally { this.disabled = false; }
});
});
function _applyChannelState(did, ch, isOn) {
const container = document.getElementById(`ch-${did}-${ch}`);
if (!container) return;
const [onBtn, offBtn] = container.querySelectorAll(".sonoff-ch-btn");
if (isOn) {
onBtn.className = onBtn.className.replace("btn-outline-success", "btn-success");
offBtn.className = offBtn.className.replace("btn-danger", "btn-outline-danger");
} else {
onBtn.className = onBtn.className.replace("btn-success", "btn-outline-success");
offBtn.className = offBtn.className.replace("btn-outline-danger", "btn-danger");
}
}
// ── SocketIO live updates ─────────────────────────────────────────────────────
_socket.on("sonoff_update", function(data) {
if (data.board_id !== {{ board.id }}) return;
_applyChannelState(data.device_id, data.channel, data.state);
// Optional: flash the card border briefly
const card = document.getElementById("sonoff-card-" + data.device_id);
if (card) {
card.querySelector(".card").classList.add("border-info");
setTimeout(() => card.querySelector(".card").classList.remove("border-info"), 800);
}
});
</script>
{% endblock %}