Files
location_managemet/app/templates/sonoff/device.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

282 lines
12 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 %}{{ device.name or device.device_id }} Sonoff Device{% endblock %}
{% block content %}
<div class="container py-3" style="max-width:720px">
{# breadcrumb #}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0 small">
<li class="breadcrumb-item">
<a href="{{ url_for('boards.list_boards') }}" class="text-decoration-none">Boards</a>
</li>
<li class="breadcrumb-item">
<a href="{{ url_for('sonoff.gateway', board_id=board.id) }}" class="text-decoration-none">
{{ board.name }}
</a>
</li>
<li class="breadcrumb-item active">{{ device.name or device.device_id }}</li>
</ol>
</nav>
{% 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 %}
{# ── Device header card ───────────────────────────────────────────── #}
<div class="card bg-dark border-secondary mb-3">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap">
<div>
<h4 class="mb-1 fw-bold">
{% 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(device.kind, 'bi-plug') %}
<i class="bi {{ kind_icon }} text-warning me-2"></i>
{{ device.name or device.device_id }}
</h4>
<div class="text-muted small">
{{ device.device_label }}
{% if device.firmware %} &nbsp;·&nbsp; v{{ device.firmware }}{% endif %}
{% if device.model %} &nbsp;·&nbsp; {{ device.model }}{% endif %}
</div>
<div class="mt-1">
{% if device.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
</div>
</div>
{# Rename form #}
{% if current_user.is_admin() %}
<form method="post" action="{{ url_for('sonoff.rename_device', board_id=board.id, device_id=device.device_id) }}"
class="d-flex gap-2 align-items-center">
<input type="text" name="name" class="form-control form-control-sm"
value="{{ device.name }}" maxlength="64" placeholder="Device name" style="width:180px">
<button type="submit" class="btn btn-outline-light btn-sm">
<i class="bi bi-pencil"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
{# ── Channel controls ─────────────────────────────────────────────── #}
{% if device.kind in ('switch', 'light', 'fan') and device.num_channels > 0 %}
<div class="card bg-dark border-secondary mb-3">
<div class="card-header small text-muted fw-semibold text-uppercase">
<i class="bi bi-toggles me-1"></i>Channels
</div>
<div class="card-body py-2">
<div class="row g-3">
{% for ch in device.all_channel_states() %}
{% set ch_on = ch.state %}
<div class="col-6 col-sm-4" id="ch-{{ device.device_id }}-{{ ch.channel }}">
<div class="border border-secondary rounded p-2 text-center">
<div class="small text-muted mb-2">{{ ch.label }}</div>
<div class="d-flex justify-content-center gap-2">
<button
class="btn btn-sm 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=device.device_id, channel=ch.channel, action='on') }}"
data-device="{{ device.device_id }}" data-ch="{{ ch.channel }}" data-target="on">ON
</button>
<button
class="btn btn-sm 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=device.device_id, channel=ch.channel, action='off') }}"
data-device="{{ device.device_id }}" data-ch="{{ ch.channel }}" data-target="off">OFF
</button>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# ── Sensor readings ──────────────────────────────────────────────── #}
{% if device.temperature is not none or device.humidity is not none or device.power is not none %}
<div class="card bg-dark border-secondary mb-3">
<div class="card-header small text-muted fw-semibold text-uppercase">
<i class="bi bi-bar-chart-fill me-1"></i>Sensor Readings
</div>
<div class="card-body">
<div class="row text-center g-3">
{% if device.temperature is not none %}
<div class="col-4">
<div class="fs-3 fw-bold text-info">{{ device.temperature }}°C</div>
<div class="small text-muted">Temperature</div>
</div>
{% endif %}
{% if device.humidity is not none %}
<div class="col-4">
<div class="fs-3 fw-bold text-primary">{{ device.humidity }}%</div>
<div class="small text-muted">Humidity</div>
</div>
{% endif %}
{% if device.power is not none %}
<div class="col-4">
<div class="fs-3 fw-bold text-warning">{{ device.power }}W</div>
<div class="small text-muted">Power</div>
</div>
{% endif %}
{% if device.voltage is not none %}
<div class="col-4">
<div class="fs-3 fw-bold text-success">{{ device.voltage }}V</div>
<div class="small text-muted">Voltage</div>
</div>
{% endif %}
{% if device.current is not none %}
<div class="col-4">
<div class="fs-3 fw-bold text-danger">{{ device.current }}A</div>
<div class="small text-muted">Current</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{# ── Device info ──────────────────────────────────────────────────── #}
<div class="card bg-dark border-secondary mb-3">
<div class="card-header small text-muted fw-semibold text-uppercase">
<i class="bi bi-info-circle me-1"></i>Device Info
</div>
<div class="card-body p-0">
<table class="table table-dark table-sm mb-0">
<tbody>
<tr><th class="ps-3 text-muted" width="140">Device ID</th><td><code>{{ device.device_id }}</code></td></tr>
<tr><th class="ps-3 text-muted">UIID</th><td>{{ device.uiid }} ({{ device.device_label }})</td></tr>
<tr><th class="ps-3 text-muted">Type</th><td>{{ device.kind }}</td></tr>
<tr><th class="ps-3 text-muted">Channels</th><td>{{ device.num_channels }}</td></tr>
{% if device.firmware %}
<tr><th class="ps-3 text-muted">Firmware</th><td>{{ device.firmware }}</td></tr>
{% endif %}
<tr>
<th class="ps-3 text-muted">LAN IP</th>
<td>{{ device.ip_address or '<span class="text-muted">Not detected</span>'|safe }}</td>
</tr>
<tr>
<th class="ps-3 text-muted">Connection</th>
<td>
{% if device.ip_address %}
<span class="text-success"><i class="bi bi-wifi me-1"></i>LAN available</span>
{% else %}
<span class="text-warning"><i class="bi bi-cloud me-1"></i>Cloud only</span>
{% endif %}
</td>
</tr>
{% if device.last_seen %}
<tr>
<th class="ps-3 text-muted">Last Seen</th>
<td>{{ device.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{# ── Set LAN IP manually ──────────────────────────────────────────── #}
{% if current_user.is_admin() %}
<div class="card bg-dark border-secondary mb-3">
<div class="card-header small text-muted fw-semibold text-uppercase">
<i class="bi bi-wifi me-1"></i>Manual LAN Override
</div>
<div class="card-body">
<p class="text-muted small mb-3">
By default the LAN IP is auto-detected during cloud sync.
You can override it here if the device is reachable on a static IP.
</p>
<form method="post"
action="{{ url_for('sonoff.set_device_ip', board_id=board.id, device_id=device.device_id) }}"
class="row g-2 align-items-end">
<div class="col-sm-6">
<label class="form-label form-label-sm">IP Address</label>
<input type="text" name="ip" class="form-control form-control-sm"
value="{{ device.ip_address }}" placeholder="192.168.1.100">
</div>
<div class="col-sm-3">
<label class="form-label form-label-sm">Port</label>
<input type="number" name="port" class="form-control form-control-sm"
value="{{ device.port or 8081 }}" min="1" max="65535">
</div>
<div class="col-sm-3">
<button type="submit" class="btn btn-sm btn-outline-light w-100">
<i class="bi bi-save me-1"></i>Save
</button>
</div>
</form>
</div>
</div>
{% endif %}
{# ── Raw params ───────────────────────────────────────────────────── #}
<div class="card bg-dark border-secondary">
<a class="card-header text-decoration-none text-muted small fw-semibold text-uppercase d-flex justify-content-between"
data-bs-toggle="collapse" href="#rawParams">
<span><i class="bi bi-code-slash me-1"></i>Raw Params</span>
<i class="bi bi-chevron-down"></i>
</a>
<div class="collapse" id="rawParams">
<div class="card-body">
<pre class="text-muted small mb-0" style="white-space:pre-wrap;word-break:break-all">{{ device.params_json }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const _socket = io();
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);
this.disabled = true;
try {
const resp = await fetch(url, {
method: "POST",
headers: { "Accept": "application/json" },
});
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");
}
}
_socket.on("sonoff_update", function(data) {
if (data.device_id !== "{{ device.device_id }}") return;
_applyChannelState(data.device_id, data.channel, data.state);
});
</script>
{% endblock %}