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.
261 lines
11 KiB
HTML
261 lines
11 KiB
HTML
{% 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 %}
|
||
· <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 %}
|