231 lines
20 KiB
HTML
231 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||
<meta name="theme-color" content="#0a1110" id="themeColorMeta">
|
||
<meta name="description" content="Local GREE air conditioner controller" data-i18n-content="meta.description">
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||
<link rel="stylesheet" href="/styles.css">
|
||
<script>
|
||
(() => {
|
||
const getCookie = name => document.cookie.split('; ').find(row => row.startsWith(`${name}=`))?.split('=')[1];
|
||
const requested = decodeURIComponent(getCookie('gree_controller_theme') || 'system');
|
||
const resolved = requested === 'light' || requested === 'dark'
|
||
? requested
|
||
: (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||
document.documentElement.dataset.theme = resolved;
|
||
})();
|
||
</script>
|
||
<title>GREE Controller</title>
|
||
</head>
|
||
<body>
|
||
<header class="topbar">
|
||
<div class="brand">
|
||
<div><strong>GREE Controller</strong><small id="connectionLabel" data-i18n="status.connecting">Connecting…</small></div>
|
||
</div>
|
||
<div class="top-actions">
|
||
<select class="toolbar-select" id="languageSelect" data-i18n-aria="controls.language" aria-label="Language">
|
||
<option value="en">English</option>
|
||
</select>
|
||
<select class="toolbar-select theme-select" id="themeSelect" data-i18n-aria="controls.theme" aria-label="Theme">
|
||
<option value="system" data-i18n="theme.system">System</option>
|
||
<option value="light" data-i18n="theme.light">Light</option>
|
||
<option value="dark" data-i18n="theme.dark">Dark</option>
|
||
</select>
|
||
<button class="icon-button" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
||
<button class="primary compact" id="discoverButton" data-i18n="actions.discover">Discover</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<section class="view active" data-view="dashboard">
|
||
<div class="hero">
|
||
<div><span class="eyebrow" data-i18n="dashboard.home">Home</span><h1 data-i18n="dashboard.title">Comfort under control</h1><p id="summaryText" data-i18n="dashboard.loading">Loading devices…</p></div>
|
||
<div class="hero-temp" id="heroTemperature">--<small>°C</small></div>
|
||
</div>
|
||
<div class="metrics" id="metrics"></div>
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="nav.devices">Devices</span><h2 data-i18n="dashboard.quickControl">Quick control</h2></div></div>
|
||
<div class="device-grid" id="dashboardDevices"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="devices">
|
||
<div class="section-heading">
|
||
<div><span class="eyebrow" data-i18n="devices.lan">LAN network</span><h1 data-i18n="nav.devices">Devices</h1></div>
|
||
<button class="secondary" data-open="deviceDialog" data-i18n="devices.addManual">Add manually</button>
|
||
</div>
|
||
<div class="device-grid" id="deviceList"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="zones">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="zones.automation">Automation</span><h1 data-i18n="nav.zones">Zones</h1></div><button class="secondary" data-open="zoneDialog" data-i18n="zones.new">New zone</button></div>
|
||
<p class="lead" data-i18n="zones.description">A zone controls a device using temperature, hysteresis and minimum cycle time.</p>
|
||
<div class="list-grid" id="zoneList"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="schedules">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="schedules.calendar">Calendar</span><h1 data-i18n="nav.schedules">Schedules</h1></div><button class="secondary" data-open="scheduleDialog" data-i18n="actions.add">Add</button></div>
|
||
<div class="list-grid" id="scheduleList"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="automations">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="automations.rules">Rules</span><h1 data-i18n="nav.automations">Automations</h1></div><button class="secondary" data-open="automationDialog" data-i18n="actions.add">Add</button></div>
|
||
<div class="list-grid" id="automationList"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="history">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="history.measurements">Measurements</span><h1 data-i18n="history.title">Temperature history</h1></div></div>
|
||
<div class="panel chart-panel">
|
||
<div class="chart-toolbar">
|
||
<select id="historyDevice"></select>
|
||
<select id="historyHours">
|
||
<option value="6" data-i18n="history.6h">6 hours</option>
|
||
<option value="24" selected 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>
|
||
</select>
|
||
<button class="secondary" id="historyRefresh" data-i18n="actions.show">Show</button>
|
||
</div>
|
||
<div class="chart-wrap"><canvas id="historyChart" width="1000" height="420"></canvas></div>
|
||
<div class="legend"><span><i class="dot indoor"></i><span data-i18n="common.temperature">Temperature</span></span><span><i class="dot target"></i><span data-i18n="common.target">Target</span></span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="view" data-view="settings">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="settings.system">System</span><h1 data-i18n="nav.settings">Settings</h1></div></div>
|
||
<form class="panel form-grid" id="settingsForm">
|
||
<h3 data-i18n="settings.controller">Controller</h3>
|
||
<label><span data-i18n="settings.clientId">Client identifier</span><input name="controller_id" required></label>
|
||
<label><span data-i18n="settings.pollInterval">Poll interval (s)</span><input type="number" name="poll_interval_seconds" min="2" max="3600" required></label>
|
||
<label><span data-i18n="settings.zoneInterval">Zone interval (s)</span><input type="number" name="zone_interval_seconds" min="2" max="3600" required></label>
|
||
<label><span data-i18n="settings.broadcast">Broadcast address</span><input name="discovery_broadcast" placeholder="255.255.255.255:7000" required></label>
|
||
<label><span data-i18n="settings.discoveryTimeout">Discovery timeout (ms)</span><input type="number" name="discovery_timeout_ms" min="300" max="30000" required></label>
|
||
<label class="check"><input type="checkbox" name="simulator_enabled"> <span data-i18n="settings.simulationMode">Simulation mode</span></label>
|
||
<hr>
|
||
<h3 data-i18n="settings.haSensorInput">Home Assistant sensor input</h3>
|
||
<p class="field-note wide" data-i18n="settings.haSensorInputHint">Optional. Used only when a room zone reads an external Home Assistant temperature sensor.</p>
|
||
<label class="wide"><span>URL</span><input type="url" name="ha_url" placeholder="http://homeassistant.local:8123"></label>
|
||
<label class="wide"><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"><span data-i18n="settings.defaultEntity">Default entity_id</span><input name="ha_entity_id" placeholder="sensor.living_room_temperature"></label>
|
||
<div class="form-actions wide"><button type="button" class="secondary" id="haTest" data-i18n="settings.testHa">Test HA</button><button class="primary" type="submit" data-i18n="actions.save">Save</button></div>
|
||
<hr>
|
||
<h3 data-i18n="settings.haIntegrationAccess">Home Assistant integration access</h3>
|
||
<p class="field-note wide" data-i18n="settings.haIntegrationHint">Create a controller token and paste it into the GREE Controller integration in Home Assistant.</p>
|
||
<div class="wide token-manager">
|
||
<div id="accessTokenList" class="token-list"></div>
|
||
<div class="form-actions"><button type="button" class="primary" id="createAccessToken" data-i18n="settings.newToken">Create new token</button></div>
|
||
</div>
|
||
</form>
|
||
<div class="panel system-panel" id="systemInfo"></div>
|
||
</section>
|
||
|
||
<section class="view" data-view="logs">
|
||
<div class="section-heading"><div><span class="eyebrow" data-i18n="logs.diagnostics">Diagnostics</span><h1 data-i18n="nav.logs">Events</h1></div><button class="secondary" id="logsRefresh" data-i18n="actions.refresh">Refresh</button></div>
|
||
<div class="panel log-list" id="logList"></div>
|
||
</section>
|
||
</main>
|
||
|
||
<nav class="bottom-nav" data-i18n-aria="nav.navigation" aria-label="Navigation">
|
||
<button class="active" data-nav="dashboard"><span>⌂</span><b data-i18n="nav.dashboard">Dashboard</b></button>
|
||
<button data-nav="devices"><span>▣</span><b data-i18n="nav.devices">Devices</b></button>
|
||
<button data-nav="zones"><span>◎</span><b data-i18n="nav.zones">Zones</b></button>
|
||
<button data-nav="history"><span>⌁</span><b data-i18n="nav.history">History</b></button>
|
||
<button data-nav="more"><span>•••</span><b data-i18n="nav.more">More</b></button>
|
||
</nav>
|
||
|
||
<dialog id="moreDialog" class="sheet">
|
||
<div class="dialog-head"><h2 data-i18n="nav.more">More</h2><button data-close>×</button></div>
|
||
<div class="menu-list">
|
||
<button data-go="schedules"><span data-i18n="nav.schedules">Schedules</span><span>›</span></button>
|
||
<button data-go="automations"><span data-i18n="nav.automations">Automations</span><span>›</span></button>
|
||
<button data-go="settings"><span data-i18n="nav.settings">Settings</span><span>›</span></button>
|
||
<button data-go="logs"><span data-i18n="nav.logsAndEvents">Events and logs</span><span>›</span></button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="deviceDialog">
|
||
<form method="dialog" id="deviceForm" class="dialog-form">
|
||
<div class="dialog-head"><h2 data-i18n="devices.add">Add device</h2><button type="button" data-close>×</button></div>
|
||
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||
<label><span>MAC / CID</span><input name="mac" required placeholder="AABBCCDDEEFF"></label>
|
||
<label><span data-i18n="devices.ipAddress">IP address</span><input name="ip" required placeholder="192.168.1.50"></label>
|
||
<div class="two"><label><span>Port</span><input type="number" name="port" value="7000" min="1" max="65535"></label><label><span data-i18n="devices.protocol">Protocol</span><select name="protocol_version"><option value="1">V1 AES-ECB</option><option value="2">V2 AES-GCM</option></select></label></div>
|
||
<label><span data-i18n="devices.keyOptional">Key (optional)</span><input name="key" data-i18n-placeholder="devices.keyPlaceholder" placeholder="Leave empty for automatic bind"></label>
|
||
<label class="check"><input type="checkbox" name="simulated"> <span data-i18n="devices.simulated">Simulated device</span></label>
|
||
<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.add">Add</button></div>
|
||
</form>
|
||
</dialog>
|
||
|
||
<dialog id="zoneDialog">
|
||
<form method="dialog" id="zoneForm" class="dialog-form">
|
||
<input type="hidden" name="id">
|
||
<div class="dialog-head"><h2 data-i18n="common.zone">Zone</h2><button type="button" data-close>×</button></div>
|
||
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
|
||
<label><span data-i18n="common.device">Device</span><select name="device_id" required></select></label>
|
||
<div class="two"><label><span data-i18n="common.mode">Mode</span><select name="mode"><option value="cool" data-i18n="mode.cool">Cooling</option><option value="heat" data-i18n="mode.heat">Heating</option></select></label><label><span data-i18n="common.targetC">Target °C</span><input type="number" step="0.1" min="8" max="32" name="setpoint" value="23"></label></div>
|
||
<div class="two"><label><span data-i18n="zones.hysteresis">Hysteresis °C</span><input type="number" step="0.1" min="0.1" max="5" name="hysteresis" value="0.6"></label><label><span data-i18n="zones.source">Temperature strategy</span><select name="sensor_source"><option value="combined" selected data-i18n="zones.combinedSensor">GREE + room sensor</option><option value="device" data-i18n="zones.greeSensor">GREE only</option><option value="home_assistant" data-i18n="zones.externalSensor">Room sensor only</option></select></label></div>
|
||
<div id="externalSensorFields">
|
||
<label><span data-i18n="zones.roomSensorEntity">Room sensor entity_id</span><input name="ha_entity_id" 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="number" 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">The room sensor is assigned only to this zone. If it becomes unavailable, the controller falls back to the GREE sensor.</p>
|
||
</div>
|
||
<div class="two"><label><span data-i18n="zones.minOn">Min. ON (s)</span><input type="number" min="0" name="min_on_seconds" value="180"></label><label><span data-i18n="zones.minOff">Min. OFF (s)</span><input type="number" min="0" name="min_off_seconds" value="180"></label></div>
|
||
<label class="check"><input type="checkbox" name="enabled" checked> <span data-i18n="common.enabled">Enabled</span></label>
|
||
<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="scheduleDialog">
|
||
<form method="dialog" id="scheduleForm" class="dialog-form">
|
||
<input type="hidden" name="id">
|
||
<div class="dialog-head"><h2 data-i18n="common.schedule">Schedule</h2><button type="button" data-close>×</button></div>
|
||
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="placeholder.night" placeholder="Night"></label>
|
||
<label><span data-i18n="common.zone">Zone</span><select name="zone_id" required></select></label>
|
||
<fieldset><legend data-i18n="schedules.weekdays">Weekdays</legend><div class="days"><label><input type="checkbox" name="weekday" value="1" checked><span data-day="1">Mon</span></label><label><input type="checkbox" name="weekday" value="2" checked><span data-day="2">Tue</span></label><label><input type="checkbox" name="weekday" value="3" checked><span data-day="3">Wed</span></label><label><input type="checkbox" name="weekday" value="4" checked><span data-day="4">Thu</span></label><label><input type="checkbox" name="weekday" value="5" checked><span data-day="5">Fri</span></label><label><input type="checkbox" name="weekday" value="6"><span data-day="6">Sat</span></label><label><input type="checkbox" name="weekday" value="7"><span data-day="7">Sun</span></label></div></fieldset>
|
||
<div class="two"><label><span data-i18n="common.from">From</span><input type="time" name="start_time" value="22:00" required></label><label><span data-i18n="common.to">To</span><input type="time" name="end_time" value="06:00" required></label></div>
|
||
<label><span data-i18n="common.temperatureC">Temperature °C</span><input type="number" name="setpoint" step="0.1" min="8" max="32" value="23" required></label>
|
||
<label class="check"><input type="checkbox" name="enabled" checked> <span data-i18n="common.enabled">Enabled</span></label>
|
||
<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="automationDialog">
|
||
<form method="dialog" id="automationForm" class="dialog-form">
|
||
<input type="hidden" name="id">
|
||
<div class="dialog-head"><h2 data-i18n="common.automation">Automation</h2><button type="button" data-close>×</button></div>
|
||
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="automations.namePlaceholder" placeholder="Emergency cooling"></label>
|
||
<label><span data-i18n="automations.trigger">Trigger</span><select name="trigger_kind"><option value="temperature_above" data-i18n="automations.tempAbove">Temperature above</option><option value="temperature_below" data-i18n="automations.tempBelow">Temperature below</option><option value="time" data-i18n="automations.time">Time</option></select></label>
|
||
<label><span data-i18n="automations.measurementDevice">Measurement device</span><select name="trigger_device_id"></select></label>
|
||
<div class="two"><label><span data-i18n="automations.thresholdC">Threshold °C</span><input type="number" step="0.1" name="threshold" value="27"></label><label><span data-i18n="automations.time">Time</span><input type="time" name="at_time" value="08:00"></label></div>
|
||
<hr><h3 data-i18n="automations.action">Action</h3>
|
||
<label><span data-i18n="common.device">Device</span><select name="action_device_id" required></select></label>
|
||
<div class="two"><label><span data-i18n="common.power">Power</span><select name="action_power"><option value="" data-i18n="actions.noChange">No change</option><option value="true" data-i18n="actions.turnOn">Turn on</option><option value="false" data-i18n="actions.turnOff">Turn off</option></select></label><label><span data-i18n="common.mode">Mode</span><select name="action_mode"><option value="" data-i18n="actions.noChange">No change</option><option value="cool" data-i18n="mode.cool">Cooling</option><option value="heat" data-i18n="mode.heat">Heating</option><option value="dry" data-i18n="mode.dry">Dry</option><option value="fan" data-i18n="mode.fan">Fan</option><option value="auto" data-i18n="mode.auto">Auto</option></select></label></div>
|
||
<div class="two"><label><span data-i18n="common.targetC">Target °C</span><input type="number" step="0.1" min="8" max="32" name="action_target_temperature"></label><label><span data-i18n="common.cooldownSeconds">Cooldown (s)</span><input type="number" min="30" name="cooldown_seconds" value="300"></label></div>
|
||
<label class="check"><input type="checkbox" name="enabled" checked> <span data-i18n="common.enabled">Enabled</span></label>
|
||
<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="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>×</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" readonly></label>
|
||
<div class="form-actions"><button type="button" class="secondary" id="copyAccessToken" data-i18n="actions.copy">Copy</button><button type="button" class="primary" data-close data-i18n="actions.done">Done</button></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="tokenDialog" class="auth-dialog">
|
||
<form method="dialog" id="tokenForm" class="dialog-form">
|
||
<div class="brand large"><div><strong>GREE Controller</strong><small data-i18n="auth.required">Authentication required</small></div></div>
|
||
<label><span data-i18n="auth.token">Access token</span><input type="password" name="token" required autocomplete="current-password"></label>
|
||
<button class="primary" type="submit" data-i18n="auth.connect">Connect</button>
|
||
</form>
|
||
</dialog>
|
||
|
||
<div id="toast" role="status" aria-live="polite"></div>
|
||
<script src="/app.js" defer></script>
|
||
</body>
|
||
</html>
|