v0.8.13
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
# GREE Controller v0.8.10 - dashboard and notification-filter report
|
||||
|
||||
Source baseline: GREE Controller v0.8.9.
|
||||
|
||||
## Changes
|
||||
|
||||
- added fine-grained notification categories with a dedicated stale Home Assistant sensor switch;
|
||||
- split stale HA reads into the `ha.sensor_stale` event kind while retaining other HA failures as `ha.sensor_error`;
|
||||
- rebuilt the Dashboard as horizontal **Main / Thermostats / Manual control** tabs with one vertically scrolling panel at a time;
|
||||
- moved LAN discovery from the global toolbar to Devices;
|
||||
- added centered `active/total running` unit status to the top toolbar;
|
||||
- changed control-owner metadata to show date and time instead of time only;
|
||||
- removed visible "Quick" wording from the Dashboard thermostat/manual-control section titles;
|
||||
- rotated the PWA cache key for the changed frontend.
|
||||
|
||||
## Validation
|
||||
|
||||
The Rust toolchain is not installed in this build environment, so `cargo test`/`cargo check` cannot be run here. JavaScript syntax checks, JSON parsing, HTML ID checks, shell syntax checks, manifest regeneration and ZIP integrity verification are performed before packaging. The target-host updater should run the full Rust test suite before replacing the service.
|
||||
Generated
+1
-1
@@ -633,7 +633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.8.10"
|
||||
version = "0.8.13"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.8.10"
|
||||
version = "0.8.13"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+13
-24
@@ -1,20 +1,11 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
f38d9bfbe2b378e02ab825b1f0bf6e7c6b8ac9a834c63bac58d2383524fbe8e1 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
875d85d3388362890ef0fa28b7192634856b8c095f5fbdacc75d60e014f692cc ./BUILD_REPORT.md
|
||||
c91ff48b9278d8394e79e734b11cc49d0c092881500edc11121b6baca23d2e67 ./Cargo.lock
|
||||
43bf5b7dc87db920fdd5a7157c50c92aef7053a293c51c86c5b5fa76c761f9d7 ./Cargo.toml
|
||||
7fdaf0c83e203b81eb3d6ae6461f61481f08998bba5f5a442722e943925f5c12 ./Cargo.lock
|
||||
ba5be0946d041a905c54c102e00d933204f6eec153b6a56585d4ff7b3f10b876 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
21dd3dc409d51f29890ee303f97eaa4e68362c100e499c48bfc1ef8e57f525fd ./README.md
|
||||
73ff8cb22b2cfb35456e04225b59cefee3c71bd6ba0348d5ecb74e9d9f804c92 ./README.md
|
||||
91c826e2f8c974bdb7b15e01e9b20487f06ff887247ecd9600f1263c11a61b96 ./build.rs
|
||||
6e2953807ea0d1f346f99adb527391806cb088afdbc62e721193be7253a3bc9c ./docs/API.md
|
||||
234dd200e380a13ecd3e61b4ea455f6f08d64ce89382077dee80684acadb9703 ./docs/HOME_ASSISTANT_MIGRATION.md
|
||||
7a88d6e76fda21e5d34ab351e26bc10dc1f8f7b3055505aefad1df7c56d65ae4 ./docs/LOCALIZATION.md
|
||||
10a0722e1100fb4a05e3067daeb67dc47b0c0a096b43b1cbf2bf002967ce7d98 ./docs/LXC.md
|
||||
2ef5cb04cc74be3aab77a9d117b9040f186c73a7dcf24375f1cca2d73e428449 ./docs/PROJECT_SPEC.md
|
||||
7748b1230029439a405139750b2e0607b8e463bfd56ad86982394105069697c5 ./docs/REVERSE_PROXY.md
|
||||
28f1403d84b01b37bebb72548aa39a75bb14bdbb611a43bfa761319957cd6f39 ./docs/SECURITY.md
|
||||
c3f3f12206c552d3fb919022d64b3fa60342fc1031081e893b7efd743961c402 ./future.md
|
||||
95e3c8c99ba439a86a2fb05cfd8ef00c75cb959a024a349a082da935b726f500 ./home-assistant/README.md
|
||||
751f5083ced1ac84f1841492c389fc5afaa6bef242dc7379538896428df7bbed ./docs/API.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
71c8058f85fd9ce9f063b7a84dcf2956f23cdbbd02bd4bd99d2fbb9d177c72ad ./home-assistant/custom_components/gree_controller/climate.py
|
||||
@@ -24,7 +15,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista
|
||||
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png
|
||||
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png
|
||||
98f494432dbc861b5b4b8928715845590189008482733006af0b0d9277d193a8 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
ada3b5ff14063c4a88638dd6165405998fc248948a37d90a7fd34193ba10e2f0 ./home-assistant/custom_components/gree_controller/manifest.json
|
||||
38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py
|
||||
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py
|
||||
1cae12876eb81085910907cf1fcf340a18dcba4f8c967ef222c9496325057849 ./home-assistant/custom_components/gree_controller/sensor.py
|
||||
@@ -32,11 +23,9 @@ c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assista
|
||||
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./home-assistant/custom_components/gree_controller/translations/en.json
|
||||
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json
|
||||
4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
29cc08123a581474a4445d8bf98dcf22f17cdd2e0e7f1358158e588e1252eec5 ./lang/en.json
|
||||
166d7daf829d8b3f9127a90f2da8c3db0ee2d8b490a083d1c278e7ccbde48a27 ./lang/pl.json
|
||||
0e8218afe7cddfa4519405213e67e5b84bd108c44e0b8bf2155bf94b81949399 ./lang/en.json
|
||||
7ee310fa91a7be09fe4a8d4ab8ab94d651dbe0c72e512ec07517461b9381731f ./lang/pl.json
|
||||
028e1f16e9fbaed57cadb88eff04e65b4bd67722c50b4d6b1fb525f5a2f39abf ./make_zip.py
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
dbb92ddc27b8724faf709983e4feecd3f188052b2cba39daba96d4bc16914df5 ./scripts/dev.sh
|
||||
@@ -47,7 +36,7 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
b48fc84d79aab381226363ac8473f981bcba5e4911c4cc0011261182debf4250 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
8295771c6907bcbc4e57d3c4572c120b72501167d02706f1fade7c1c09d6a6bf ./src/api.rs
|
||||
b8f000150bac599d6a8c57b64ab3d145f2e6154ecf5e49bcd3d6cfa5241c11bc ./src/api.rs
|
||||
9040e8cb6647c76a875148b7591abcccfe4e2d4708ef462a1e5d25dd4ccac911 ./src/config.rs
|
||||
5dfda2f4dc540c502885b0cd7017dc77768684588acf528f01d1fd88f1af4aec ./src/db.rs
|
||||
15d07a2c0ad8ec6530897980787e6e9a94103bda849f07c233f6a37796d1f0b1 ./src/engine.rs
|
||||
@@ -63,10 +52,10 @@ a910bd9432a393740c0f6fab52bfcb551f0ea756718d66d290fd2610767cf07c ./src/protocol
|
||||
6a1c0cab3eab80ecd254c5e486b1fba068523edb8d699c64054c17f67f4a31d0 ./src/queries.rs
|
||||
2d69811db832c90ce06035ee29665205a04e4c514a21ecae9623991a9b14d825 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
22c7882d8830101f21435069489dc976acb31fe027be8182c5bc444d3367c451 ./web/app.js
|
||||
2e4e214a39f2ec4fab32d6cdcb480186c93a30ca7b8cac17fd11b196102324ae ./web/app.js
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
5900f4f892c1515178c26a8a243476010513970185009a0b3d44889e0c4c9681 ./web/index.html
|
||||
fe994148bb0e28027c9ddf5962a4878e2bde6d8158cbac1fb76c8363bea6405e ./web/index.html
|
||||
fd26156e9f1d6713d3def564ad000553d9a16a24376059701db2ee762c99ee6c ./web/manifest.webmanifest
|
||||
db9319c0a0c6927a4f3e2b0a2a83927dd61ad6a456e9ca8fad9f3b48d1987778 ./web/styles.css
|
||||
2da305f344ba9cf2d56975fdc4c446ef47b65a117daf13e0689091bd31303f32 ./web/sw.js
|
||||
cc5849d3f60fb5c72cde5995dad32a9675e826dbf6b2be5e08382e706cb66fd2 ./web/styles.css
|
||||
47d57bebd83e4738f5b6c8dbbcb33d125a657b2b0c3ebcfcc2c4f35ab4484326 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -1,609 +1,401 @@
|
||||
# GREE Controller
|
||||
|
||||
> Identifier convention: project-owned namespace identifiers use `gree_controller`. Home Assistant uses `gree_controller`, UI storage uses `gree_controller_*`, and environment variables use `GREE_CONTROLLER_*`.
|
||||
Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API.
|
||||
|
||||
Standalone local GREE air-conditioner controller written in Rust. It runs on a regular Linux host or an LXC container and provides a mobile-first web interface without depending on the vendor cloud.
|
||||
**Current release: 0.8.13**
|
||||
|
||||
Current version: **0.8.10**.
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
## Highlights
|
||||
## What it does
|
||||
|
||||
- local GREE discovery over UDP/7000,
|
||||
- V1 AES-128-ECB and V2 AES-128-GCM discovery/bind/status/command transport,
|
||||
- automatic ECB/GCM detection from discovery responses and bind fallback,
|
||||
- power, HVAC mode, target temperature, fan, vertical/horizontal swing, quiet, turbo and display light,
|
||||
- capability discovery and control for optional GREE X-FAN, Air, Health and native Sleep features,
|
||||
- SQLite state/history/event storage,
|
||||
- automatic SQLite history compaction plus optional InfluxDB 1.x/2.x long-term archive,
|
||||
- smart thermostat zones with global house Heat/Cool/Off mode and per-zone overrides,
|
||||
- setpoint modulation that keeps indoor units powered during normal operation instead of repeatedly cycling power,
|
||||
- Comfort/Sleep/Away profiles, temporary overrides and one-tap **Sleep**,
|
||||
- named climate groups (for example Upstairs/Downstairs) with independent power, Heat/Cool/house-follow mode and profile control, including automation targets,
|
||||
- editable weekly schedules plus Family/Child room/Bedroom/Workday/Always-comfort templates,
|
||||
- temperature/time automations for advanced exceptions,
|
||||
- per-zone optional Home Assistant room-temperature sensors with GREE fallback,
|
||||
- optional Home Assistant outdoor-temperature assist for setpoint/fan decisions,
|
||||
- opt-in support for self-signed/invalid HTTPS certificates on the outbound HA sensor connection,
|
||||
- combined zone temperature using configurable GREE/external sensor weighting and discrepancy protection,
|
||||
- REST API and WebSocket updates,
|
||||
- responsive PWA optimized for phones,
|
||||
- dashboard automation-plan blocks showing current zone decisions and upcoming schedule changes,
|
||||
- Node-RED-style live automation flow board with sensor → thermostat → decision → unit command nodes, group/unit filtering, standalone-tab view and browser full-screen mode,
|
||||
- configurable night mode with quiet hours, maximum fan speed and optional GREE Quiet/native Sleep requests,
|
||||
- configurable event-log retention and presentation-only Home Assistant sensor aliases used by history charts/selectors,
|
||||
- settings import/export and an optional live debug overlay with API logs and decrypted GREE frame payloads,
|
||||
- JSON-based UI localization loaded from embedded `lang/*.json` language packs,
|
||||
- light, dark and system appearance modes stored in a browser cookie,
|
||||
- optional Bearer-token authentication,
|
||||
- simulator mode for development without physical hardware,
|
||||
- Debian/Ubuntu LXC systemd installer,
|
||||
- Home Assistant custom integration that proxies device `climate` commands, exposes full zone thermostat entities and publishes every climate group as a separate HA device with power, mode, profile and control-plan entities,
|
||||
- migration mapping generator for retaining existing HA entity IDs such as `climate.klima_salon`.
|
||||
- Discovers and binds GREE Wi-Fi units on the LAN, including mixed protocol generations.
|
||||
- Provides direct technical device control and a separate thermostat layer built around zones.
|
||||
- Supports cooling/heating profiles: Comfort, Sleep, Away and custom quick targets.
|
||||
- Coordinates house, group and per-zone control without silently fighting physical/manual overrides.
|
||||
- Supports schedules, automations, night mode and temporary Quick Thermostat sessions.
|
||||
- Can use GREE sensors, Home Assistant room sensors or a weighted combination.
|
||||
- Stores recent history in SQLite and can archive/query long-term history in InfluxDB 1.x or 2.x.
|
||||
- Exposes native Home Assistant entities through the bundled custom integration.
|
||||
- Includes PWA-capable responsive UI, Polish/English localization and live diagnostics.
|
||||
- Can run in Simulation mode without physical air conditioners. A large warning is shown whenever simulation is enabled.
|
||||
|
||||
See [`BUILD_REPORT.md`](BUILD_REPORT.md) for package validation details and [`docs/LXC.md`](docs/LXC.md) for the LXC deployment/update workflow.
|
||||
## UI overview
|
||||
|
||||
## Quick start
|
||||
The interface is intentionally split by responsibility:
|
||||
|
||||
On Debian, Ubuntu or an LXC container:
|
||||
| Area | Purpose |
|
||||
| --- | --- |
|
||||
| Dashboard | Whole-house summary and Quick Thermostat controls. |
|
||||
| Devices | Discovery, binding and direct/technical GREE device control. |
|
||||
| Zones | Thermostat configuration, profiles, sensors and control ownership. |
|
||||
| Groups | Climate group membership and group power/mode/preset control. |
|
||||
| Schedules | Weekly thermostat schedules and ready-made templates. |
|
||||
| Automations | Time/temperature triggered device or group actions. |
|
||||
| History | Overview, zones, devices and HA sensors with precise chart tooltips and zoom. |
|
||||
| Simulation | Simulated units for safe testing. |
|
||||
| Night mode | Quiet/Sleep/fan limits during an overnight window. |
|
||||
| Home Assistant | Sensor connection, aliases and restricted integration tokens. |
|
||||
| Settings → Application | Simulation, notifications, metrics/logs, InfluxDB, debug and backup. |
|
||||
| Settings → GREE | Controller identity, polling/discovery, GREE commands and live GREE traffic. |
|
||||
| Events | Operational event log and diagnostics. |
|
||||
|
||||
The top bar uses the same compact language and theme controls on desktop and mobile. The live debug overlay can be filtered to **All**, **Requests** or **GREE** frames.
|
||||
|
||||
## Quick start for development
|
||||
|
||||
Requirements are installed automatically on supported Debian/Ubuntu environments unless `--no-install` is used.
|
||||
|
||||
```bash
|
||||
unzip gree-controller-v0.8.10.zip
|
||||
cd gree-controller
|
||||
chmod +x scripts/*.sh
|
||||
cp .env.example .env
|
||||
./scripts/dev.sh
|
||||
```
|
||||
|
||||
`scripts/dev.sh` installs missing build tools when possible, installs stable Rust with `rustup` when required, creates `.env`, builds the application and starts the web panel.
|
||||
|
||||
Default address:
|
||||
|
||||
```text
|
||||
http://HOST_ADDRESS:8787
|
||||
```
|
||||
|
||||
The first empty database can be seeded with **Living Room (simulator)** so the UI, history, zones and automations can be tested without an AC.
|
||||
|
||||
### Development commands
|
||||
Useful development commands:
|
||||
|
||||
```bash
|
||||
./scripts/dev.sh --check
|
||||
./scripts/dev.sh --release
|
||||
./scripts/dev.sh --reset
|
||||
./scripts/dev.sh --host 0.0.0.0 --port 8787
|
||||
./scripts/dev.sh --no-install
|
||||
./scripts/dev.sh --check # format, tests, build and HTTP smoke test
|
||||
./scripts/dev.sh --release # optimized local build
|
||||
./scripts/dev.sh --reset # recreate the local SQLite database
|
||||
./scripts/smoke.sh # API smoke test against an isolated simulated instance
|
||||
```
|
||||
|
||||
## Web interface
|
||||
The default panel is available at `http://127.0.0.1:8787` when the process is bound locally.
|
||||
|
||||
The UI is mobile-first and uses no external CDN.
|
||||
## LXC / systemd installation
|
||||
|
||||
Language selector:
|
||||
The production scripts target Debian/Ubuntu systemd hosts and LXC containers.
|
||||
|
||||
- English is the required default/fallback language,
|
||||
- Polish is included,
|
||||
- additional languages are discovered automatically from `lang/*.json` at build time.
|
||||
|
||||
The selected language is stored in the `gree_controller_language` cookie. The UI contains no hard-coded list of supported languages. To add a language, copy `lang/en.json`, translate the `translations` values, set the `meta` fields, save it as `<code>.json`, and rebuild. For example, `lang/de.json` becomes an additional language after `cargo build` / `./scripts/dev.sh`. Missing translation keys fall back to English.
|
||||
|
||||
See [`docs/LOCALIZATION.md`](docs/LOCALIZATION.md) for the language-pack format and validation rules.
|
||||
|
||||
Appearance selector:
|
||||
|
||||
- System,
|
||||
- Light,
|
||||
- Dark.
|
||||
|
||||
The selected appearance is stored in the `gree_controller_theme` cookie. The current UI keeps the classic GREE Controller interface: large rounded cards, circular thermostat controls, compact desktop navigation and mobile bottom navigation. It keeps the newer neutral dark/light palette and green accent (`#3ecf8e` in dark mode and `#24b47e` in light mode).
|
||||
|
||||
### Static offline CSS
|
||||
|
||||
The interface is plain static CSS. `web/styles.css` is committed with the project and embedded directly into the Rust binary. There is no frontend package manager, CDN, or CSS build step. Edit `web/styles.css` and `web/index.html` directly, then rebuild the Rust application.
|
||||
|
||||
|
||||
## Connecting a physical GREE device
|
||||
|
||||
1. Put the controller and AC in a network where UDP/7000 traffic is allowed.
|
||||
2. Open the web interface and select **Discover**.
|
||||
3. Start with **Auto (V1 + V2)** and 3 scan passes. The controller recognizes ECB responses and GCM responses carrying a `tag` and automatically binds discovered units.
|
||||
4. If required, repeat discovery using **V1 AES-ECB** or **V2 AES-GCM** to isolate a model family.
|
||||
5. New devices immediately open a naming step so you can enter room-friendly names such as **Salon** or **Sypialnia**. They can also be renamed later from **Devices -> Rename**. Re-discovery preserves your custom name.
|
||||
6. If LXC/VLAN broadcast does not pass, add the unit manually with IP and MAC/CID.
|
||||
|
||||
Version 0.3.7 additionally uses forgiving GREE Base64 decoding for non-canonical GCM tags seen on real Wi-Fi modules, automatically selects the directly connected local IPv4 interface for unicast GREE traffic, and refreshes the short bind window with a subnet broadcast immediately before bind. A single lost UDP response no longer marks a device offline; three consecutive communication failures are required.
|
||||
|
||||
## LXC/systemd installation and updates
|
||||
|
||||
All operator scripts are under `scripts/`. On a clean Debian/Ubuntu LXC container:
|
||||
### First installation
|
||||
|
||||
```bash
|
||||
chmod +x scripts/*.sh
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
The installer installs build dependencies/Rust when required, runs the Rust tests, builds a release binary, creates the `gree-controller` service account, stores runtime data in `/var/lib/gree-controller`, installs the binary under `/opt/gree-controller`, creates `/etc/gree-controller.env`, generates an administrator token and enables the systemd service. Existing `/etc/gree-controller.env` is preserved.
|
||||
Optional flags:
|
||||
|
||||
For a later release, unpack the new source archive and run:
|
||||
```bash
|
||||
sudo ./scripts/install.sh --skip-tests
|
||||
sudo ./scripts/install.sh --no-start
|
||||
```
|
||||
|
||||
Installed layout:
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `/opt/gree-controller/gree-controller` | Release binary. |
|
||||
| `/etc/gree-controller.env` | Environment configuration; mode `0600`. |
|
||||
| `/var/lib/gree-controller/gree-controller.db` | SQLite database. |
|
||||
| `/etc/systemd/system/gree-controller.service` | systemd unit. |
|
||||
| `/var/backups/gree-controller/` | Update backups used for rollback. |
|
||||
|
||||
### Update
|
||||
|
||||
Run from the extracted source tree of the new release:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh
|
||||
```
|
||||
|
||||
The update is designed for LXC testing and production-style upgrades: compilation/tests happen before the running service is stopped; then the script backs up the installed binary, service unit, environment file and stopped SQLite database under `/var/backups/gree-controller/<timestamp>/`. After replacement it checks `/api/health`. A failed startup triggers automatic rollback to the previous binary, unit and database backup.
|
||||
The updater builds/tests first, stops the old service, backs up the binary/unit/environment/database, installs the release, checks `/api/health` and rolls back automatically if startup fails.
|
||||
|
||||
Service helpers:
|
||||
### Service helper
|
||||
|
||||
```bash
|
||||
./scripts/service.sh status
|
||||
sudo ./scripts/service.sh restart
|
||||
./scripts/service.sh logs
|
||||
./scripts/service.sh health
|
||||
./scripts/service.sh logs
|
||||
sudo ./scripts/service.sh restart
|
||||
```
|
||||
|
||||
Use `--skip-tests` with `install.sh` or `update.sh` only when you explicitly want to skip `cargo test --all-targets`. `scripts/install-lxc.sh` remains as a compatibility alias to `scripts/install.sh`.
|
||||
`./scripts/install-lxc.sh` remains as a compatibility alias to `install.sh`.
|
||||
|
||||
## Environment configuration
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---:|---|
|
||||
| `GREE_CONTROLLER_BIND` | `0.0.0.0:8787` | HTTP/WebSocket bind address |
|
||||
| `GREE_CONTROLLER_DATABASE` | `./data/gree-controller.db` | SQLite file |
|
||||
| `GREE_CONTROLLER_APP_TOKEN` | empty | Bearer token; empty disables API authentication |
|
||||
| `GREE_CONTROLLER_SIMULATE` | `false` | Enables simulator seeding for development/testing |
|
||||
| `GREE_CONTROLLER_AUTO_SEED` | `false` | Seeds a simulator into an empty database when simulation is enabled |
|
||||
| `GREE_CONTROLLER_POLL_INTERVAL_SECONDS` | `15` | Device polling interval |
|
||||
| `GREE_CONTROLLER_ZONE_INTERVAL_SECONDS` | `5` | Zone-control interval |
|
||||
| `GREE_CONTROLLER_DISCOVERY_TIMEOUT_MS` | `3000` | UDP discovery timeout |
|
||||
| `GREE_CONTROLLER_DISCOVERY_BROADCAST` | `255.255.255.255:7000` | Discovery broadcast target |
|
||||
| `GREE_CONTROLLER_GREE_INTERFACE` | empty | Optional Linux interface used for all GREE UDP traffic, e.g. `eth1` |
|
||||
| `GREE_CONTROLLER_ID` | `gree-controller` | Controller instance identifier used for logs/metadata; the GREE wire protocol uses the standard `cid=app` |
|
||||
| `GREE_CONTROLLER_HOUSE_MODE` | `cool` | Initial seasonal house mode: `cool`, `heat` or `off` |
|
||||
| `GREE_CONTROLLER_OUTDOOR_ASSIST_ENABLED` | `true` | Initial outdoor-temperature assist state |
|
||||
| `GREE_CONTROLLER_HISTORY_RETENTION_DAYS` | `30` | Local SQLite retention before pruning |
|
||||
| `GREE_CONTROLLER_HISTORY_COMPACTION_ENABLED` | `true` | Compact old local samples to chart-oriented resolution |
|
||||
| `GREE_CONTROLLER_SUPPRESS_DEVICE_BEEP` | `false` | Send only changed fields and request GREE buzzer suppression where supported |
|
||||
| `GREE_CONTROLLER_INFLUX_ENABLED` | inferred from URL | Enable optional long-term InfluxDB archive |
|
||||
| `GREE_CONTROLLER_INFLUX_VERSION` | `2` | `1` for InfluxDB 1.x or `2` for InfluxDB 2.x |
|
||||
| `GREE_CONTROLLER_INFLUX_URL` / `INFLUXDB_URL` | empty | InfluxDB base URL |
|
||||
| `GREE_CONTROLLER_INFLUX_THRESHOLD_DAYS` | `30` | Read history older than this age from InfluxDB |
|
||||
| `GREE_CONTROLLER_INFLUX_DATABASE` | `gree_controller` | InfluxDB 1.x database |
|
||||
| `GREE_CONTROLLER_INFLUX_USERNAME` / `GREE_CONTROLLER_INFLUX_PASSWORD` | empty | Optional InfluxDB 1.x credentials |
|
||||
| `GREE_CONTROLLER_INFLUX_ORG` / `GREE_CONTROLLER_INFLUX_BUCKET` / `GREE_CONTROLLER_INFLUX_TOKEN` | empty | InfluxDB 2.x organization, bucket and token |
|
||||
| `GREE_CONTROLLER_DEBUG_OVERLAY` | `false` | Show the live debug window on every web view |
|
||||
| `GREE_CONTROLLER_DEBUG_GREE_FRAMES` | `false` | Stream decrypted GREE request/response payloads into debug |
|
||||
| `HA_URL` | empty | Optional Home Assistant URL |
|
||||
| `HA_TOKEN` | empty | Optional Home Assistant Long-Lived Access Token |
|
||||
| `HA_ENTITY_ID` | empty | Optional default HA room-temperature sensor |
|
||||
| `HA_OUTDOOR_ENTITY_ID` | empty | Optional HA outdoor-temperature sensor |
|
||||
| `HA_ALLOW_INVALID_TLS` | `false` | Opt in to invalid/self-signed HA HTTPS certificates |
|
||||
Copy `.env.example` for the full supported example. Command-line arguments exist for the core `Config` options, while most day-to-day runtime settings are also editable in the Web UI and persisted in SQLite.
|
||||
|
||||
Settings changed from the web panel are stored in SQLite. Explicit history/debug/Influx environment variables override their persisted values at process startup. Supplying an Influx URL enables the archive unless `GREE_CONTROLLER_INFLUX_ENABLED=false` is explicitly set. `GREE_CONTROLLER_APP_TOKEN` is loaded at process startup.
|
||||
Environment values explicitly supplied for supported runtime overrides win over the persisted value after restart.
|
||||
|
||||
Web UI organization: **Settings** contains controller/application and technical storage/debug options, while **Night mode** and **Home Assistant / Sensors** are separate pages under More. Disabled zones continue reporting room temperature even though thermostat control is stopped.
|
||||
### Core variables
|
||||
|
||||
## Climate history
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `GREE_CONTROLLER_BIND` | `0.0.0.0:8787` | HTTP/WebSocket listen address. |
|
||||
| `GREE_CONTROLLER_DATABASE` | `./data/gree-controller.db` | SQLite database path. |
|
||||
| `GREE_CONTROLLER_APP_TOKEN` | empty | Optional administrator API/Web UI token. Empty means trusted-LAN mode. |
|
||||
| `GREE_CONTROLLER_BASE_PATH` | empty | Optional reverse-proxy prefix such as `/gree`. |
|
||||
| `GREE_CONTROLLER_ID` | `gree-controller` | GREE client/controller identifier. |
|
||||
| `GREE_CONTROLLER_SIMULATE` | `false` | Initial Simulation mode. |
|
||||
| `GREE_CONTROLLER_AUTO_SEED` | `false` | Seed simulated sample data when applicable. |
|
||||
| `GREE_CONTROLLER_POLL_INTERVAL_SECONDS` | `15` | Device polling interval. |
|
||||
| `GREE_CONTROLLER_ZONE_INTERVAL_SECONDS` | `5` | Thermostat control interval. |
|
||||
| `GREE_CONTROLLER_DISCOVERY_TIMEOUT_MS` | `3000` | Discovery timeout. |
|
||||
| `GREE_CONTROLLER_DISCOVERY_BROADCAST` | `255.255.255.255:7000` | GREE discovery target. |
|
||||
| `GREE_CONTROLLER_GREE_INTERFACE` | auto | Optional interface name or local IP used for GREE UDP traffic. |
|
||||
|
||||
History is collected independently for physical GREE devices, control zones, and configured Home Assistant temperature sensors. Device samples continue to use the long-standing `readings` table, so upgrading does not require waiting for a new zone-history table to fill. When rich `zone_readings` are not available yet, the API automatically reconstructs a compatible zone timeline from existing device readings.
|
||||
Additional environment variables cover history retention, debug, night mode, Home Assistant and InfluxDB. See [`.env.example`](.env.example).
|
||||
|
||||
The History area has linkable sub-pages:
|
||||
## Connecting physical GREE units
|
||||
|
||||
- `/history/overview` — all GREE indoor temperatures, available GREE/HA outdoor temperatures, and all zone control temperatures,
|
||||
- `/history/zones` — all zones or one detailed zone with GREE, HA room, control, target and device-setpoint series,
|
||||
- `/history/devices` — indoor temperature, available `OutEnvTem`, and target for every GREE unit,
|
||||
- `/history/sensors` — configured Home Assistant room/outdoor temperature sensors,
|
||||
- `/history/custom` — compose arbitrary series, save chart definitions in the browser and copy a URL that recreates the chart.
|
||||
1. Place the controller host on a network that can reach the air-conditioner Wi-Fi modules by UDP.
|
||||
2. Open **Devices** and start discovery.
|
||||
3. Auto protocol mode is recommended; the controller accepts both supported GREE encryption generations.
|
||||
4. Newly discovered devices are bound automatically when possible. Manual bind is also available.
|
||||
5. Create a thermostat zone for each unit you want the thermostat engine to own.
|
||||
|
||||
A zone timeline includes GREE indoor temperature, optional HA room temperature, calculated control temperature, active profile target, actual setpoint sent to the AC, available outdoor temperature, demand, power, mode and fan speed. HA sensor samples and zone samples are throttled to at least 15 seconds (or the configured device poll interval, whichever is longer). Local samples older than 24 hours are compacted to 10-minute resolution and samples older than 7 days to 30-minute resolution. When InfluxDB is configured, older queries are merged from InfluxDB with recent SQLite history. The maintenance task backfills compacted legacy samples into InfluxDB and deletes an old SQLite sample only after the archive write succeeds, so an unavailable archive does not cause local data loss.
|
||||
Discovery uses UDP broadcast, so routed/VLAN networks must explicitly permit or relay the required traffic.
|
||||
|
||||
All main UI views also use browser URLs (`/dashboard`, `/devices`, `/zones`, `/groups`, `/schedules`, `/automations`, `/simulation`, `/settings`, `/events`) so refresh, browser Back/Forward, bookmarks and direct links work normally.
|
||||
### Multi-NIC / dedicated GREE interface
|
||||
|
||||
## Per-zone room temperature sensors
|
||||
For a host with separate management and GREE networks, set for example:
|
||||
|
||||
Each control zone can pair one GREE indoor unit with its own optional room sensor from Home Assistant. This is intentionally configured per zone, so rooms do not share a global temperature source.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Living room -> GREE Living Room + sensor.living_room_temperature
|
||||
Bedroom -> GREE Bedroom + sensor.bedroom_temperature
|
||||
Office -> GREE Office + sensor.office_temperature
|
||||
```bash
|
||||
GREE_CONTROLLER_GREE_INTERFACE=eth1
|
||||
```
|
||||
|
||||
Zone temperature strategies:
|
||||
or a local address:
|
||||
|
||||
- **GREE only** — use the AC internal sensor.
|
||||
- **GREE + room sensor** — recommended; calculate a weighted control temperature from both sensors. The default room-sensor weight is 40%.
|
||||
- **Room sensor only** — use the assigned HA room sensor, with automatic fallback to GREE if HA or the entity becomes unavailable.
|
||||
```bash
|
||||
GREE_CONTROLLER_GREE_INTERFACE=192.168.50.2
|
||||
```
|
||||
|
||||
For combined control, `max_sensor_difference` protects against an obviously incorrect external measurement. If the two sensors differ by more than the configured threshold (default `3.0°C`), the zone uses the GREE temperature and logs a sensor-discrepancy event.
|
||||
The controller will use that interface for GREE UDP traffic while keeping the HTTP UI on `GREE_CONTROLLER_BIND`.
|
||||
|
||||
The zone API exposes `device_temperature`, `external_temperature`, `current_temperature` (the actual control temperature) and `control_temperature_source` for diagnostics. Existing SQLite zone records remain compatible because the new fields have defaults and are stored in the existing JSON payload.
|
||||
Diagnostics:
|
||||
|
||||
Zone cards are fast thermostats: `- / +`, **Auto schedule / Comfort / Sleep / Away**, **Global / Heat / Cool**, and local thermostat **ON/OFF** are available without opening the edit dialog. Local ON/OFF is thermostat ownership, not direct/pilot control: a locally started zone can bypass a disabled climate-group power gate while still using its room sensor, hysteresis, selected target/profile, Smart Fan and GREE setpoint modulation. **Resume automation** removes that local power override and returns the zone to group/schedule ownership; if its group is off, the unit then stays off. Only a real physical/remote change or the technical Devices command path creates `device_manual_override`. Controller-originated GREE transitions are tracked through the short device settling window so group/thermostat commands are not misclassified as remote takeover. Temporary thermostat overrides otherwise end at the next schedule boundary; when a local thermostat is explicitly active, its selected manual target/profile is retained until Auto/Resume is chosen.
|
||||
```bash
|
||||
sudo ./scripts/configure-gree-network.sh
|
||||
./scripts/network-debug.sh
|
||||
```
|
||||
|
||||
## Smart thermostat and schedules
|
||||
The hardened systemd unit allows `AF_NETLINK`, which is required for interface discovery on multi-NIC Linux/LXC systems.
|
||||
|
||||
Version 0.4.0 is designed around a simple rule: during normal heating/cooling, keep the indoor unit powered and modulate its target instead of repeatedly switching the unit OFF and ON. When a room is satisfied, cooling moves the device target above the comfort target and heating moves it below the comfort target. When hysteresis requests conditioning again, the working target is restored. Smart Quiet and standby Low fan are transition hints rather than commands that are blindly reasserted every adjustment interval, avoiding unnecessary beeps when the room and device setpoint are already stable. **House Off** is now a no-control state for inherited zones, so manual device operation is not overwritten; whole-house master power on/off is available separately and can authoritatively keep every unit off without changing the selected thermostat mode. Disabled zones are excluded from normal house/group/automation control; their physical unit remains available for explicit technical control from the Devices view.
|
||||
## Thermostat model
|
||||
|
||||
Choose one seasonal **global mode** — Cooling in summer, Heating in winter, or Off. Zones inherit this global mode by default, while a zone may explicitly override Heat/Cool when required. In **Mode policy**, leave **Follow global mode** unchanged when the zone should inherit the mode selected globally in GREE Controller; **Cooling only** / **Heating only** are deliberate per-zone overrides. Each zone has separate Comfort/Sleep/Away temperatures for cooling and heating.
|
||||
### Devices vs zones
|
||||
|
||||
Schedules store profiles rather than duplicating temperatures. Ready-made **Family**, **Child room**, **Bedroom**, **Workday** and **Always comfort** templates generate ordinary editable schedule entries. For example, a child room template can enter Sleep earlier; if the child goes to bed even earlier, press **Sleep** and that zone remains in Sleep until its next schedule boundary (or until manually cleared when no future boundary exists). Whole-house **Comfort all / Sleep all / Away all / Auto schedule** actions are also available.
|
||||
A **Device** is the physical GREE unit. Device commands are direct/technical commands.
|
||||
|
||||
The optional HA outdoor sensor is an assist signal only. It never replaces the room-control temperature. In extreme weather it can make the active device setpoint slightly more assertive and let Smart Fan increase airflow. When cooling is actually controlled from a valid external/combined room sensor, demand also forces the next lower whole-degree unit target (typically 0.5–1.0°C below the room target); the correction is disabled on GREE fallback and is not stacked above the outdoor-assist maximum.
|
||||
A **Zone** is the thermostat owner for a device. It decides demand from room temperature, setpoint, hysteresis, schedules, group/house gates, manual ownership and safety lockouts.
|
||||
|
||||
At startup, thermostat, schedule and automation commands are held until one complete live device poll has finished. This prevents a restart from issuing commands based only on stale persisted device state while still preserving all saved zone, group, schedule, override and device configuration.
|
||||
This distinction is important: direct device control intentionally behaves differently from thermostat control.
|
||||
|
||||
### Control ownership
|
||||
|
||||
The zone state exposes who currently owns control (`control_owner`, `control_source`, timestamps and reason). Physical remote/direct device actions can create a manual-device takeover so normal schedules do not immediately overwrite the user. Explicit zone thermostat control hands ownership back to the thermostat engine when appropriate.
|
||||
|
||||
Whole-house master OFF remains authoritative and powers managed devices down.
|
||||
|
||||
### Profiles and schedules
|
||||
|
||||
Each zone stores separate cooling and heating temperatures for:
|
||||
|
||||
- Comfort
|
||||
- Sleep
|
||||
- Away
|
||||
- Custom/manual target
|
||||
|
||||
Schedules select profiles or a custom target for selected ISO weekdays (`1=Monday`, `7=Sunday`). Overnight windows are supported. Overlapping enabled schedules for the same zone are rejected.
|
||||
|
||||
Quick preset/setpoint overrides normally hand control back at the next schedule boundary. Temporary Quick Thermostat adds explicit start/finish rules such as duration, exact time, temperature reached/stable or next schedule boundary.
|
||||
|
||||
## Home Assistant
|
||||
|
||||
There are two independent HA directions:
|
||||
There are two independent Home Assistant directions.
|
||||
|
||||
1. **HA as an optional sensor source**: the Rust controller can read a selected HA sensor and use it for a zone.
|
||||
2. **HA as a client of GREE Controller**: the included custom integration creates `climate` entities whose commands are sent to this Rust application.
|
||||
### 1. Home Assistant as a temperature source
|
||||
|
||||
For the HA client connection, open **More -> Home Assistant / Sensors** in GREE Controller and press **Create new token**. The secret is shown once. Paste that token into the Home Assistant `GREE Controller` integration together with the controller URL. Managed HA tokens are stored as SHA-256 hashes and are scoped to the dedicated HA device/control-plan/house-control/zone-control API; they cannot change controller settings or manage other tokens.
|
||||
|
||||
The outbound HA sensor client also supports local HTTPS endpoints such as `https://192.168.50.25`. If the server uses a self-signed, expired or hostname-mismatched certificate, enable **Home Assistant / Sensors -> Allow invalid/self-signed HTTPS certificate**. This disables certificate/hostname validation only for the controller's outbound Home Assistant sensor client and should only be used on a trusted LAN.
|
||||
|
||||
The second option is designed to replace the built-in/default GREE integration without changing automation/dashboard references.
|
||||
|
||||
Example migration target:
|
||||
Configure **Home Assistant / Sensors** in the Web UI or use:
|
||||
|
||||
```text
|
||||
old: climate.klima_salon -> built-in GREE integration
|
||||
new: climate.klima_salon -> GREE Controller custom integration -> Rust API -> AC
|
||||
HA_URL=
|
||||
HA_TOKEN=
|
||||
HA_ENTITY_ID=
|
||||
HA_OUTDOOR_ENTITY_ID=
|
||||
HA_SENSOR_STALE_AFTER_SECONDS=300
|
||||
HA_ALLOW_INVALID_TLS=false
|
||||
```
|
||||
|
||||
Generate an entity mapping:
|
||||
Per zone, `sensor_source` can be:
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--entity climate.klima_salon \
|
||||
--device gree-aabbccddeeff
|
||||
```
|
||||
- `device` — GREE indoor sensor,
|
||||
- `home_assistant` — configured HA room sensor,
|
||||
- `combined` — weighted GREE + HA value.
|
||||
|
||||
Or validate against the controller and automatically use its only device:
|
||||
Stale/unavailable external data falls back to the GREE sensor when possible. The optional outdoor sensor is only an assist signal; it never replaces room temperature.
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--entity climate.klima_salon \
|
||||
--controller-url http://192.168.1.20:8787 \
|
||||
--controller-token YOUR_CONTROLLER_TOKEN
|
||||
```
|
||||
### 2. GREE Controller entities inside Home Assistant
|
||||
|
||||
The generated file must be copied to:
|
||||
Bundled integration directory:
|
||||
|
||||
```text
|
||||
/config/gree_controller_entities.json
|
||||
home-assistant/custom_components/gree_controller/
|
||||
```
|
||||
|
||||
Full installation and safe takeover procedure: [`home-assistant/README.md`](home-assistant/README.md) and [`docs/HOME_ASSISTANT_MIGRATION.md`](docs/HOME_ASSISTANT_MIGRATION.md).
|
||||
Copy `gree_controller` to Home Assistant's `custom_components` directory, restart Home Assistant and add **GREE Controller** from Integrations.
|
||||
|
||||
## API
|
||||
Create a restricted token in **Home Assistant / Sensors → Access tokens**. The token secret is displayed only once and the database stores only its SHA-256 hash. The integration uses a restricted `/api/integrations/home-assistant/*` surface rather than administrator endpoints.
|
||||
|
||||
Important endpoints:
|
||||
The integration exposes physical device climate controls, thermostat zone climate entities, whole-house controls, groups and optional unit capabilities. Prefer zone thermostat entities for normal comfort control; physical device entities are direct/manual controls.
|
||||
|
||||
```text
|
||||
GET /api/health
|
||||
GET /api/bootstrap
|
||||
POST /api/discovery
|
||||
GET /api/devices
|
||||
POST /api/devices
|
||||
GET /api/devices/:id
|
||||
PATCH /api/devices/:id
|
||||
POST /api/devices/:id/bind
|
||||
POST /api/devices/:id/poll
|
||||
POST /api/devices/:id/command
|
||||
GET /api/zones
|
||||
POST /api/zones
|
||||
POST /api/zones/:id/control
|
||||
POST /api/zones/:id/schedule-template
|
||||
GET /api/groups
|
||||
POST /api/groups
|
||||
GET /api/groups/:id
|
||||
PUT /api/groups/:id
|
||||
DELETE /api/groups/:id
|
||||
POST /api/groups/:id/control
|
||||
POST /api/house/control
|
||||
POST /api/house/power
|
||||
POST /api/house/preset
|
||||
GET /api/schedules
|
||||
POST /api/schedules
|
||||
GET /api/automations
|
||||
POST /api/automations
|
||||
GET /api/readings
|
||||
GET /api/history
|
||||
GET /api/control-plan
|
||||
GET /api/events
|
||||
GET /api/settings
|
||||
PUT /api/settings
|
||||
GET /api/settings/export
|
||||
POST /api/settings/import
|
||||
GET /api/debug
|
||||
PUT /api/debug
|
||||
GET /api/access-tokens
|
||||
POST /api/access-tokens
|
||||
DELETE /api/access-tokens/{id}
|
||||
POST /api/integrations/home-assistant/test
|
||||
GET /api/integrations/home-assistant/devices
|
||||
POST /api/integrations/home-assistant/devices/{id}/command
|
||||
GET /api/integrations/home-assistant/control-plan
|
||||
GET /api/integrations/home-assistant/groups
|
||||
POST /api/integrations/home-assistant/groups/{id}/control
|
||||
POST /api/integrations/home-assistant/house/control
|
||||
POST /api/integrations/home-assistant/house/preset
|
||||
POST /api/integrations/home-assistant/house/power
|
||||
POST /api/integrations/home-assistant/zones/{id}/control
|
||||
WS /ws
|
||||
```
|
||||
|
||||
Example command:
|
||||
For entity-ID migration tooling:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8787/api/devices/sim-salon/command \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-d '{"power":true,"mode":"cool","target_temperature":22,"fan_speed":3}'
|
||||
python3 scripts/generate_ha_migration.py --help
|
||||
```
|
||||
|
||||
`/api/health` is public. The normal controller API and Web UI require `GREE_CONTROLLER_APP_TOKEN` only when that administrator token is configured. The dedicated `/api/integrations/home-assistant/*` client endpoints always require either a generated HA access token or the administrator token. WebSocket accepts only the administrator token using `?token=...`.
|
||||
The generated mapping example is under `home-assistant/generated/`.
|
||||
|
||||
More examples: [`docs/API.md`](docs/API.md).
|
||||
## History and InfluxDB
|
||||
|
||||
SQLite is the local hot store. History compaction reduces older local sample density while preserving the resolutions needed by charts. Retention is configurable from the UI or environment.
|
||||
|
||||
Optional InfluxDB writes samples in parallel and can serve the older part of long history queries:
|
||||
|
||||
- InfluxDB 1.x: URL, database, optional username/password.
|
||||
- InfluxDB 2.x: URL, organization, bucket and token.
|
||||
|
||||
If an Influx query fails, the API returns available SQLite data and can include a `storage_warning` rather than discarding the whole history response.
|
||||
|
||||
See [`GET /api/history`](docs/API.md#history-and-readings) for scopes and bucket behavior.
|
||||
|
||||
## Notifications
|
||||
|
||||
Optional notifications support:
|
||||
|
||||
- Pushover
|
||||
- Slack webhook
|
||||
- Discord webhook
|
||||
|
||||
Modes can report problems only or problems plus important state changes. Individual categories include sensor freshness/errors, communication failures, target timeout, automation/control errors and other warnings. Cooldown and failure thresholds are configurable.
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable **Settings → Application → On-screen debug**.
|
||||
|
||||
The overlay has three filters:
|
||||
|
||||
- **All** — request diagnostics, application events and GREE frames,
|
||||
- **Requests** — HTTP method/path/status/duration,
|
||||
- **GREE** — sanitized protocol frames when GREE frame logging is enabled.
|
||||
|
||||
Binding/encryption keys are not intentionally exposed in GREE debug frames.
|
||||
|
||||
For Linux/network issues also use:
|
||||
|
||||
```bash
|
||||
./scripts/network-debug.sh
|
||||
./scripts/service.sh logs
|
||||
```
|
||||
|
||||
## Backup and restore
|
||||
|
||||
**Settings → Application → Configuration backup** exports settings, devices, zones, groups, schedules and automations.
|
||||
|
||||
The export intentionally does **not** contain metric history, event history or generated API-token records. It **does contain** GREE binding keys and configured integration secrets, so store it like a credential file.
|
||||
|
||||
Import replaces application configuration while preserving local metric/event history and generated access-token records. Runtime ownership/timers are sanitized and live device state is re-polled before automatic control resumes.
|
||||
|
||||
## Reverse proxy and sub-path deployment
|
||||
|
||||
For a root deployment, proxy HTTP and WebSocket traffic to the controller normally.
|
||||
|
||||
For a sub-path, configure:
|
||||
|
||||
```bash
|
||||
GREE_CONTROLLER_BASE_PATH=/gree
|
||||
```
|
||||
|
||||
Then serve the application under `/gree/`. The proxy must preserve WebSocket upgrade headers. A typical nginx location is:
|
||||
|
||||
```nginx
|
||||
location /gree/ {
|
||||
proxy_pass http://127.0.0.1:8787/gree/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
```
|
||||
|
||||
If your proxy strips the prefix, keep its upstream/base-path handling consistent. Do not publish the controller directly to an untrusted network without authentication and TLS at the proxy/VPN layer.
|
||||
|
||||
## Security
|
||||
|
||||
- The application does not terminate TLS. Use HTTPS reverse proxy or VPN on untrusted networks.
|
||||
- Do not expose port 8787 directly to the public Internet.
|
||||
- Use a long random `GREE_CONTROLLER_APP_TOKEN`.
|
||||
- Protect `.env`, `/etc/gree-controller.env`, SQLite data and HA tokens.
|
||||
- The controller sends device commands only to configured local-network devices.
|
||||
The default empty `GREE_CONTROLLER_APP_TOKEN` is intended for a trusted LAN only. Set a long random token when the interface is reachable by other networks.
|
||||
|
||||
## Backup
|
||||
Implemented safeguards include:
|
||||
|
||||
For a systemd installation:
|
||||
- optional bearer administrator authentication,
|
||||
- separate restricted generated tokens for Home Assistant,
|
||||
- hashed generated-token storage,
|
||||
- secrets omitted from normal settings responses,
|
||||
- restrictive security headers and no permissive CORS,
|
||||
- API responses marked `no-store`,
|
||||
- hardened systemd service,
|
||||
- safe device power-off behavior when thermostat ownership is removed,
|
||||
- startup synchronization before the thermostat engine begins issuing commands.
|
||||
|
||||
For remote access, prefer a VPN or authenticated TLS reverse proxy. Treat configuration exports and `/etc/gree-controller.env` as secrets.
|
||||
|
||||
## Localization
|
||||
|
||||
Language packs are JSON files under `lang/`. The build validates/embeds them and generates the public language catalog.
|
||||
|
||||
To add a language:
|
||||
|
||||
1. Copy `lang/en.json` to `lang/<code>.json`.
|
||||
2. Update `meta.code`, names and locale.
|
||||
3. Keep the same translation keys as the reference pack.
|
||||
4. Rebuild the Rust binary.
|
||||
|
||||
Runtime assets are available at `/lang/index.json` and `/lang/<code>.json`.
|
||||
|
||||
## API
|
||||
|
||||
The HTTP API is the same backend used by the Web UI. It includes devices, zones, groups, house control, schedules, automations, history, control plan, events, settings, backup/restore, debug, tokens and integration tests.
|
||||
|
||||
Start here:
|
||||
|
||||
**[docs/API.md — complete API reference](docs/API.md)**
|
||||
|
||||
Public health check:
|
||||
|
||||
```bash
|
||||
systemctl stop gree-controller
|
||||
cp /var/lib/gree-controller/gree-controller.db /safe/backup/location/
|
||||
systemctl start gree-controller
|
||||
curl http://127.0.0.1:8787/api/health
|
||||
```
|
||||
|
||||
Development data is stored under `data/` by default.
|
||||
Authenticated example when an app token is configured:
|
||||
|
||||
```bash
|
||||
curl -H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
http://127.0.0.1:8787/api/system/info
|
||||
```
|
||||
|
||||
## Project layout
|
||||
|
||||
```text
|
||||
src/api.rs HTTP API, WebSocket, embedded web assets
|
||||
src/db.rs SQLite persistence and row/domain mapping
|
||||
src/queries.rs all SQLite schema and SQL statements
|
||||
src/engine.rs polling, zones, schedules and automations
|
||||
src/protocol/ GREE UDP/AES discovery, bind, status, command
|
||||
src/home_assistant.rs optional HA sensor client
|
||||
web/ mobile-first bilingual PWA
|
||||
home-assistant/custom_components/ HA custom integration
|
||||
scripts/install.sh first LXC/systemd installation
|
||||
scripts/update.sh safe LXC/systemd update with backup/rollback
|
||||
scripts/service.sh service status/start/stop/restart/logs/health
|
||||
scripts/dev.sh development build/run/check workflow
|
||||
scripts/generate_ha_migration.py legacy HA entity-ID mapping generator
|
||||
scripts/smoke.sh API smoke test
|
||||
systemd/ systemd service unit
|
||||
src/ Rust backend, GREE protocol and thermostat engine
|
||||
web/ Embedded Web UI / PWA
|
||||
lang/ Runtime language packs
|
||||
home-assistant/ Home Assistant custom integration and migration output
|
||||
scripts/ Development, install, update, service and diagnostics
|
||||
systemd/ Production service unit
|
||||
docs/API.md Complete API documentation
|
||||
.env.example Environment reference
|
||||
```
|
||||
|
||||
The application is intentionally self-contained: static assets and language packs are embedded into the binary at build time; SQLite is the default data store; no external frontend build chain is required.
|
||||
|
||||
## Validation before release
|
||||
|
||||
Recommended full check on a host with Rust installed:
|
||||
|
||||
```bash
|
||||
./scripts/dev.sh --check
|
||||
```
|
||||
|
||||
This runs formatting checks, Rust tests, a build and an isolated simulated API smoke test.
|
||||
|
||||
## License
|
||||
|
||||
MIT. This project uses a community-reconstructed local device protocol and is not an official product of GREE Electric Appliances Inc.
|
||||
|
||||
|
||||
## Multi-NIC / dedicated GREE interface
|
||||
|
||||
For LXC hosts with a management NIC and a separate GREE/VLAN NIC, bind GREE UDP traffic explicitly to the GREE interface:
|
||||
|
||||
```env
|
||||
GREE_CONTROLLER_GREE_INTERFACE=eth1
|
||||
GREE_CONTROLLER_DISCOVERY_BROADCAST=auto
|
||||
GREE_CONTROLLER_SIMULATE=false
|
||||
GREE_CONTROLLER_AUTO_SEED=false
|
||||
```
|
||||
|
||||
`GREE_CONTROLLER_GREE_INTERFACE` is resolved to the interface current IPv4 address whenever a UDP socket is created, so DHCP address changes do not require hard-coding the source IP. `GREE_CONTROLLER_DISCOVERY_BROADCAST=auto` derives the subnet broadcast from the selected interface and overrides an older runtime value stored in SQLite.
|
||||
|
||||
Run `sudo ./scripts/network-debug.sh eth1 10.87.65.127` to verify addressing and routing.
|
||||
|
||||
### v0.3.7 LXC networking fix
|
||||
|
||||
The systemd service explicitly allows `AF_NETLINK`. This is required by Linux `getifaddrs()` when resolving `GREE_CONTROLLER_GREE_INTERFACE` on multi-NIC LXC installations. Without it, older releases could fail with `Address family not supported by protocol (os error 97)` even though the configured interface was correct.
|
||||
|
||||
### v0.3.8 legacy V1 device compatibility
|
||||
|
||||
GREE protocol identifiers are now emitted as canonical lowercase hexadecimal in `tcid` and `pack.mac`. This improves compatibility with older V1 Wi-Fi modules (including devices in the `502cc6...` family) that can answer discovery but silently ignore bind/status packets when MAC casing differs from their discovery identity. Stored device IDs and friendly names are not changed.
|
||||
|
||||
### v0.4.0 smart thermostat
|
||||
|
||||
Zones now use setpoint modulation, global seasonal house mode, cooling/heating Comfort/Sleep/Away profiles, temporary per-zone overrides, ready-made editable schedule templates, whole-house preset actions and optional outdoor-temperature assist. The UI also uses theme-aware thin scrollbars and can opt in to invalid/self-signed certificates for the outbound Home Assistant sensor connection. Existing zones remain readable; a legacy zone keeps its previous setpoint as its comfort target until it is edited and saved with the new profile fields.
|
||||
|
||||
|
||||
## 0.7.1 quick-control visual consistency
|
||||
|
||||
- Unified dashboard Quick Thermostats and Quick Manual Control under shared `quick-control-*` CSS classes instead of separate dashboard-ID overrides.
|
||||
- Standardized compact option buttons, gaps and row grids across presets, HVAC modes, fan speeds and quick unit toggles.
|
||||
- Kept full Zones and Devices pages at their existing larger control sizes; the compact style applies only to dashboard quick controls.
|
||||
- Renamed the compact zone mode from House to Global to make its meaning explicit: the zone inherits the global Heating/Cooling mode selected in GREE Controller.
|
||||
- Renamed Sleep now to Sleep in quick thermostat presets.
|
||||
- Advanced the PWA cache key to `gree-controller-v071-quick-controls`.
|
||||
|
||||
## 0.7.2 zone configuration separation
|
||||
|
||||
- The **Zones** view is now configuration-only: no live +/- temperature, preset or mode buttons are shown there.
|
||||
- Current zone status remains visible as read-only context, together with mode policy, sensor strategy, hysteresis and Smart Fan configuration.
|
||||
- The **Control** button opens the Dashboard, expands **Quick thermostats** and scrolls to the matching zone.
|
||||
- The zone form no longer exposes the legacy/base `setpoint` as a live-looking control; profile temperatures remain configurable.
|
||||
- Live zone temperature, preset, mode and enable/disable controls remain available in **Dashboard -> Quick thermostats**.
|
||||
|
||||
## 0.7.3 Home Assistant climate groups
|
||||
|
||||
- Added climate groups to the restricted Home Assistant API.
|
||||
- Each configured group is exposed as its own Home Assistant device with **Power**, **Thermostat mode**, **Work profile** and **Control plan** entities.
|
||||
- Group mode options are **Global**, **Cooling** and **Heating**; Global means the member zones inherit the controller-wide Heating/Cooling selection.
|
||||
- Group profile options are **Auto schedule**, **Comfort**, **Sleep** and **Away**.
|
||||
- The group control-plan sensor reports member zones, online unit count, current average temperature, demand count and upcoming member schedule events.
|
||||
- Mixed member modes/profiles are reported without inventing a false common value; choosing a group option normalizes all member zones through the existing group-control engine.
|
||||
- Group commands use the restricted Home Assistant access token and the dedicated `/api/integrations/home-assistant/groups/*` endpoints.
|
||||
- After adding/removing/renaming groups, reload the GREE Controller integration in Home Assistant so its entity/device registry is rebuilt.
|
||||
|
||||
## 0.7.6 Home Assistant command-state stability
|
||||
|
||||
- Physical-unit commands now tolerate the short GREE firmware settling window after a successful command ACK before publishing a contradictory status. This prevents stale immediate polls from producing visible `ON -> OFF -> ON` state bounce.
|
||||
- The Home Assistant coordinator keeps a just-accepted device command as a short-lived pending value while refreshing controller metadata. An overlapping stale read cannot temporarily roll back the HA climate/feature state; normal factual polling resumes automatically after the bounded guard window.
|
||||
|
||||
## 0.7.5 group power state visibility
|
||||
|
||||
- Group **ON/OFF** buttons now use the same strong active-state highlight as group mode and profile controls, so the current power gate is immediately visible.
|
||||
- Simulator summary wording now uses **Zones with demand** / **Strefy z zapotrzebowaniem** instead of the less natural comfort-request wording.
|
||||
|
||||
## 0.7.4 manual remote override
|
||||
|
||||
- Physical GREE changes detected during polling for power, HVAC mode, target temperature and meaningful fan-speed changes now enter a per-zone **Manual control / remote** takeover state.
|
||||
- While takeover is active, thermostat modulation, schedules, groups and direct-device automations observe the zone but do not overwrite the unit.
|
||||
- The pre-takeover climate state is remembered. If the user returns the unit to that operational state, takeover clears automatically and **Resume automation** is no longer shown. If the unit was OFF before takeover, returning it to OFF is enough even when the remote retained a different dormant setpoint. Otherwise takeover expires at the next schedule transition when available; without one it remains until **Resume automation** or another explicit quick-thermostat action.
|
||||
- Technical climate control from the Devices view uses the same takeover semantics, while unrelated controls such as panel light do not pause the thermostat.
|
||||
- Disabled zones remain outside normal group/automation ownership; their unit can be operated manually.
|
||||
- **Turn all off** remains authoritative: it clears all takeovers, disables groups and powers off enabled units. Whole-house/group ON paths do not wake zones currently protected by manual takeover.
|
||||
- The UI and control-plan API expose the takeover state and its optional expiry.
|
||||
|
||||
|
||||
## 0.7.7 Quick-control label fit
|
||||
|
||||
- Adjusted the five-column quick-device mode row so longer Polish labels such as **Chłodzenie** and **Osuszanie** receive more width without reducing the whole row font size.
|
||||
- The change is limited to the dashboard quick-device mode row; thermostat/group layouts are unchanged.
|
||||
- Rotated the PWA cache key so browsers fetch the corrected stylesheet after upgrade.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.8.10 Dashboard sections and notification filters
|
||||
|
||||
- Dashboard is split into three horizontal tabs: **Main**, **Thermostats** and **Manual control**. Each tab opens one normal vertically scrolling section instead of nested disclosure panels.
|
||||
- Main keeps the summary, global power/house climate and control plan; Thermostats contains group controls and zone thermostats; Manual control contains direct unit controls.
|
||||
- The top toolbar shows the live powered-unit count (`active/total`), and **Discover** moved from the global toolbar to the Devices page.
|
||||
- Runtime control ownership timestamps now include both date and time.
|
||||
- Notification settings now expose per-category delivery switches. Stale Home Assistant readings have their own `ha.sensor_stale` category and can be suppressed without disabling other sensor, communication, automation or thermostat alerts.
|
||||
- Rotated the PWA cache key so upgraded clients fetch the new dashboard and settings UI.
|
||||
|
||||
## 0.8.9 Remote-takeover diagnostics
|
||||
|
||||
- Reverted the v0.8.8 two-poll confirmation heuristic for unexpected GREE states; unexpected states outside the known controller-command/baseline settling envelope are again acted on immediately.
|
||||
- Added `device.remote_control_detected` diagnostics for poll-detected manual/pilot takeover, including raw/detected fields, before/after physical state, active controller settling commands/baselines and remaining settling-window time, plus current zone/Temporary Quick Thermostat ownership state.
|
||||
- Retains the v0.8.8 narrow-screen Temporary Quick Thermostat modal layout fix.
|
||||
|
||||
## 0.8.8 Poll-takeover confirmation and mobile modal fit
|
||||
|
||||
- A single unexpected GREE poll snapshot inside the controller-command settling window no longer immediately becomes a physical/pilot takeover. The same unexpected physical state must persist into the next poll, so one transient mixed/out-of-order frame cannot break a scheduled Temporary Quick Thermostat start.
|
||||
- Genuine direct/remote changes are still recognized after confirmation; deferred candidates are not lost when the next poll has no before/after delta.
|
||||
- On narrow screens the Temporary Quick Thermostat **End and return to automation** button is placed below the active-session text and uses the full modal width.
|
||||
- Rotated the PWA cache key so the corrected mobile stylesheet is fetched after upgrade.
|
||||
|
||||
## 0.8.7 Temporary-session state cleanup
|
||||
|
||||
- `activated_at` is now the only source of truth for Temporary Quick Thermostat ownership.
|
||||
- Removed the legacy `generation=0` / `local_thermostat_power` activation fallback from the backend and Web UI.
|
||||
- Removed the Temporary-session `generation` compatibility field and its legacy-only test path.
|
||||
- Kept ordinary Quick Thermostat state only as an independent lower-priority owner restored after Temporary hand-back.
|
||||
|
||||
## 0.8.6 Control-logic audit remediation
|
||||
|
||||
- Separates ordinary Quick Thermostat handback state from Temporary Quick Thermostat lifecycle and makes fresh-session activation depend on `activated_at`.
|
||||
- Hardens ownership priority so direct/manual takeover outranks temporary completion and pauses temporary hold/runtime accounting.
|
||||
- Freezes active Temporary mode/target while deferring house/group climate changes until handback.
|
||||
- Prevents unmanaged whole-house/group power-on, closes generic Settings/import bypasses, and safely stops devices before disable/detach.
|
||||
- Makes thermostat-managed device automations durable zone-state changes instead of competing one-shot device frames.
|
||||
- Adds schedule/time-automation deadlines to the common wakeup path and refreshes temporary schedule-boundary deadlines after schedule edits.
|
||||
- Aligns Web UI/API lifecycle handling for active delayed/at sessions and exposes scheduled/waiting-master/paused-manual/active states.
|
||||
|
||||
## 0.8.5 Temporary temperature-condition activation fix
|
||||
|
||||
- Fixed `temperature_stable` / `temperature_reached` evaluation for sessions already marked active through `activated_at`.
|
||||
- Kept delayed sessions protected from evaluating or completing before their scheduled start.
|
||||
- Preserved migration compatibility with active v0.8.3 sessions by also accepting existing local Quick Thermostat ownership as an activation marker.
|
||||
- Fixes the two v0.8.4 regression tests that stopped `scripts/update.sh` before service replacement.
|
||||
|
||||
## 0.8.4 Scheduled Temporary Quick Thermostat start and ownership hardening
|
||||
|
||||
- Temporary Quick Thermostat can start immediately, after a configurable delay, or at a specific local date/time.
|
||||
- Delayed sessions do not take thermostat ownership early: normal schedules and automations continue until the effective start instant.
|
||||
- Duration, safety timeout and next-schedule-boundary finish rules are calculated from the effective start, not from the time the session is configured.
|
||||
- Active temporary ownership keeps its requested target across schedule transitions and schedule edits; direct device automations and group climate changes cannot overwrite it while it is active.
|
||||
- A local Quick Thermostat can run using the zone's local Heat/Cool mode even when the inherited house climate mode is set to **Do not control**; the separate whole-house master power remains authoritative.
|
||||
- Cancelling a not-yet-started session only removes that session and does not clear unrelated quick-control state.
|
||||
|
||||
## 0.8.3 Temporary Quick Thermostat sessions
|
||||
|
||||
- Adds a separate persisted timed Quick Thermostat session without reusing the existing 15-minute local-OFF hand-back timer.
|
||||
- Supports ending after a duration, at a date/time, after reaching a temperature, after continuously holding a temperature condition, or at the next schedule boundary.
|
||||
- Temperature conditions support within/at-or-below/at-or-above operators, tolerance and an optional safety timeout.
|
||||
- Active sessions show their countdown/status directly on the Quick Thermostat card and restore the previous automation-enabled state when finished.
|
||||
- Moves the zone automation enabled switch out of Quick Thermostat controls so automation availability and temporary thermostat ownership are visually separate.
|
||||
|
||||
## 0.8.2 UI/i18n and Home Assistant sensor freshness
|
||||
|
||||
- fixes ownership translations that were accidentally stored outside the language-pack `translations` object,
|
||||
- adds a configurable Home Assistant sensor freshness limit in the Home Assistant / Sensors settings page (default 5 minutes),
|
||||
- keeps legacy non-default per-zone stale limits as API-level overrides,
|
||||
- prevents taller Quick Thermostat / Direct Device cards from stretching neighbouring dashboard cards,
|
||||
- adds build-time validation for misplaced language-pack keys.
|
||||
|
||||
## 0.8.0 Control ownership and thermostat safety
|
||||
|
||||
- Adds an explicit zone control-ownership model (`control_owner`, command source, since/resume/reason) shared by the engine, API, Web UI and Home Assistant control plan.
|
||||
- Distinguishes direct Web, direct Home Assistant and external/remote takeovers. Quick Thermostat commands explicitly return ownership to the thermostat.
|
||||
- Adds per-zone operation mutexes plus an optional monotonic `revision` check for conflict-safe zone updates without breaking older clients that do not send a revision.
|
||||
- Recomputes both thermostat override and physical-device manual takeover deadlines after schedule edits.
|
||||
- Rejects stale Home Assistant temperature samples (default 300 seconds per zone) and falls back to the GREE sensor through the existing fallback path.
|
||||
- Enforces automatic HVAC compressor protection: minimum OFF time before restart and an OFF delay when automatically changing Heat/Cool modes. Direct/manual commands and global safety OFF remain authoritative.
|
||||
- Extends control-plan with desired vs actual HVAC state, ownership, block reason and lockout deadline.
|
||||
- Shows the current control owner and timing in the Web dashboard. Home Assistant zone climate exposes the same diagnostics; the physical climate is named `Direct control` to make its semantics explicit.
|
||||
|
||||
## 0.7.13 Quick-thermostat race protection and GREE frame counters
|
||||
|
||||
- Quick thermostat actions from Web/Home Assistant are serialized with device polling, so a poll running at the same time cannot restore an older `device_manual_override` snapshot after the thermostat deliberately takes control back.
|
||||
- Recent controller-originated climate transitions keep a bounded settling history of both requested and pre-command values. Delayed or out-of-order GREE status frames can pass through those known states without being misclassified as physical/pilot takeover.
|
||||
- In Dashboard -> Quick thermostats, **Global** is disabled when the whole-house thermostat mode is **Do not control**. Heat/Cool remain available as explicit per-zone overrides.
|
||||
- Settings now shows live UDP receive counters, both total and per configured air conditioner, since controller startup. Counters are independent of the debug-frame overlay and update over WebSocket.
|
||||
|
||||
## 0.7.12 Timed quick-thermostat hand-back
|
||||
|
||||
- Switching a single Quick Thermostat **OFF** now starts a 15-minute local-OFF window instead of leaving the zone outside automation indefinitely.
|
||||
- The Web UI shows a live `MM:SS` countdown and a **Resume now** action during that window.
|
||||
- At expiry, only local quick-thermostat ownership and its temporary quick profile/setpoint are cleared. The controller does not restore a stale physical ON/OFF state; it recalculates the current group, house mode, schedule, profile, room temperature and hysteresis.
|
||||
- Every fresh local **OFF** action creates a new backend-owned 15-minute deadline; switching the local thermostat back **ON** cancels it.
|
||||
- A physical/direct `device_manual_override` remains separate and temporarily suspends expiry of an existing local-OFF hand-back. Returning the unit to its previous OFF state re-arms a fresh 15-minute countdown from that moment.
|
||||
- Persisted local-OFF state from 0.7.10/0.7.11 receives a fresh 15-minute hand-back deadline on the first controller cycle after upgrade.
|
||||
|
||||
## 0.7.11 Cleanup and Rust test fix
|
||||
|
||||
- Removed the obsolete `/api/zones/{id}/manual-power` compatibility endpoint and its handler; local thermostat power has one canonical API path: `/api/zones/{id}/control`.
|
||||
- Renamed dashboard internals from `manual-power` terminology to `local-thermostat-power` to match the actual control model.
|
||||
- Fixed the Rust ownership error in `local_thermostat_ownership_blocks_direct_automation` by borrowing the test zone via `std::slice::from_ref`, so the test no longer moves `zone` while borrowing `zone.device_id`.
|
||||
|
||||
## 0.7.10 Local quick-thermostat ownership
|
||||
|
||||
- Quick thermostat ON/OFF now controls a dedicated local thermostat state instead of using remote/manual-device semantics.
|
||||
- A locally started zone bypasses its disabled group power gate but keeps full thermostat regulation: room sensor, hysteresis, profile/target, Smart Fan and GREE setpoint modulation.
|
||||
- **Resume automation** clears local thermostat ownership and returns the zone to group/schedule control.
|
||||
- Physical remote/direct device changes remain the only source of `device_manual_override`; explicit thermostat actions can take ownership back.
|
||||
- Controller-originated GREE changes that appear after the device settling window are correlated with the pending controller command, preventing group ON and thermostat commands from falsely showing **Manual control / remote**.
|
||||
- Home Assistant zone climate ON/OFF now uses the same local thermostat power semantics instead of technically disabling the zone.
|
||||
|
||||
## 0.7.9 Zone profile control and Home Assistant stability
|
||||
|
||||
- Quick thermostat profile/mode/target/enable actions now wake the thermostat regulator immediately instead of waiting for the next fixed zone interval.
|
||||
- A manual/remote takeover keeps the configured thermostat profile target visible; the physical GREE target is reported separately as the device setpoint. This prevents standby setpoints such as 25 C from being shown as the Sleep/Comfort target.
|
||||
- Control-plan `enabled` now represents the configured per-zone switch. A new `effective_enabled` field reports group gating, so Home Assistant zone switches/climate entities no longer bounce back to Off solely because a climate group is disabled.
|
||||
- Home Assistant zone controls use a short pending-state guard, matching the existing physical-device anti-bounce protection.
|
||||
- Every zone now exposes a **Work profile** select and the zone `climate` entity supports preset modes: Auto schedule, Comfort, Sleep and Away.
|
||||
|
||||
## 0.7.8 Zone status wording and unit power
|
||||
|
||||
- Zone runtime status now says **Zapotrzebowanie aktywne** when the thermostat is requesting work and **Cel osiągnięty** when the target/hysteresis condition is satisfied; **Komfort** remains only the preset name. Paused control is shown as **Oczekiwanie**, and missing control temperature as **Brak pomiaru**.
|
||||
- Quick thermostats use one contextual **Włącz jednostkę / Wyłącz jednostkę** control instead of the redundant **Jednostka · pilot** caption and separate ON/OFF buttons.
|
||||
- Direct unit power still behaves as manual/remote takeover and does not enable the whole group.
|
||||
See [LICENSE](LICENSE).
|
||||
|
||||
+1359
-219
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
# Home Assistant entity-ID migration
|
||||
|
||||
## Goal
|
||||
|
||||
Replace an existing GREE climate entity while preserving its `entity_id`.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Before: climate.klima_salon -> default GREE integration -> AC
|
||||
After: climate.klima_salon -> GREE Controller integration -> Rust controller -> AC
|
||||
```
|
||||
|
||||
Keeping the same entity ID allows existing dashboards, scripts, scenes and automations that reference the entity by ID to continue working.
|
||||
|
||||
## Why a takeover step is required
|
||||
|
||||
Home Assistant's entity registry reserves entity IDs. A second integration cannot create another active `climate.klima_salon` while the original entity still exists. The new integration therefore checks for conflicts and stops setup rather than allowing HA to generate a suffixed name such as `climate.klima_salon_2`.
|
||||
|
||||
## Generate the mapping
|
||||
|
||||
For one entity:
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--entity climate.klima_salon \
|
||||
--device gree-aabbccddeeff
|
||||
```
|
||||
|
||||
For several:
|
||||
|
||||
```bash
|
||||
./scripts/generate_ha_migration.py \
|
||||
--map climate.klima_salon=gree-aabbccddeeff \
|
||||
--map climate.klima_sypialnia=gree-112233445566
|
||||
```
|
||||
|
||||
Generated structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": "climate.klima_salon",
|
||||
"device_id": "gree-aabbccddeeff"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Copy it to `/config/gree_controller_entities.json` on the Home Assistant host.
|
||||
|
||||
## Migration sequence
|
||||
|
||||
1. Add the physical AC to the standalone Rust application.
|
||||
2. Test power, mode and target temperature from its web UI.
|
||||
3. Generate the HA entity mapping.
|
||||
4. Copy the custom component to `/config/custom_components/gree_controller/`.
|
||||
5. Disable/remove the previous GREE integration in HA so it no longer controls or publishes the old climate entity.
|
||||
6. Remove any stale entity registry entry only after the old integration is unloaded.
|
||||
7. Add the **GREE Controller** integration and provide its URL/token.
|
||||
8. Confirm the exact old entity ID is present again.
|
||||
9. Test `climate.set_temperature`, HVAC modes and power from HA.
|
||||
10. Verify automations and dashboards that use the preserved entity ID.
|
||||
|
||||
The standalone controller remains available during the HA migration, so the AC can still be controlled from its own web UI if HA is restarting.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Localization
|
||||
|
||||
The web interface uses JSON language packs from `lang/`. Language files are discovered at Rust build time and embedded in the application binary.
|
||||
|
||||
English (`lang/en.json`) is required and is always the fallback language. Polish (`lang/pl.json`) is included by default.
|
||||
|
||||
## Add a language
|
||||
|
||||
1. Copy `lang/en.json` to a file named with the new language code, for example `lang/de.json`.
|
||||
2. Update the `meta` object.
|
||||
3. Translate values inside `translations`. Do not rename translation keys.
|
||||
4. Run `./scripts/dev.sh --check` or build the project again.
|
||||
5. Start the rebuilt binary. The new language appears automatically in the language selector.
|
||||
|
||||
Example structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"code": "de",
|
||||
"name": "German",
|
||||
"native_name": "Deutsch",
|
||||
"locale": "de-DE"
|
||||
},
|
||||
"translations": {
|
||||
"controls.language": "Sprache",
|
||||
"controls.theme": "Darstellung"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The filename and `meta.code` must match (`de.json` -> `"code": "de"`). File names may contain ASCII letters, digits, `-` and `_` only.
|
||||
|
||||
## Fallback behavior
|
||||
|
||||
A language pack does not have to duplicate every English key while it is being developed. If a key is missing from the selected language, the UI uses the value from `en.json`. If a key is also missing from English, the translation key itself is shown, which makes incomplete strings visible during development.
|
||||
|
||||
## Build-time validation
|
||||
|
||||
`build.rs` checks that:
|
||||
|
||||
- at least one JSON language file exists,
|
||||
- `en.json` exists,
|
||||
- every language file contains valid JSON,
|
||||
- every file has `meta.code`, `meta.name`, `meta.native_name` and `meta.locale`,
|
||||
- `meta.code` matches the filename,
|
||||
- `translations` is a JSON object.
|
||||
|
||||
A malformed language pack fails the Rust build instead of producing a broken selector at runtime.
|
||||
|
||||
## Runtime endpoints
|
||||
|
||||
The embedded language catalog is available at:
|
||||
|
||||
```text
|
||||
GET /lang/index.json
|
||||
```
|
||||
|
||||
Individual embedded packs are available at:
|
||||
|
||||
```text
|
||||
GET /lang/en.json
|
||||
GET /lang/pl.json
|
||||
GET /lang/<code>.json
|
||||
```
|
||||
|
||||
These endpoints are intentionally public so that localization also works before API authentication is completed.
|
||||
|
||||
## Browser preference
|
||||
|
||||
The selected language code is stored for one year in the `gree_controller_language` cookie. If the stored language is no longer present in a later build, the UI falls back to English.
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
# LXC installation and update
|
||||
|
||||
This document describes the supported Debian/Ubuntu systemd deployment used for LXC testing.
|
||||
|
||||
## First installation
|
||||
|
||||
Unpack the source archive inside the container and run:
|
||||
|
||||
```bash
|
||||
cd gree-controller
|
||||
chmod +x scripts/*.sh
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
The installer:
|
||||
|
||||
1. installs required build packages when missing,
|
||||
2. installs stable Rust with rustup when Cargo is unavailable,
|
||||
3. runs `cargo test --all-targets`,
|
||||
4. builds `target/release/gree-controller`,
|
||||
5. creates the `gree-controller` system user/group,
|
||||
6. creates `/var/lib/gree-controller`, `/opt/gree-controller` and `/var/backups/gree-controller`,
|
||||
7. installs the systemd unit,
|
||||
8. creates `/etc/gree-controller.env` only when it does not already exist,
|
||||
9. enables/restarts the service,
|
||||
10. verifies `GET /api/health`.
|
||||
|
||||
Installed state is intentionally outside the extracted source directory:
|
||||
|
||||
```text
|
||||
/opt/gree-controller/gree-controller installed binary
|
||||
/etc/gree-controller.env persistent configuration/secrets
|
||||
/var/lib/gree-controller/gree-controller.db persistent SQLite database
|
||||
/var/backups/gree-controller/ update backups
|
||||
/etc/systemd/system/gree-controller.service systemd service
|
||||
```
|
||||
|
||||
The default installation starts in simulator mode. Change `GREE_CONTROLLER_SIMULATE=false` and `GREE_CONTROLLER_AUTO_SEED=false` in `/etc/gree-controller.env` when moving to physical units, then restart the service.
|
||||
|
||||
## Update
|
||||
|
||||
Unpack a newer source archive and run from that new directory:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh
|
||||
```
|
||||
|
||||
The update workflow is designed to minimize downtime and preserve the database:
|
||||
|
||||
1. build dependencies/Rust are checked,
|
||||
2. tests run while the currently installed controller stays online,
|
||||
3. the new release binary is built while the old service stays online,
|
||||
4. the service is stopped,
|
||||
5. the installed binary, service unit, environment file and SQLite database are copied to `/var/backups/gree-controller/<UTC timestamp>/`,
|
||||
6. the new binary/unit is installed,
|
||||
7. systemd starts the new version,
|
||||
8. `/api/health` is checked,
|
||||
9. on failure the old binary, unit and database are restored automatically.
|
||||
|
||||
The updater does not overwrite `/etc/gree-controller.env` during a successful update.
|
||||
|
||||
Use `--skip-tests` only for deliberate fast testing:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh --skip-tests
|
||||
```
|
||||
|
||||
## Service helper
|
||||
|
||||
```bash
|
||||
./scripts/service.sh status
|
||||
./scripts/service.sh health
|
||||
./scripts/service.sh logs
|
||||
sudo ./scripts/service.sh restart
|
||||
sudo ./scripts/service.sh stop
|
||||
sudo ./scripts/service.sh start
|
||||
```
|
||||
|
||||
Equivalent native commands remain available:
|
||||
|
||||
```bash
|
||||
systemctl status gree-controller
|
||||
journalctl -u gree-controller -f
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit:
|
||||
|
||||
```text
|
||||
/etc/gree-controller.env
|
||||
```
|
||||
|
||||
and restart:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/service.sh restart
|
||||
```
|
||||
|
||||
Do not store controller or Home Assistant secrets in the source tree.
|
||||
|
||||
## Database and SQL layout
|
||||
|
||||
Runtime persistence uses SQLite. All schema definitions and SQL statements in the Rust application are centralized in:
|
||||
|
||||
```text
|
||||
src/queries.rs
|
||||
```
|
||||
|
||||
`src/db.rs` contains connection/transaction logic and maps database rows to Rust domain models, but it does not embed SQL statements.
|
||||
|
||||
|
||||
## Two network interfaces
|
||||
|
||||
When the LXC has a management interface and a dedicated GREE network, configure the service environment explicitly. Example for `eth1` on `10.87.65.0/25`:
|
||||
|
||||
```env
|
||||
GREE_CONTROLLER_GREE_INTERFACE=eth1
|
||||
GREE_CONTROLLER_DISCOVERY_BROADCAST=auto
|
||||
|
||||
This explicit interface configuration is recommended for predictable LXC deployments. Version 0.3.7 also automatically selects the directly connected local IPv4 address for a GREE device when the interface variable is omitted; for example, a target in `10.87.65.0/25` selects the local address on that subnet.
|
||||
GREE_CONTROLLER_SIMULATE=false
|
||||
GREE_CONTROLLER_AUTO_SEED=false
|
||||
```
|
||||
|
||||
Restart the service after editing `/etc/gree-controller.env`:
|
||||
|
||||
```bash
|
||||
systemctl restart gree-controller
|
||||
journalctl -u gree-controller -n 100 --no-pager
|
||||
```
|
||||
|
||||
During discovery the log should contain a line similar to:
|
||||
|
||||
```text
|
||||
Starting GREE discovery target=10.87.65.127:7000 local=10.87.65.27:<ephemeral> interface=eth1
|
||||
```
|
||||
|
||||
Use `scripts/network-debug.sh` for routing diagnostics.
|
||||
|
||||
|
||||
## Mixed GREE model generations
|
||||
|
||||
Version 0.3.7 can discover both AES-ECB and AES-GCM modules. In the Web UI choose **Discover -> Auto (V1 + V2)** and use 3-5 scan passes. If a family is still missing, repeat with V1-only and V2-only to see which protocol its Wi-Fi module answers with.
|
||||
|
||||
A single command/status timeout no longer immediately flips a device offline; offline requires three consecutive communication failures.
|
||||
|
||||
## Multi-NIC LXC and AF_NETLINK
|
||||
|
||||
When `GREE_CONTROLLER_GREE_INTERFACE` is set, the controller enumerates IPv4 addresses with Linux `getifaddrs()`. On Linux this requires a Netlink socket. The systemd sandbox therefore allows `AF_NETLINK` in addition to `AF_UNIX`, `AF_INET`, and `AF_INET6`.
|
||||
|
||||
If an older unit reports:
|
||||
|
||||
```text
|
||||
getifaddrs failed
|
||||
Address family not supported by protocol (os error 97)
|
||||
```
|
||||
|
||||
update the systemd unit or add `AF_NETLINK` to `RestrictAddressFamilies`, then run:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl restart gree-controller
|
||||
```
|
||||
|
||||
For a dedicated GREE NIC such as `eth1`, the expected startup/bind log should identify the IPv4 address of that interface instead of `0.0.0.0`.
|
||||
|
||||
### Legacy V1 devices discovered but not binding
|
||||
|
||||
Version 0.3.8 sends the GREE protocol `tcid` and inner `mac` identifiers in canonical lowercase hexadecimal. If an older `502cc6...` device is discovered on UDP/7000 but stays offline after bind timeouts, update to v0.3.8 before changing routing or firewall settings. With debug logging, `Sending GREE request` should show `wire_mac=502cc6...` in lowercase.
|
||||
|
||||
|
||||
## Home Assistant HTTPS with a self-signed certificate
|
||||
|
||||
The optional outbound HA sensor client validates TLS certificates by default. For a trusted local endpoint such as `https://10.87.65.2` that uses a self-signed, expired or hostname-mismatched certificate, enable **Settings -> Allow invalid/self-signed HTTPS certificate** and save. The same initial setting can be supplied as `HA_ALLOW_INVALID_TLS=true`.
|
||||
|
||||
This opt-in affects only controller -> Home Assistant sensor requests. It does not change the GREE UDP transport and it does not add HTTPS termination to the controller itself.
|
||||
@@ -1,196 +0,0 @@
|
||||
# GREE Controller project specification
|
||||
|
||||
## Product goal
|
||||
|
||||
Build an autonomous local GREE HVAC controller running primarily as a Rust service in a dedicated Linux/LXC environment. Home Assistant is optional: it can provide external sensor data and can consume controller devices through a thin custom integration, but it must not contain the core GREE protocol or heating logic.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Home Assistant / Web UI / REST clients
|
||||
|
|
||||
REST + WebSocket
|
||||
|
|
||||
GREE Controller (Rust)
|
||||
+--------------------------------+
|
||||
| Device/state manager |
|
||||
| GREE protocol UDP/AES |
|
||||
| Zone/heating engine |
|
||||
| Scheduler |
|
||||
| Automation engine |
|
||||
| SQLite |
|
||||
| Mobile-first web UI |
|
||||
+--------------------------------+
|
||||
|
|
||||
UDP/7000
|
||||
|
|
||||
GREE AC
|
||||
```
|
||||
|
||||
## Independence requirements
|
||||
|
||||
- GREE control must work when Home Assistant is stopped.
|
||||
- Local schedules and automations must continue without HA.
|
||||
- HA sensor failures must never remove basic access to the AC.
|
||||
- Device state is persisted by the controller and can be restored to clients after reconnect/restart.
|
||||
|
||||
## GREE protocol layer
|
||||
|
||||
The protocol implementation is isolated from application logic and covers:
|
||||
|
||||
- discovery,
|
||||
- packet encoding/decoding,
|
||||
- AES-128-ECB support,
|
||||
- AES-128-GCM envelope support,
|
||||
- bind/key acquisition,
|
||||
- status polling,
|
||||
- command transport,
|
||||
- reconnect/offline handling.
|
||||
|
||||
The rest of the application works with generic device state and commands rather than raw protocol packets.
|
||||
|
||||
## Device model
|
||||
|
||||
Each device stores:
|
||||
|
||||
- stable controller ID,
|
||||
- MAC/CID,
|
||||
- name,
|
||||
- IP/port,
|
||||
- protocol version,
|
||||
- model/firmware when available,
|
||||
- encryption key when bound,
|
||||
- enabled/simulated flags,
|
||||
- power and HVAC mode,
|
||||
- target/current/outdoor temperature,
|
||||
- fan, swing, quiet, turbo, light,
|
||||
- online/last-seen/error state.
|
||||
|
||||
## Smart thermostat, zones and schedules
|
||||
|
||||
The primary user model is a fast thermostat, not raw device automation. A global climate mode selects the season (`cool`, `heat`, `off`). Zones follow it by default and can optionally override Heat/Cool. The UI labels this as **Mode policy**: keep **Follow global mode** when the zone should inherit the mode selected globally in GREE Controller; the fixed Cooling/Heating policies are explicit overrides. House `off` is a **do not control** state for inherited zones: manual device operation is left untouched, while a zone explicitly switched to Heat/Cool can operate independently. Whole-house power on/off is a separate master state. Master Off is authoritative for zones and controller automations but does not modify the selected house thermostat mode.
|
||||
|
||||
During normal heating/cooling the controller minimizes unit power cycling. It keeps the indoor unit powered and uses **setpoint modulation**:
|
||||
|
||||
- when the zone requires conditioning, use the active target (optionally assisted slightly by outdoor weather),
|
||||
- when the zone is satisfied, move the device setpoint to the non-demand side of the room target so the inverter/compressor can stop naturally,
|
||||
- use hysteresis and a minimum adjustment interval to avoid command chatter,
|
||||
- optionally adjust fan speed based on room error and outdoor extremes; with Smart fan enabled, a satisfied zone uses Low fan together with the standby setpoint and requests Quiet mode in the same frame when the unit supports it. Unsupported Quiet commands automatically fall back to Low fan without breaking thermostat control.
|
||||
|
||||
Each zone has separate seasonal profile temperatures:
|
||||
|
||||
- Cooling: Comfort / Sleep / Away,
|
||||
- Heating: Comfort / Sleep / Away.
|
||||
|
||||
The zone can use the GREE internal sensor, its own Home Assistant room sensor, or a weighted combination. External room sensors are configured per zone; loss/discrepancy falls back safely to the GREE sensor.
|
||||
|
||||
Weekly schedules reference profiles instead of duplicating temperatures. Ready-made Family, Child room, Bedroom, Workday and Always-comfort templates create ordinary schedule rows that remain fully editable. A manual **Sleep**, Comfort, Away or custom-temperature override ends automatically at the next schedule boundary; if no future transition exists, it remains active until explicitly cleared. Whole-house preset actions apply the same temporary policy to every zone.
|
||||
|
||||
Automations remain available for advanced exceptions. Daily comfort should be implemented with zones/profiles/schedules so direct-device automation commands do not compete with the thermostat engine.
|
||||
|
||||
Climate groups are named collections of thermostat zones (for example Upstairs/Downstairs). Group commands can independently set power, Heat/Cool/house-follow mode and Auto/Comfort/Sleep/Away profile for only their member zones. Automation actions may target either a direct device or a climate group.
|
||||
|
||||
### Outdoor temperature assist
|
||||
|
||||
A configured Home Assistant outdoor-temperature entity is optional auxiliary context. It never replaces the room-control temperature. Under strong heat/cold the engine may slightly bias the active AC target and increase Smart Fan airflow. If HA is unavailable, the smart thermostat continues without outdoor assistance.
|
||||
|
||||
## Scheduler and automations
|
||||
|
||||
Schedules are weekly time windows and may cross midnight. Equal start/end times mean a 24-hour block for each selected weekday. Enabled schedules for one zone may not overlap. Schedules can select Comfort/Sleep/Away or a custom setpoint. Automations support time or temperature triggers and device commands with cooldown protection; a failed command also observes the cooldown instead of retrying every control cycle. They are intended for exceptions rather than the normal daily thermostat cycle.
|
||||
|
||||
The automation engine runs in Rust and does not require Home Assistant YAML automation logic.
|
||||
|
||||
## API
|
||||
|
||||
The controller exposes REST for configuration/commands and WebSocket for live state updates. The HA integration and web UI use the same controller API.
|
||||
|
||||
## Home Assistant direction 1: external sensor input
|
||||
|
||||
The Rust application can read a specifically configured HA entity using a Long-Lived Access Token. This input is optional and is not a prerequisite for GREE control.
|
||||
|
||||
## Home Assistant direction 2: native climate entities
|
||||
|
||||
A custom integration creates HA climate entities and translates HA service calls to controller API commands. It does not communicate with GREE directly.
|
||||
|
||||
For migrations from the default GREE integration, a mapping file can request an existing entity ID such as `climate.klima_salon`. The previous integration must release that ID before takeover.
|
||||
|
||||
## Web UI
|
||||
|
||||
Primary use is from a phone. Requirements:
|
||||
|
||||
- responsive mobile-first layout,
|
||||
- touch-friendly controls,
|
||||
- live state updates,
|
||||
- device, zone, schedule, automation, history and diagnostics screens,
|
||||
- JSON-based language packs from `lang/*.json`, with English as the required default/fallback and Polish included,
|
||||
- automatic language discovery at build time, so a new valid `<code>.json` file adds a language without JavaScript changes,
|
||||
- language stored in the `gree_controller_language` cookie,
|
||||
- system/light/dark appearance modes,
|
||||
- appearance stored in a cookie,
|
||||
- flat visual design without decorative shadows,
|
||||
- no letter-logo badge next to the application name.
|
||||
|
||||
## Storage
|
||||
|
||||
SQLite stores configuration, state, readings and events. PostgreSQL or other external database services are not required for the single-node deployment target.
|
||||
|
||||
## Deployment
|
||||
|
||||
Primary target:
|
||||
|
||||
```text
|
||||
LXC / Debian or Ubuntu
|
||||
/opt/gree-controller/gree-controller
|
||||
/etc/gree-controller.env
|
||||
/var/lib/gree-controller/gree-controller.db
|
||||
systemd: gree-controller.service
|
||||
```
|
||||
|
||||
A development script prepares dependencies/build/runtime configuration, and a separate installer creates the systemd deployment.
|
||||
|
||||
## Security
|
||||
|
||||
- optional administrator Bearer token for the controller Web/API,
|
||||
- separately generated, revocable Home Assistant client tokens stored only as SHA-256 hashes,
|
||||
- Home Assistant client tokens are restricted to device read/control endpoints,
|
||||
- secrets kept outside source control,
|
||||
- no direct public Internet exposure,
|
||||
- TLS delegated to a trusted reverse proxy/VPN when required,
|
||||
- local device keys and HA token protected in environment/database files,
|
||||
- invalid/self-signed HA HTTPS certificates are rejected by default; bypass is an explicit trusted-LAN opt-in.
|
||||
|
||||
|
||||
## Per-zone room sensors
|
||||
|
||||
A zone represents one room and normally maps one GREE indoor unit to one optional external room-temperature sensor. External sensors are not global. For example, Living Room can use `sensor.living_room_temperature` while Bedroom independently uses `sensor.bedroom_temperature`.
|
||||
|
||||
The zone controller supports `device`, `combined`, and `home_assistant` temperature strategies. Combined mode uses a configurable external-sensor weight (40% by default), validates the difference between sensors, and falls back to the GREE sensor when the external source is missing or outside the configured discrepancy limit. The local GREE measurement therefore remains available even when Home Assistant is offline.
|
||||
|
||||
|
||||
## Operational packaging conventions
|
||||
|
||||
- All operator-facing shell/Python utilities live under `scripts/`.
|
||||
- `scripts/install.sh` performs the first systemd/LXC installation.
|
||||
- `scripts/update.sh` performs an in-place update with a stopped SQLite backup, health check and automatic rollback.
|
||||
- `scripts/service.sh` provides common systemd operations.
|
||||
- Cargo's `build.rs` stays at the package root because Cargo requires that location.
|
||||
- Every SQLite statement and schema definition is centralized in `src/queries.rs`; database/domain code must not embed SQL strings elsewhere.
|
||||
|
||||
|
||||
## Web UI design system (v0.4.4)
|
||||
|
||||
The embedded UI uses the classic GREE Controller visual language: large rounded surfaces, circular thermostat controls, comfortable spacing, compact desktop navigation and mobile bottom navigation. Light, Dark and System themes use neutral surfaces with a restrained green accent.
|
||||
|
||||
The UI ships as a normal static `web/styles.css` file embedded into the Rust binary. There is no frontend package manager, CDN, or CSS build pipeline.
|
||||
|
||||
### Rich zone history
|
||||
|
||||
Zone history stores GREE temperature, optional Home Assistant room temperature, calculated control temperature, profile target, actual device setpoint, outdoor temperature assist, power, mode, fan speed, demand, active preset and control source. The UI can compare all zones on common temperature/target charts or inspect one zone in detail.
|
||||
|
||||
### History information architecture (v0.4.4)
|
||||
|
||||
History is not a single long page. It is split into URL-addressable views: Overview, Zones, GREE devices, HA sensors and Custom chart. Device history exists independently from zones, HA sensors have their own history stream, and zone history adds the thermostat/control context. A compatibility fallback maps existing GREE device readings into a zone timeline when the richer zone table is still empty.
|
||||
|
||||
The custom chart composer can mix GREE indoor/outdoor/target values, zone GREE/HA/control/target/device-setpoint/outdoor values, and HA temperature entities. Saved definitions persist in browser storage and a chart definition can be serialized into the `/history/custom?chart=...` URL for direct sharing/bookmarking.
|
||||
|
||||
Quick thermostat temperature nudges are optimistic in the browser and stored separately from the selected Comfort/Sleep/Away/Auto preset. Changing `+/-` therefore does not silently leave Auto mode.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Reverse proxy
|
||||
|
||||
The web UI supports HTTPS/WSS and sub-path deployments. The server honors `GREE_CONTROLLER_BASE_PATH` and also understands `X-Forwarded-Prefix` when a proxy strips the prefix before forwarding.
|
||||
|
||||
## Root deployment
|
||||
|
||||
Proxy `/` to `http://127.0.0.1:8787` and forward WebSocket upgrade headers. No controller setting is required.
|
||||
|
||||
## Sub-path deployment
|
||||
|
||||
For a public path such as `/gree`, either:
|
||||
|
||||
- set `GREE_CONTROLLER_BASE_PATH=/gree` and proxy `/gree` to the controller without stripping the path, or
|
||||
- strip `/gree` in the proxy and send `X-Forwarded-Prefix: /gree`.
|
||||
|
||||
Static assets, language packs, browser routes, API requests, PWA scope and WebSocket URLs use the effective base path.
|
||||
|
||||
### nginx example (prefix stripped)
|
||||
|
||||
```nginx
|
||||
location /gree/ {
|
||||
proxy_pass http://127.0.0.1:8787/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /gree;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
For an Internet-facing endpoint, add TLS and proxy authentication. If the application token is enabled, avoid access-log formats that record query strings because the browser WebSocket connection includes the token in its URL.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Security notes
|
||||
|
||||
The controller is designed primarily for a trusted LAN. This release adds safe defaults without forcing authentication on existing installations.
|
||||
|
||||
## Implemented safeguards
|
||||
|
||||
- `GREE_CONTROLLER_APP_TOKEN=` remains supported. Authentication is optional and existing installations are compatible.
|
||||
- Permissive CORS was removed. The web UI and API are same-origin by default.
|
||||
- Responses include `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Content-Security-Policy` and a restrictive `Permissions-Policy`; API responses are marked `no-store`.
|
||||
- Runtime secrets for Home Assistant, InfluxDB and notifications are stored server-side and never returned to the browser. Empty secret fields keep the previously stored value.
|
||||
- Slack and Discord notification webhooks require HTTPS and are restricted to the official webhook hosts. Redirect following is disabled for these outbound webhook requests. Pushover uses its fixed official API endpoint.
|
||||
- API access tokens are stored hashed. Administrator token behavior is unchanged for backwards compatibility.
|
||||
- History/event API limits and SQLite indexes bound common read paths. History retention/compaction is configurable.
|
||||
|
||||
## Remaining risks / recommendations
|
||||
|
||||
1. With an empty application token, every client that can reach the controller HTTP port can operate the air conditioners and change configuration. Keep port 8787 on a trusted VLAN or put authentication in the reverse proxy.
|
||||
2. The WebSocket administrator token is still passed in its query string because browser WebSocket APIs cannot set an Authorization header. Avoid logging query strings at the proxy and prefer HTTPS/WSS when a token is enabled.
|
||||
3. The application does not terminate TLS. Use a reverse proxy or VPN for traffic crossing an untrusted network.
|
||||
4. Home Assistant `allow_invalid_tls` should be used only with a known local instance; keep it disabled otherwise.
|
||||
5. Configuration exports contain device keys and may contain integration secrets. Treat backup files as secrets.
|
||||
6. There is no per-client API rate limiter. For exposure beyond a trusted LAN, configure rate limiting at the reverse proxy.
|
||||
7. No CSRF token is used. Same-origin operation, removal of permissive CORS and bearer-token authentication reduce the risk, but an authenticated public deployment should also use proxy-level origin/access controls.
|
||||
|
||||
## Suggested reverse-proxy hardening
|
||||
|
||||
- TLS only; redirect HTTP to HTTPS.
|
||||
- Do not log URL query strings if administrator authentication is enabled.
|
||||
- Pass WebSocket upgrades for `/ws`.
|
||||
- Add authentication at the proxy when `GREE_CONTROLLER_APP_TOKEN` is empty and the service is reachable outside the trusted LAN.
|
||||
- Limit request body sizes and add basic request-rate limiting.
|
||||
@@ -1,73 +0,0 @@
|
||||
1. Timer powrotu do automatyzacji
|
||||
|
||||
Backend już przechowywał local_thermostat_resume_at, ale przy takim scenariuszu:
|
||||
|
||||
lokalny OFF → licznik 15 min → pilot/direct ON → pilot/direct OFF
|
||||
|
||||
stary deadline nadal istniał. Sterowanie pilotem miało wyższy priorytet, ale timer lokalnego OFF nadal „leciał pod spodem”.
|
||||
|
||||
Poprawiłem to tak:
|
||||
|
||||
dodałem centralną metodę set_local_thermostat_power(...),
|
||||
każde nowe OFF ustawia świeże now + 15 min,
|
||||
ON kasuje deadline,
|
||||
podczas device_manual_override timer lokalnego OFF nie może wygasnąć,
|
||||
gdy użytkownik po pilocie/direct wróci do wcześniejszego stanu OFF, backend ustawia nowe pełne 15 minut,
|
||||
HA bierze dokładny local_thermostat_resume_at zwrócony przez backend, więc stary odczyt nie powinien chwilowo przywracać poprzedniego timera.
|
||||
|
||||
Frontend nadal może odświeżać MM:SS co sekundę lokalnie — to jest właściwe. Frontend tylko wyświetla czas, backend decyduje kiedy przejąć sterowanie. Jeśli chcesz całkowicie uniezależnić wyświetlanie od zegara przeglądarki, później można dodać server_time albo remaining_seconds.
|
||||
|
||||
2. Pilot + Home Assistant + Web — jak powinno to działać
|
||||
|
||||
Tutaj obecna architektura jest już częściowo dobra. Masz dwa różne rodzaje poleceń:
|
||||
|
||||
bezpośrednie urządzenie — pilot, Web „Devices”, fizyczny climate w HA,
|
||||
termostat strefy — Quick Thermostat w Web i climate.<strefa>_thermostat w HA.
|
||||
|
||||
Logicznie ustawiłbym następujące zasady:
|
||||
|
||||
Pilot/direct przejmuje urządzenie. Powstaje device_manual_override. Harmonogram, grupa i automatyka nie walczą wtedy z człowiekiem.
|
||||
Kolejna komenda direct z HA/Web zmienia tę samą ręczną sesję i nadal nie uruchamia automatyki.
|
||||
Komenda do termostatu strefy oznacza świadome: „teraz znowu steruje regulator”. Wtedy device_manual_override jest kasowany i zaczyna obowiązywać termostat.
|
||||
Grupy, harmonogram i automatyzacje nie powinny po cichu odbierać sterowania pilotowi.
|
||||
Globalne OFF powinno pozostać nadrzędne — i obecny kod właśnie tak działa.
|
||||
|
||||
To jest bardzo sensowna polityka.
|
||||
|
||||
W panelu dodałbym jednak jawny status jednego właściciela:
|
||||
|
||||
Automatyka / Termostat lokalny / Pilot / HA — sterowanie bezpośrednie / Web — sterowanie bezpośrednie / Globalnie wyłączone
|
||||
|
||||
oraz:
|
||||
|
||||
od kiedy,
|
||||
do kiedy,
|
||||
dlaczego,
|
||||
przycisk Przejmij sterowanie termostatem.
|
||||
|
||||
API też powinno to wystawiać, np. logicznie jako:
|
||||
|
||||
control_owner
|
||||
control_source
|
||||
control_since
|
||||
resume_at
|
||||
|
||||
Obecnie device_manual_override jest zapisany, ale dokładne źródło przejęcia jest głównie w logach. Przy dalszym rozwoju warto zrobić z tego normalny stan.
|
||||
|
||||
Szczególnie w HA rozważyłbym, żeby fizyczny climate urządzenia był oznaczony jako „sterowanie bezpośrednie / zaawansowane”, a głównym encją użytkową był termostat strefy. Teraz użytkownik może mieć dwa climate dla jednego klimatyzatora, które celowo mają inne znaczenie — łatwo się pomylić.
|
||||
|
||||
3. Co jeszcze brakuje przed używaniem tego jako głównego ogrzewania/chłodzenia
|
||||
|
||||
Najważniejsze rzeczy, w tej kolejności:
|
||||
|
||||
Blokada współbieżnych zmian strefy. To jest realny problem w obecnym kodzie. HA i Web mogą prawie równocześnie pobrać ten sam Zone, zmienić różne pola i zapisać cały JSON. Ostatni zapis może nadpisać poprzednią zmianę. Dałbym per-zone mutex oraz docelowo revision/optimistic locking. To jest dla mnie najwyższy następny priorytet.
|
||||
Formalny model własności sterowania. Zamiast sprawdzania w wielu miejscach device_manual_override, local_thermostat_power, group gate itd., jedna metoda typu resolve_control_owner() powinna określać kto aktualnie może wysyłać komendy. Panel, HA i engine korzystałyby z tej samej odpowiedzi.
|
||||
Ochrona przed szybkim przełączaniem Heat ↔ Cool / OFF ↔ ON. W modelu masz min_on_seconds i min_off_seconds, ale obecnie praktycznie nie są wykorzystywane przez regulator. Zmiana trybu jest wręcz traktowana jako pilna. Dla głównego źródła ogrzewania dodałbym minimum 3–5 minut blokady po wyłączeniu oraz przy zmianie Heat/Cool.
|
||||
Kontrola świeżości czujnika HA. read_temperature() sprawdza, czy wartość jest liczbą, ale nie sprawdza wieku last_updated. Sensor może wisieć z poprawną wartością przez wiele godzin. Potrzebny jest np. sensor_stale_after=5 min, fallback na GREE i informacja/alarm.
|
||||
Tryby ręcznego „hold”. Zamiast jednej zasady warto mieć: 15 min, 30 min, 1 h, do kolejnego harmonogramu, do godziny..., bezterminowo. Ten sam mechanizm można potem wykorzystać dla temperatury, profilu, pilota i lokalnego OFF.
|
||||
Zmiana harmonogramu podczas ręcznego przejęcia. Kod aktualizuje granicę manual_override_until dla ręcznego profilu/setpointu po edycji harmonogramu, ale nie widzę analogicznego przeliczania device_manual_override_until. To może pozostawić nieaktualny termin przejęcia po zmianie grafiku.
|
||||
Desired state vs actual state. Warto w API jasno publikować „czego chce regulator” i „co faktycznie ma klimatyzator”, plus powód rozbieżności: pilot, offline, timer, group OFF, lockout. Część tego już masz w control-plan, więc to jest naturalne rozszerzenie.
|
||||
Ochrona temperaturowa. Dla głównego ogrzewania przyda się opcjonalny „frost protection”, np. alarm lub awaryjne grzanie poniżej określonej temperatury. Powinno być osobno konfigurowalne, żeby świadome globalne OFF nie zostało niespodziewanie złamane.
|
||||
Dopiero później rozważałbym automatyczne Heat/Cool na podstawie temperatury z dużą martwą strefą i minimalnym czasem pozostawania w jednym trybie.
|
||||
|
||||
Najbliższa zmiana architektoniczna, którą zrobiłbym teraz, to więc ControlOwnership + per-zone locking/revision. To rozwiąże większość przyszłych konfliktów HA ↔ Web ↔ pilot w jednym miejscu, zamiast dodawać kolejne wyjątki.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.13",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
+79
-3
@@ -406,8 +406,8 @@
|
||||
"settings.importConfirm": "Replace the current application configuration with this file? Existing metrics and API access tokens will be kept.",
|
||||
"debug.title": "Live debug",
|
||||
"debug.clear": "Clear",
|
||||
"debug.apiAndGree": "API logs + GREE frames",
|
||||
"debug.apiOnly": "API logs",
|
||||
"debug.apiAndGree": "Requests, events + GREE frames",
|
||||
"debug.apiOnly": "Requests and application events",
|
||||
"debug.empty": "No debug events yet.",
|
||||
"toast.exported": "Configuration exported",
|
||||
"toast.imported": "Configuration imported",
|
||||
@@ -692,6 +692,82 @@
|
||||
"notifications.alertControlErrors": "Thermostat and group control errors",
|
||||
"notifications.alertImportantEvents": "Important state changes",
|
||||
"notifications.alertOther": "Other warnings and errors",
|
||||
"notifications.testSent": "Test notification sent"
|
||||
"notifications.testSent": "Test notification sent",
|
||||
"actions.close": "Close",
|
||||
"actions.remove": "Remove",
|
||||
"plan.strategySetpoint": "Setpoint",
|
||||
"simulation.running": "RUNNING",
|
||||
"simulation.idle": "IDLE",
|
||||
"logs.info": "Info",
|
||||
"logs.days7": "7 days",
|
||||
"logs.days14": "14 days",
|
||||
"logs.days30": "30 days",
|
||||
"logs.days90": "90 days",
|
||||
"logs.days180": "180 days",
|
||||
"logs.days365": "365 days",
|
||||
"history.hoverHint": "Hover, tap or use ←/→ for exact values. Use +/− to zoom and swipe to pan.",
|
||||
"actions.saving": "Saving…",
|
||||
"actions.retry": "Retry",
|
||||
"auth.connecting": "Connecting…",
|
||||
"confirm.discardChanges": "Discard unsaved changes?",
|
||||
"status.offlineHint": "Live updates are unavailable. Displayed data may be stale.",
|
||||
"validation.required": "This field is required.",
|
||||
"validation.url": "Enter a valid http:// or https:// URL.",
|
||||
"validation.range": "Enter a value from {min} to {max}.",
|
||||
"validation.invalid": "Check this value.",
|
||||
"validation.entityId": "Use a Home Assistant entity_id such as sensor.living_room_temperature.",
|
||||
"validation.fixFields": "Correct the highlighted fields before saving.",
|
||||
"history.zoomControls": "Chart zoom",
|
||||
"history.zoomOut": "Zoom out",
|
||||
"history.zoomReset": "Reset zoom",
|
||||
"history.zoomIn": "Zoom in",
|
||||
"house.powerOffConfirm": "Turn off all air conditioners and disable all groups?",
|
||||
"zones.temporaryStarting": "Starting…",
|
||||
"notifications.applicationTokenPlaceholder": "Application token",
|
||||
"notifications.userKeyPlaceholder": "User/group key",
|
||||
"settings.testingHa": "Testing…",
|
||||
"settings.settingsSections": "Settings sections",
|
||||
"settings.applicationSettings": "Application",
|
||||
"settings.greeSettings": "GREE",
|
||||
"settings.applicationRuntime": "Application runtime",
|
||||
"settings.applicationRuntimeHint": "Application-wide behavior that does not change GREE transport or discovery.",
|
||||
"settings.simulationModeHint": "When enabled, a prominent simulation warning is shown throughout the application.",
|
||||
"settings.greeConnection": "GREE connection",
|
||||
"settings.greeConnectionHint": "Controller identity, polling intervals and LAN discovery transport.",
|
||||
"settings.systemStatusTitle": "Controller health",
|
||||
"settings.systemStateHint": "Live runtime, connectivity and storage details useful during setup and diagnostics.",
|
||||
"settings.controlEngine": "Control engine",
|
||||
"settings.ready": "Ready",
|
||||
"settings.syncing": "Synchronizing",
|
||||
"settings.websocket": "Live connection",
|
||||
"settings.runtimeMode": "Runtime mode",
|
||||
"settings.runtimeSimulation": "Simulation",
|
||||
"settings.runtimeLive": "Live devices",
|
||||
"settings.devicesConfigured": "Configured devices",
|
||||
"settings.devicesOnline": "Devices online",
|
||||
"settings.simulatedDevices": "Simulated devices",
|
||||
"settings.receivedFrames": "GREE frames received",
|
||||
"settings.httpBind": "HTTP bind",
|
||||
"settings.greeInterface": "GREE interface",
|
||||
"settings.database": "Database",
|
||||
"settings.basePath": "Base path",
|
||||
"settings.healthy": "Healthy",
|
||||
"settings.attention": "Check status",
|
||||
"debug.filterAria": "Debug source",
|
||||
"debug.all": "All",
|
||||
"debug.requests": "Requests",
|
||||
"debug.gree": "GREE",
|
||||
"debug.emptyRequests": "No HTTP requests captured yet.",
|
||||
"debug.emptyGree": "No GREE frames captured yet.",
|
||||
"simulation.bannerTitle": "SIMULATION MODE",
|
||||
"simulation.bannerText": "Simulation mode is enabled. Treat current states and results as test data.",
|
||||
"settings.pollCycle": "Device poll interval",
|
||||
"settings.zoneCycle": "Zone control interval",
|
||||
"settings.homeAssistantStatus": "Home Assistant",
|
||||
"settings.influxStatus": "InfluxDB history",
|
||||
"settings.notificationsStatus": "Notifications",
|
||||
"settings.debugStatus": "Debug overlay",
|
||||
"settings.configured": "configured",
|
||||
"settings.notConfigured": "not configured"
|
||||
}
|
||||
}
|
||||
|
||||
+85
-9
@@ -39,7 +39,7 @@
|
||||
"actions.edit": "Edytuj",
|
||||
"actions.delete": "Usuń",
|
||||
"actions.read": "Odczyt",
|
||||
"actions.bind": "Bind",
|
||||
"actions.bind": "Powiąż",
|
||||
"actions.noChange": "Bez zmiany",
|
||||
"actions.turnOn": "Włącz",
|
||||
"actions.turnOff": "Wyłącz",
|
||||
@@ -82,8 +82,8 @@
|
||||
"zones.greeSensor": "Tylko GREE",
|
||||
"zones.combinedSensor": "GREE + czujnik pokojowy",
|
||||
"zones.externalSensor": "Tylko czujnik pokojowy",
|
||||
"zones.minOn": "Min. ON (s)",
|
||||
"zones.minOff": "Min. OFF (s)",
|
||||
"zones.minOn": "Min. czas WŁ. (s)",
|
||||
"zones.minOff": "Min. czas WYŁ. (s)",
|
||||
"zones.greeSource": "sensor GREE",
|
||||
"zones.combinedSource": "GREE + czujnik pokojowy",
|
||||
"zones.externalSource": "czujnik pokojowy",
|
||||
@@ -144,7 +144,7 @@
|
||||
"settings.haTokenSaved": "Token zapisany — pozostaw puste",
|
||||
"settings.haLongLivedToken": "Długotrwały token dostępu",
|
||||
"settings.defaultEntity": "Domyślny entity_id",
|
||||
"settings.testHa": "Test HA",
|
||||
"settings.testHa": "Testuj HA",
|
||||
"settings.systemState": "Stan systemu",
|
||||
"settings.version": "Wersja",
|
||||
"settings.uptime": "Czas pracy",
|
||||
@@ -406,8 +406,8 @@
|
||||
"settings.importConfirm": "Zastąpić bieżącą konfigurację aplikacji tym plikiem? Istniejące metryki i tokeny dostępu API zostaną zachowane.",
|
||||
"debug.title": "Debug na żywo",
|
||||
"debug.clear": "Wyczyść",
|
||||
"debug.apiAndGree": "Logi API + ramki GREE",
|
||||
"debug.apiOnly": "Logi API",
|
||||
"debug.apiAndGree": "Requesty, zdarzenia + ramki GREE",
|
||||
"debug.apiOnly": "Requesty i zdarzenia aplikacji",
|
||||
"debug.empty": "Brak zdarzeń debug.",
|
||||
"toast.exported": "Konfiguracja wyeksportowana",
|
||||
"toast.imported": "Konfiguracja zaimportowana",
|
||||
@@ -500,7 +500,7 @@
|
||||
"devices.xfan": "X-FAN",
|
||||
"devices.health": "Health",
|
||||
"devices.air": "Air",
|
||||
"devices.sleep": "Sleep",
|
||||
"devices.sleep": "Sen",
|
||||
"nav.nightMode": "Tryb nocny",
|
||||
"nav.homeAssistant": "Home Assistant / Czujniki",
|
||||
"night.eyebrow": "Komfort",
|
||||
@@ -526,7 +526,7 @@
|
||||
"notifications.test": "Wyślij powiadomienie testowe",
|
||||
"zones.copyFrom": "Kopiuj ustawienia termostatu z…",
|
||||
"zones.copySettings": "Kopiuj ustawienia",
|
||||
"automations.preset": "Preset…",
|
||||
"automations.preset": "Szablon…",
|
||||
"automations.presetHot": "Chłodzenie przy wysokiej temperaturze",
|
||||
"automations.presetCold": "Grzanie przy niskiej temperaturze",
|
||||
"automations.presetMorning": "Komfort rano",
|
||||
@@ -692,6 +692,82 @@
|
||||
"notifications.alertControlErrors": "Błędy sterowania termostatami i grupami",
|
||||
"notifications.alertImportantEvents": "Ważne zmiany stanu",
|
||||
"notifications.alertOther": "Pozostałe ostrzeżenia i błędy",
|
||||
"notifications.testSent": "Wysłano powiadomienie testowe"
|
||||
"notifications.testSent": "Wysłano powiadomienie testowe",
|
||||
"actions.close": "Zamknij",
|
||||
"actions.remove": "Usuń",
|
||||
"plan.strategySetpoint": "Nastawa",
|
||||
"simulation.running": "PRACA",
|
||||
"simulation.idle": "CZUWA",
|
||||
"logs.info": "Informacje",
|
||||
"logs.days7": "7 dni",
|
||||
"logs.days14": "14 dni",
|
||||
"logs.days30": "30 dni",
|
||||
"logs.days90": "90 dni",
|
||||
"logs.days180": "180 dni",
|
||||
"logs.days365": "365 dni",
|
||||
"history.hoverHint": "Najedź, dotknij lub użyj ←/→, aby zobaczyć dokładne wartości. Użyj +/− do powiększenia i przesuń wykres palcem.",
|
||||
"actions.saving": "Zapisywanie…",
|
||||
"actions.retry": "Ponów",
|
||||
"auth.connecting": "Łączenie…",
|
||||
"confirm.discardChanges": "Odrzucić niezapisane zmiany?",
|
||||
"status.offlineHint": "Aktualizacje na żywo są niedostępne. Wyświetlane dane mogą być nieaktualne.",
|
||||
"validation.required": "To pole jest wymagane.",
|
||||
"validation.url": "Podaj poprawny adres URL http:// lub https://.",
|
||||
"validation.range": "Podaj wartość od {min} do {max}.",
|
||||
"validation.invalid": "Sprawdź tę wartość.",
|
||||
"validation.entityId": "Podaj entity_id Home Assistanta, np. sensor.salon_temperature.",
|
||||
"validation.fixFields": "Popraw zaznaczone pola przed zapisaniem.",
|
||||
"history.zoomControls": "Powiększenie wykresu",
|
||||
"history.zoomOut": "Pomniejsz wykres",
|
||||
"history.zoomReset": "Przywróć powiększenie",
|
||||
"history.zoomIn": "Powiększ wykres",
|
||||
"house.powerOffConfirm": "Wyłączyć wszystkie klimatyzatory i dezaktywować wszystkie grupy?",
|
||||
"zones.temporaryStarting": "Uruchamianie…",
|
||||
"notifications.applicationTokenPlaceholder": "Token aplikacji",
|
||||
"notifications.userKeyPlaceholder": "Klucz użytkownika/grupy",
|
||||
"settings.testingHa": "Testowanie…",
|
||||
"settings.settingsSections": "Sekcje ustawień",
|
||||
"settings.applicationSettings": "Aplikacja",
|
||||
"settings.greeSettings": "GREE",
|
||||
"settings.applicationRuntime": "Praca aplikacji",
|
||||
"settings.applicationRuntimeHint": "Ustawienia całej aplikacji niezwiązane z transportem ani wykrywaniem GREE.",
|
||||
"settings.simulationModeHint": "Po włączeniu na całym ekranie będzie widoczne wyraźne ostrzeżenie o trybie symulacji.",
|
||||
"settings.greeConnection": "Połączenie GREE",
|
||||
"settings.greeConnectionHint": "Identyfikator kontrolera, interwały odczytu i transport wykrywania urządzeń w LAN.",
|
||||
"settings.systemStatusTitle": "Kondycja kontrolera",
|
||||
"settings.systemStateHint": "Bieżące informacje o pracy, połączeniu i pamięci danych przydatne podczas konfiguracji i diagnostyki.",
|
||||
"settings.controlEngine": "Silnik sterowania",
|
||||
"settings.ready": "Gotowy",
|
||||
"settings.syncing": "Synchronizacja",
|
||||
"settings.websocket": "Połączenie na żywo",
|
||||
"settings.runtimeMode": "Tryb pracy",
|
||||
"settings.runtimeSimulation": "Symulacja",
|
||||
"settings.runtimeLive": "Urządzenia fizyczne",
|
||||
"settings.devicesConfigured": "Skonfigurowane urządzenia",
|
||||
"settings.devicesOnline": "Urządzenia online",
|
||||
"settings.simulatedDevices": "Urządzenia symulowane",
|
||||
"settings.receivedFrames": "Odebrane ramki GREE",
|
||||
"settings.httpBind": "Nasłuch HTTP",
|
||||
"settings.greeInterface": "Interfejs GREE",
|
||||
"settings.database": "Baza danych",
|
||||
"settings.basePath": "Ścieżka bazowa",
|
||||
"settings.healthy": "Działa prawidłowo",
|
||||
"settings.attention": "Sprawdź stan",
|
||||
"debug.filterAria": "Źródło debug",
|
||||
"debug.all": "Wszystko",
|
||||
"debug.requests": "Requesty",
|
||||
"debug.gree": "GREE",
|
||||
"debug.emptyRequests": "Brak przechwyconych requestów HTTP.",
|
||||
"debug.emptyGree": "Brak przechwyconych ramek GREE.",
|
||||
"simulation.bannerTitle": "TRYB SYMULACJI",
|
||||
"simulation.bannerText": "Tryb symulacji jest włączony. Traktuj bieżące stany i wyniki jako testowe.",
|
||||
"settings.pollCycle": "Interwał odczytu urządzeń",
|
||||
"settings.zoneCycle": "Interwał regulatora stref",
|
||||
"settings.homeAssistantStatus": "Home Assistant",
|
||||
"settings.influxStatus": "Historia InfluxDB",
|
||||
"settings.notificationsStatus": "Powiadomienia",
|
||||
"settings.debugStatus": "Panel debug",
|
||||
"settings.configured": "skonfigurowany",
|
||||
"settings.notConfigured": "nieskonfigurowany"
|
||||
}
|
||||
}
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_git_command(args, repo_path: Path) -> bytes:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=repo_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def get_files_to_archive(repo_path: Path) -> list[str]:
|
||||
output = run_git_command(
|
||||
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
||||
repo_path,
|
||||
)
|
||||
files = output.decode("utf-8", errors="surrogateescape").split("\0")
|
||||
return [f for f in files if f]
|
||||
|
||||
|
||||
def make_zip(repo_path: Path, output_zip: Path) -> None:
|
||||
files = get_files_to_archive(repo_path)
|
||||
|
||||
output_zip = output_zip.resolve()
|
||||
if output_zip.exists():
|
||||
output_zip.unlink()
|
||||
|
||||
with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for rel_path in files:
|
||||
abs_path = repo_path / rel_path
|
||||
|
||||
if not abs_path.exists():
|
||||
continue
|
||||
|
||||
if abs_path.resolve() == output_zip:
|
||||
continue
|
||||
|
||||
zf.write(abs_path, arcname=rel_path)
|
||||
|
||||
print(f"Created: {output_zip}")
|
||||
print(f"Added files: {len(files)}")
|
||||
|
||||
|
||||
def main():
|
||||
repo_path = Path.cwd()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
output_zip = Path(sys.argv[1])
|
||||
else:
|
||||
output_zip = repo_path / f"{repo_path.name}.zip"
|
||||
|
||||
try:
|
||||
run_git_command(["rev-parse", "--show-toplevel"], repo_path)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: this directory is not a Git repository.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
make_zip(repo_path, output_zip)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+14
-1
@@ -208,9 +208,13 @@ async fn bootstrap(State(state): State<AppState>) -> Result<Json<Value>, AppErro
|
||||
|
||||
async fn build_bootstrap(state: &AppState) -> Result<Value, AppError> {
|
||||
let settings = state.settings.read().await.clone();
|
||||
let devices = state.db.list_devices()?;
|
||||
let device_count = devices.len();
|
||||
let online_count = devices.iter().filter(|value| value.online).count();
|
||||
let simulator_count = devices.iter().filter(|value| value.simulated).count();
|
||||
let (received_frames_total, received_frames_by_device) = state.gree.received_frame_stats();
|
||||
Ok(json!({
|
||||
"devices": state.db.list_devices()?,
|
||||
"devices": devices,
|
||||
"zones": state.db.list_zones()?,
|
||||
"groups": state.db.list_groups()?,
|
||||
"schedules": state.db.list_schedules()?,
|
||||
@@ -223,6 +227,13 @@ async fn build_bootstrap(state: &AppState) -> Result<Value, AppError> {
|
||||
"uptime_seconds": state.started.elapsed().as_secs(),
|
||||
"auth_required": !state.config.app_token.trim().is_empty(),
|
||||
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
|
||||
"database": state.config.database.display().to_string(),
|
||||
"device_count": device_count,
|
||||
"online_count": online_count,
|
||||
"simulator_count": simulator_count,
|
||||
"bind": state.config.bind.to_string(),
|
||||
"base_path": if state.config.base_path.is_empty() { "/" } else { state.config.base_path.as_str() },
|
||||
"gree_interface": if state.config.gree_interface.trim().is_empty() { "auto" } else { state.config.gree_interface.trim() },
|
||||
"gree_received_frames": received_frames_total,
|
||||
"gree_received_frames_by_device": received_frames_by_device,
|
||||
}
|
||||
@@ -240,7 +251,9 @@ async fn system_info(State(state): State<AppState>) -> Result<Json<Value>, AppEr
|
||||
"online_count": devices.iter().filter(|v| v.online).count(),
|
||||
"simulator_count": devices.iter().filter(|v| v.simulated).count(),
|
||||
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
|
||||
"auth_required": !state.config.app_token.trim().is_empty(),
|
||||
"bind": state.config.bind.to_string(),
|
||||
"base_path": if state.config.base_path.is_empty() { "/" } else { state.config.base_path.as_str() },
|
||||
"gree_interface": if state.config.gree_interface.trim().is_empty() { "auto" } else { state.config.gree_interface.trim() },
|
||||
"gree_received_frames": received_frames_total,
|
||||
"gree_received_frames_by_device": received_frames_by_device,
|
||||
|
||||
+646
-76
File diff suppressed because it is too large
Load Diff
+71
-44
@@ -18,15 +18,32 @@
|
||||
</div>
|
||||
<div class="toolbar-runtime-status" id="toolbarRuntimeStatus" aria-live="polite">0/0</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>
|
||||
<div class="toolbar-picker" data-i18n-title="controls.language" title="Language">
|
||||
<span class="toolbar-picker-value" id="languageLabel" aria-hidden="true">EN</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<select class="toolbar-picker-select" id="languageSelect" data-i18n-aria="controls.language" aria-label="Language">
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-picker" data-i18n-title="controls.theme" title="Theme">
|
||||
<span class="toolbar-picker-value theme-picker-value" id="themeIcon" aria-hidden="true">◐</span>
|
||||
<span class="toolbar-picker-chevron" aria-hidden="true">⌄</span>
|
||||
<select class="toolbar-picker-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>
|
||||
</div>
|
||||
<button class="icon-button toolbar-icon-control" id="refreshButton" data-i18n-title="actions.refresh" data-i18n-aria="actions.refresh" title="Refresh" aria-label="Refresh">↻</button>
|
||||
</div>
|
||||
<div class="connection-banner" id="connectionBanner" role="status" aria-live="polite" hidden>
|
||||
<span class="connection-banner-dot" aria-hidden="true"></span>
|
||||
<div><strong id="connectionBannerTitle">Disconnected</strong><span id="connectionBannerText">Live updates are unavailable. Displayed data may be stale.</span></div>
|
||||
<button class="secondary" type="button" id="connectionRetry" data-i18n="actions.retry">Retry</button>
|
||||
</div>
|
||||
<div class="simulation-mode-banner" id="simulationModeBanner" role="status" aria-live="polite" hidden>
|
||||
<strong data-i18n="simulation.bannerTitle">SIMULATION MODE</strong>
|
||||
<span data-i18n="simulation.bannerText">Simulation is enabled. Verify the mode before relying on controller output.</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -158,7 +175,7 @@
|
||||
<div class="panel chart-panel history-toolbar-panel">
|
||||
<div class="chart-toolbar">
|
||||
<div id="historyContextControls" class="history-context-controls"></div>
|
||||
<label class="history-range-control"><span data-i18n="history.range">Range</span><select id="historyHours" aria-label="History range">
|
||||
<label class="history-range-control"><span data-i18n="history.range">Range</span><select id="historyHours" data-i18n-aria="history.range" aria-label="History range">
|
||||
<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>
|
||||
@@ -228,28 +245,26 @@
|
||||
|
||||
<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="settings-form" id="settingsForm">
|
||||
<div class="settings-mode-tabs" role="tablist" data-i18n-aria="settings.settingsSections" aria-label="Settings sections">
|
||||
<button type="button" class="active" role="tab" aria-selected="true" data-settings-tab="app" data-i18n="settings.applicationSettings">Application</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-settings-tab="gree" data-i18n="settings.greeSettings">GREE</button>
|
||||
</div>
|
||||
<form class="settings-form" id="settingsForm" novalidate>
|
||||
<div class="settings-pane" data-settings-pane="app">
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.controller">Controller</h3><p data-i18n="settings.controllerHint">Runtime and LAN discovery settings.</p></div></div>
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.applicationRuntime">Application runtime</h3><p data-i18n="settings.applicationRuntimeHint">Application-wide behavior that does not change GREE transport or discovery.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<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>
|
||||
<label class="check wide"><input type="checkbox" name="simulator_enabled"> <span data-i18n="settings.simulationMode">Simulation mode</span></label>
|
||||
<p class="field-note wide warning-note" data-i18n="settings.simulationModeHint">When enabled, a prominent warning is shown throughout the application.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="notifications.title">Notifications</h3><p data-i18n="notifications.hint">Send important events or only problems/anomalies.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<label class="check wide"><input type="checkbox" name="notifications_enabled"> <span data-i18n="notifications.enable">Enable notifications</span></label>
|
||||
<label><span data-i18n="notifications.mode">Mode</span><select name="notifications_mode"><option value="problems" data-i18n="notifications.problems">Errors and anomalies only</option><option value="important" data-i18n="notifications.important">Important events + problems</option></select></label>
|
||||
<label><span data-i18n="notifications.provider">Provider</span><select name="notifications_provider"><option value="pushover">Pushover</option><option value="slack">Slack</option><option value="discord">Discord</option></select></label>
|
||||
<div class="wide notification-provider" data-notification-provider="pushover"><label><span data-i18n="notifications.pushoverToken">Pushover application token</span><input type="password" name="pushover_app_token" autocomplete="new-password" placeholder="Leave empty to keep saved secret"></label><label><span data-i18n="notifications.pushoverUser">Pushover user/group key</span><input type="password" name="pushover_user_key" autocomplete="new-password" placeholder="Leave empty to keep saved secret"></label></div>
|
||||
<div class="wide notification-provider" data-notification-provider="pushover"><label><span data-i18n="notifications.pushoverToken">Pushover application token</span><input type="password" name="pushover_app_token" autocomplete="new-password" data-i18n-placeholder="settings.secretKeep" placeholder="Leave empty to keep saved secret"></label><label><span data-i18n="notifications.pushoverUser">Pushover user/group key</span><input type="password" name="pushover_user_key" autocomplete="new-password" data-i18n-placeholder="settings.secretKeep" placeholder="Leave empty to keep saved secret"></label></div>
|
||||
<label class="wide notification-provider" data-notification-provider="slack"><span data-i18n="notifications.slackWebhook">Slack incoming webhook</span><input type="password" name="slack_webhook_url" autocomplete="new-password" placeholder="https://hooks.slack.com/services/…"></label>
|
||||
<label class="wide notification-provider" data-notification-provider="discord"><span data-i18n="notifications.discordWebhook">Discord webhook</span><input type="password" name="discord_webhook_url" autocomplete="new-password" placeholder="https://discord.com/api/webhooks/…"></label>
|
||||
<label><span data-i18n="notifications.cooldown">Cooldown (s)</span><input type="number" name="notification_cooldown_seconds" min="30" max="86400" value="300"></label>
|
||||
@@ -272,7 +287,6 @@
|
||||
<div class="form-actions wide"><button type="button" class="secondary" id="testNotifications" data-i18n="notifications.test">Send test notification</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.metricsAndLogs">Metrics and logs</h3><p data-i18n="settings.compactionHint">SQLite keeps recent data locally and compacts older samples to the resolution used by charts.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
@@ -281,17 +295,6 @@
|
||||
<label class="check wide"><input type="checkbox" name="history_compaction_enabled"> <span data-i18n="settings.compaction">Compact old metrics</span></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.greeCommands">GREE commands</h3><p data-i18n="settings.suppressBeepHint">Only changed properties are sent. Buzzer suppression is also requested when supported by the unit firmware.</p></div></div>
|
||||
<div class="settings-grid"><label class="check wide"><input type="checkbox" name="suppress_device_beep"> <span data-i18n="settings.suppressBeep">Try to suppress command beeps</span></label></div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.greeTraffic">GREE traffic</h3><p data-i18n="settings.greeTrafficHint">Live counters of UDP frames received from configured air conditioners since controller startup.</p></div></div>
|
||||
<div id="greeFrameStats" class="gree-frame-stats"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.influx">Long-term InfluxDB history</h3><p data-i18n="settings.influxHint">Optional archive for older history. InfluxDB 1.x and 2.x are supported.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
@@ -311,7 +314,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.debug">On-screen debug</h3><p data-i18n="settings.debugHint">Live diagnostics displayed on every application page.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
@@ -319,24 +321,39 @@
|
||||
<label class="check"><input type="checkbox" name="debug_gree_frames"> <span data-i18n="settings.debugGreeFrames">Include GREE protocol frames</span></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.backup">Configuration backup</h3><p class="warning-note" data-i18n="settings.backupHint">Export/import application configuration. Exported files can contain GREE device keys plus Home Assistant and InfluxDB secrets; metrics and API access tokens are not included.</p></div></div>
|
||||
<div class="form-actions backup-actions"><button type="button" class="secondary" id="exportSettings" data-i18n="settings.export">Export settings</button><button type="button" class="secondary" id="importSettings" data-i18n="settings.import">Import settings</button><input type="file" id="importSettingsFile" accept="application/json,.json" hidden></div>
|
||||
</section>
|
||||
|
||||
<div class="panel system-panel" id="systemInfo"></div>
|
||||
</div>
|
||||
<div class="settings-pane" data-settings-pane="gree" hidden>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.greeConnection">GREE connection</h3><p data-i18n="settings.greeConnectionHint">Controller identity, polling intervals and LAN discovery transport.</p></div></div>
|
||||
<div class="settings-grid">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.greeCommands">GREE commands</h3><p data-i18n="settings.suppressBeepHint">Only changed properties are sent. Buzzer suppression is also requested when supported by the unit firmware.</p></div></div>
|
||||
<div class="settings-grid"><label class="check wide"><input type="checkbox" name="suppress_device_beep"> <span data-i18n="settings.suppressBeep">Try to suppress command beeps</span></label></div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head"><div><h3 data-i18n="settings.greeTraffic">GREE traffic</h3><p data-i18n="settings.greeTrafficHint">Live counters of UDP frames received from configured air conditioners since controller startup.</p></div></div>
|
||||
<div id="greeFrameStats" class="gree-frame-stats"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="settings-save-bar"><span data-i18n="settings.saveHint">Save changes made in the blocks above.</span><button class="primary" type="submit" data-i18n="actions.save">Save</button></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 logs-toolbar"><div><strong data-i18n="logs.retention">Log retention</strong><small data-i18n="logs.retentionHint">Old event rows are removed automatically during maintenance.</small></div><label><span data-i18n="logs.level">Level</span><select id="logLevelFilter"><option value="all" data-i18n="logs.all">All</option><option value="error" data-i18n="logs.errors">Errors</option><option value="warn" data-i18n="logs.warnings">Warnings</option><option value="info">Info</option></select></label><label><span data-i18n="logs.category">Category</span><select id="logCategoryFilter"><option value="all" data-i18n="logs.all">All</option><option value="device" data-i18n="nav.devices">Devices</option><option value="zone" data-i18n="nav.zones">Thermostats</option><option value="automation" data-i18n="nav.automations">Automations</option><option value="settings" data-i18n="nav.settings">Settings</option><option value="integration" data-i18n="logs.integrations">Integrations</option><option value="system" data-i18n="logs.systemOther">System/other</option></select></label><label><span data-i18n="logs.keepFor">Keep for</span><select id="logRetentionDays"><option value="7">7 days</option><option value="14">14 days</option><option value="30">30 days</option><option value="90">90 days</option><option value="180">180 days</option><option value="365">365 days</option></select></label><button class="secondary" id="saveLogRetention" data-i18n="actions.save">Save</button></div>
|
||||
<div class="panel logs-toolbar"><div><strong data-i18n="logs.retention">Log retention</strong><small data-i18n="logs.retentionHint">Old event rows are removed automatically during maintenance.</small></div><label><span data-i18n="logs.level">Level</span><select id="logLevelFilter"><option value="all" data-i18n="logs.all">All</option><option value="error" data-i18n="logs.errors">Errors</option><option value="warn" data-i18n="logs.warnings">Warnings</option><option value="info" data-i18n="logs.info">Info</option></select></label><label><span data-i18n="logs.category">Category</span><select id="logCategoryFilter"><option value="all" data-i18n="logs.all">All</option><option value="device" data-i18n="nav.devices">Devices</option><option value="zone" data-i18n="nav.zones">Thermostats</option><option value="automation" data-i18n="nav.automations">Automations</option><option value="settings" data-i18n="nav.settings">Settings</option><option value="integration" data-i18n="logs.integrations">Integrations</option><option value="system" data-i18n="logs.systemOther">System/other</option></select></label><label><span data-i18n="logs.keepFor">Keep for</span><select id="logRetentionDays"><option value="7" data-i18n="logs.days7">7 days</option><option value="14" data-i18n="logs.days14">14 days</option><option value="30" data-i18n="logs.days30">30 days</option><option value="90" data-i18n="logs.days90">90 days</option><option value="180" data-i18n="logs.days180">180 days</option><option value="365" data-i18n="logs.days365">365 days</option></select></label><button class="secondary" id="saveLogRetention" data-i18n="actions.save">Save</button></div>
|
||||
<div class="panel log-list" id="logList"></div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -557,7 +574,17 @@
|
||||
</dialog>
|
||||
|
||||
<aside id="debugOverlay" class="debug-overlay" hidden>
|
||||
<div class="debug-overlay-head"><div><strong data-i18n="debug.title">Live debug</strong><small id="debugOverlayStatus"></small></div><button type="button" data-action="debug-clear" data-i18n="debug.clear">Clear</button></div>
|
||||
<div class="debug-overlay-head">
|
||||
<div class="debug-overlay-title"><strong data-i18n="debug.title">Live debug</strong><small id="debugOverlayStatus"></small></div>
|
||||
<div class="debug-overlay-actions">
|
||||
<div class="debug-source-tabs" role="tablist" data-i18n-aria="debug.filterAria" aria-label="Debug source">
|
||||
<button type="button" class="active" role="tab" aria-selected="true" data-debug-filter="all" data-i18n="debug.all">All</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="requests" data-i18n="debug.requests">Requests</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-debug-filter="gree" data-i18n="debug.gree">GREE</button>
|
||||
</div>
|
||||
<button type="button" class="debug-clear" data-action="debug-clear" data-i18n="debug.clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="debugOverlayLines" class="debug-overlay-lines"></div>
|
||||
</aside>
|
||||
<div id="toastStack" class="toast-stack" role="status" aria-live="polite" aria-atomic="false"></div>
|
||||
|
||||
+343
-12
@@ -203,8 +203,21 @@ input:focus, select:focus { border-color: var(--accent); outline: 2px solid colo
|
||||
.influx-fields[hidden] { display: none; }
|
||||
.influx-fields label:last-child:nth-child(odd) { grid-column: 1/-1; }
|
||||
.backup-actions { align-items: center; }
|
||||
.system-panel { margin-top: 14px; color: var(--muted); line-height: 1.7; }
|
||||
.system-panel strong { color: var(--text); }
|
||||
.system-panel { display:grid; gap:16px; margin-top:14px; padding:18px; }
|
||||
.system-panel-head { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
|
||||
.system-panel-head h3 { margin:4px 0 5px; font-size:20px; }
|
||||
.system-panel-head p { max-width:700px; margin:0; color:var(--muted); font-size:12px; line-height:1.5; }
|
||||
.system-health { display:inline-flex; align-items:center; min-height:30px; padding:6px 10px; border:1px solid var(--line); border-radius:999px; color:var(--muted); background:var(--surface-muted); font-size:10px; font-weight:800; white-space:nowrap; }
|
||||
.system-health.ok { border-color:var(--accent-border); color:var(--accent); background:var(--accent-soft); }
|
||||
.system-health.warn { border-color:color-mix(in srgb,var(--warning) 35%,var(--line)); color:var(--warning); background:var(--warning-soft); }
|
||||
.system-status-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(145px,1fr)); gap:9px; }
|
||||
.system-status-item { min-width:0; padding:12px; border:1px solid var(--line); border-radius:14px; background:var(--surface-muted); }
|
||||
.system-status-item small,.system-status-item strong { display:block; }
|
||||
.system-status-item small { margin-bottom:5px; color:var(--muted); font-size:10px; }
|
||||
.system-status-item strong { overflow:hidden; color:var(--text); font-size:13px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.system-status-item.ok { border-color:color-mix(in srgb,var(--accent) 25%,var(--line)); }
|
||||
.system-status-item.warn { border-color:color-mix(in srgb,var(--warning) 28%,var(--line)); background:color-mix(in srgb,var(--warning-soft) 52%,var(--surface-muted)); }
|
||||
.system-status-item.mono strong { font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; font-size:11px; }
|
||||
.gree-frame-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:10px; }
|
||||
.gree-frame-stat { display:flex; align-items:center; justify-content:space-between; gap:12px; min-width:0; padding:12px 14px; border:1px solid var(--line); border-radius:14px; background:var(--surface-2); }
|
||||
.gree-frame-stat small { overflow:hidden; color:var(--muted); text-overflow:ellipsis; white-space:nowrap; }
|
||||
@@ -464,22 +477,38 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
|
||||
|
||||
.debug-overlay { position: fixed; z-index: 110; right: 18px; bottom: 92px; width: min(680px, calc(100vw - 36px)); max-height: min(46vh, 440px); overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line)); border-radius: 18px; background: color-mix(in srgb, var(--surface) 96%, transparent); box-shadow: 0 18px 60px rgba(0,0,0,.22); backdrop-filter: blur(18px); }
|
||||
.debug-overlay[hidden] { display: none; }
|
||||
.debug-overlay-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
|
||||
.debug-overlay-head > div { display: grid; gap: 2px; }
|
||||
.debug-overlay-head strong { font-size: 12px; }
|
||||
.debug-overlay-head small { color: var(--muted); font-size: 10px; }
|
||||
.debug-overlay-head button { padding: 6px 9px; color: var(--muted); background: var(--surface-muted); font-size: 10px; }
|
||||
.debug-overlay-lines { max-height: min(38vh, 360px); overflow: auto; padding: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
||||
.debug-line { display: grid; grid-template-columns: 70px 46px 1fr; gap: 8px; padding: 5px 6px; border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent); font-size: 10px; line-height: 1.35; }
|
||||
.debug-line time, .debug-line b { color: var(--muted); font-weight: 600; }
|
||||
.debug-line span { overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
.debug-overlay-head { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 12px; border-bottom:1px solid var(--line); }
|
||||
.debug-overlay-title { display:grid; flex:0 1 auto; gap:2px; min-width:90px; }
|
||||
.debug-overlay-head strong { font-size:12px; }
|
||||
.debug-overlay-head small { color:var(--muted); font-size:10px; }
|
||||
.debug-overlay-actions { display:flex; align-items:center; justify-content:flex-end; gap:7px; min-width:0; }
|
||||
.debug-source-tabs { display:flex; align-items:center; gap:3px; padding:3px; border:1px solid var(--line); border-radius:10px; background:var(--surface-muted); }
|
||||
.debug-source-tabs button,.debug-overlay-head .debug-clear { min-height:28px; padding:5px 8px; border-radius:7px; color:var(--muted); background:transparent; font-size:9px; font-weight:750; }
|
||||
.debug-source-tabs button.active { color:var(--accent); background:var(--accent-soft); }
|
||||
.debug-overlay-head .debug-clear { border:1px solid var(--line); background:var(--surface-muted); }
|
||||
.debug-overlay-lines { max-height:min(38vh,360px); overflow:auto; padding:7px; font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; }
|
||||
.debug-line { display:grid; grid-template-columns:70px 48px 1fr; gap:8px; margin:3px 0; padding:7px 8px; border:1px solid var(--line); border-radius:9px; background:color-mix(in srgb,var(--surface-muted) 76%,transparent); font-size:10px; line-height:1.35; }
|
||||
.debug-line.source-gree { border-color:color-mix(in srgb,var(--accent) 25%,var(--line)); }
|
||||
.debug-line.source-request { border-color:color-mix(in srgb,var(--info) 24%,var(--line)); }
|
||||
.debug-line time,.debug-line b { color:var(--muted); font-weight:650; }
|
||||
.debug-line.source-gree b { color:var(--accent); }
|
||||
.debug-line.source-request b { color:var(--info); }
|
||||
.debug-line span { overflow-wrap:anywhere; white-space:pre-wrap; }
|
||||
.debug-empty { padding: 18px; color: var(--muted); text-align: center; font-size: 11px; }
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.custom-chart-add, .custom-chart-save { align-items: stretch; flex-direction: column; }
|
||||
.toast-stack { right: 12px; bottom: 88px; width: calc(100vw - 24px); }
|
||||
.debug-overlay { right: 12px; bottom: 88px; width: calc(100vw - 24px); }
|
||||
.debug-line { grid-template-columns: 58px 38px 1fr; }
|
||||
.debug-overlay-head { align-items:stretch; flex-direction:column; }
|
||||
.debug-overlay-actions { justify-content:space-between; }
|
||||
.debug-source-tabs { flex:1 1 auto; }
|
||||
.debug-source-tabs button { flex:1 1 auto; }
|
||||
.debug-line { grid-template-columns: 58px 42px 1fr; }
|
||||
.system-panel-head { align-items:stretch; flex-direction:column; }
|
||||
.system-health { align-self:flex-start; }
|
||||
.simulation-mode-banner { align-items:stretch; flex-direction:column; gap:3px; }
|
||||
.simulation-mode-banner strong { white-space:normal; }
|
||||
.influx-fields { grid-template-columns: 1fr; }
|
||||
.influx-fields label:last-child:nth-child(odd) { grid-column: auto; }
|
||||
}
|
||||
@@ -549,6 +578,11 @@ html[data-theme='light'] .simulation-metrics > div, html[data-theme='light'] .si
|
||||
.history-refresh-button { min-height:45px; align-self:end; }
|
||||
|
||||
.settings-form { display:grid; gap:14px; }
|
||||
.settings-mode-tabs { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; margin:0 0 14px; padding:6px; border:1px solid var(--line); border-radius:17px; background:var(--surface); }
|
||||
.settings-mode-tabs button { min-height:42px; border:1px solid transparent; border-radius:12px; color:var(--muted); background:transparent; font-size:12px; font-weight:800; }
|
||||
.settings-mode-tabs button.active { border-color:var(--accent-border); color:var(--accent); background:var(--accent-soft); }
|
||||
.settings-pane { display:grid; gap:14px; }
|
||||
.settings-pane[hidden] { display:none; }
|
||||
.settings-block { display:grid; gap:18px; }
|
||||
.settings-block-head { display:flex; align-items:flex-start; gap:13px; }
|
||||
.settings-block-head h3 { margin:1px 0 4px; font-size:18px; }
|
||||
@@ -971,3 +1005,300 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
|
||||
}
|
||||
|
||||
/* 0.7.1+: quick thermostats and quick manual control use the shared quick-control classes above. */
|
||||
|
||||
/* 0.8.11 mobile toolbar and chart inspection UX */
|
||||
.topbar { grid-template-columns: minmax(0, 1fr) auto auto; column-gap: 10px; }
|
||||
.top-actions { min-width: 0; }
|
||||
.language-picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
transition: border-color .15s ease, background .15s ease, transform .15s ease;
|
||||
}
|
||||
.language-picker:hover { background: var(--surface-2); }
|
||||
.language-picker:active { transform: scale(.97); }
|
||||
.language-label { font-size: 11px; font-weight: 850; letter-spacing: .06em; line-height: 1; }
|
||||
.language-chevron { margin-top: -2px; color: var(--muted); font-size: 13px; line-height: 1; }
|
||||
.language-select {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolbar-icon-control {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
.toolbar-icon-control:hover { background: var(--surface-2); }
|
||||
#refreshButton.toolbar-icon-control { font-size: 21px; }
|
||||
button:focus-visible,
|
||||
.language-picker:focus-within {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chart-hover-line {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 1px;
|
||||
background: color-mix(in srgb, var(--text) 40%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.chart-tooltip {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
min-width: 175px;
|
||||
max-width: min(290px, calc(100vw - 32px));
|
||||
padding: 10px 11px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--surface) 96%, transparent);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, .2);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.chart-tooltip-time { display: block; margin-bottom: 7px; color: var(--text-soft); font-weight: 800; font-variant-numeric: tabular-nums; }
|
||||
.chart-tooltip-values { display: grid; gap: 5px; }
|
||||
.chart-tooltip-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px; }
|
||||
.chart-tooltip-label { display: inline-flex; min-width: 0; align-items: center; gap: 6px; color: var(--muted); }
|
||||
.chart-tooltip-label i { flex: 0 0 7px; width: 7px; height: 7px; border-radius: 50%; background: var(--tooltip-color); }
|
||||
.chart-tooltip-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chart-tooltip-value { color: var(--text); font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.chart-tooltip-sample-time { color: var(--muted); font-size: 9px; font-weight: 600; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.topbar { min-height: 64px; gap: 6px; padding: 9px 10px; }
|
||||
.brand strong { font-size: 13px; }
|
||||
.toolbar-runtime-status { padding: 6px 8px; font-size: 10px; }
|
||||
.top-actions { gap: 5px; }
|
||||
.language-picker { width: 50px; height: 38px; border-radius: 10px; }
|
||||
.toolbar-icon-control { flex-basis: 38px; width: 38px; height: 38px; min-height: 38px; border-radius: 10px; }
|
||||
.language-chevron { font-size: 11px; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.topbar { grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; }
|
||||
.brand { grid-column: 1; grid-row: 1; }
|
||||
.top-actions { grid-column: 2; grid-row: 1; }
|
||||
.toolbar-runtime-status {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
justify-self: stretch;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.chart-tooltip-sample-time { display:block; margin-top:1px; }
|
||||
@media (max-width: 700px) { .top-actions { width: auto; } }
|
||||
.chart-hover-hint { display:block; margin-top:5px; color:var(--muted); font-size:10px; }
|
||||
.chart-tooltip-value { text-align:right; }
|
||||
.chart-wrap canvas:focus-visible { outline:2px solid color-mix(in srgb, var(--accent) 55%, transparent); outline-offset:-2px; }
|
||||
.toolbar-runtime-status { min-width:max-content; }
|
||||
|
||||
/* 0.8.13 settings separation, simulation warning, system status and debug filters */
|
||||
.toolbar-picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
transition: border-color .15s ease, background .15s ease, transform .15s ease;
|
||||
}
|
||||
.toolbar-picker:hover { background: var(--surface-2); }
|
||||
.toolbar-picker:active { transform: scale(.97); }
|
||||
.toolbar-picker:focus-within {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.toolbar-picker-value {
|
||||
min-width: 18px;
|
||||
color: var(--text-soft);
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
letter-spacing: .06em;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.theme-picker-value { font-size: 16px; letter-spacing: 0; }
|
||||
.toolbar-picker-chevron { margin-top: -2px; color: var(--muted); font-size: 12px; line-height: 1; }
|
||||
.toolbar-picker-select {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.connection-banner {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid color-mix(in srgb, var(--danger) 34%, var(--line));
|
||||
border-radius: 13px;
|
||||
background: color-mix(in srgb, var(--danger-soft) 78%, var(--surface));
|
||||
}
|
||||
.connection-banner[hidden] { display: none; }
|
||||
.simulation-mode-banner { grid-column:1 / -1; display:flex; align-items:center; justify-content:center; gap:14px; width:100%; padding:13px 16px; border:2px solid color-mix(in srgb,var(--warning) 62%,var(--line)); border-radius:14px; background:color-mix(in srgb,var(--warning-soft) 90%,var(--surface)); text-align:center; box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--warning) 10%,transparent),0 5px 18px color-mix(in srgb,var(--warning) 10%,transparent); }
|
||||
.simulation-mode-banner[hidden] { display:none; }
|
||||
.simulation-mode-banner strong { color:var(--warning); font-size:clamp(18px,2.6vw,26px); font-weight:950; letter-spacing:.09em; line-height:1; white-space:nowrap; }
|
||||
.simulation-mode-banner span { max-width:720px; color:var(--text-soft); font-size:12px; font-weight:700; line-height:1.35; }
|
||||
.connection-banner-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 13%, transparent); }
|
||||
.connection-banner > div { min-width: 0; }
|
||||
.connection-banner strong, .connection-banner span { display: block; }
|
||||
.connection-banner strong { margin-bottom: 1px; font-size: 11px; }
|
||||
.connection-banner > div span { overflow: hidden; color: var(--muted); font-size: 10px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.connection-banner button { min-height: 32px; padding: 6px 10px; border-radius: 9px; font-size: 10px; }
|
||||
body.connection-lost .connection-dot { box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 13%, transparent); }
|
||||
|
||||
.field-error {
|
||||
display: block;
|
||||
margin-top: -2px;
|
||||
color: var(--danger);
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.has-error > input,
|
||||
.has-error > select,
|
||||
.has-error > textarea,
|
||||
input[aria-invalid="true"],
|
||||
select[aria-invalid="true"],
|
||||
textarea[aria-invalid="true"] {
|
||||
border-color: color-mix(in srgb, var(--danger) 68%, var(--line));
|
||||
outline: 2px solid color-mix(in srgb, var(--danger) 13%, transparent);
|
||||
}
|
||||
.form-error-summary {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--danger) 36%, var(--line));
|
||||
border-radius: 12px;
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.settings-form > .form-error-summary { margin: 0 0 12px; }
|
||||
.dialog-form > .form-error-summary { margin-top: -2px; }
|
||||
.settings-form.is-saving,
|
||||
.dialog-form.is-saving { cursor: progress; }
|
||||
.button-spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 7px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
vertical-align: -2px;
|
||||
animation: button-spin .7s linear infinite;
|
||||
}
|
||||
@keyframes button-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.chart-title-row { align-items: flex-start; }
|
||||
.chart-zoom-controls {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 32px minmax(52px, auto) 32px;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
.chart-zoom-controls button {
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
border-radius: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
.chart-zoom-controls button:last-child { border-right: 0; }
|
||||
.chart-zoom-controls button:hover:not(:disabled) { background: var(--surface-2); }
|
||||
.chart-zoom-controls button:disabled { cursor: default; opacity: .34; }
|
||||
.chart-zoom-reset { color: var(--muted) !important; font-size: 10px !important; font-variant-numeric: tabular-nums; }
|
||||
.chart-wrap { overscroll-behavior-inline: contain; scroll-behavior: smooth; }
|
||||
.chart-wrap canvas { touch-action: pan-x pan-y; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar-picker { flex-basis: 38px; width: 38px; height: 38px; border-radius: 10px; gap: 2px; }
|
||||
.toolbar-picker-value { min-width: 15px; font-size: 10px; }
|
||||
.theme-picker-value { font-size: 14px; }
|
||||
.toolbar-picker-chevron { font-size: 9px; }
|
||||
.connection-banner { grid-template-columns: auto minmax(0, 1fr) auto; padding: 8px 9px; }
|
||||
.connection-banner > div span { white-space: normal; }
|
||||
.chart-title-row { align-items: stretch; flex-direction: column; }
|
||||
.chart-zoom-controls { align-self: flex-end; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.connection-banner { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.topbar {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
.brand { grid-column: 1; grid-row: 1; }
|
||||
.top-actions { grid-column: 2; grid-row: 1; }
|
||||
.toolbar-runtime-status {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
justify-self: stretch;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.connection-banner { grid-column: 1 / -1; grid-row: auto; }
|
||||
}
|
||||
.settings-form.has-unsaved-changes .settings-save-bar {
|
||||
border-color: color-mix(in srgb, var(--accent) 48%, var(--line-strong));
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,.18), 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const CACHE = 'gree-controller-v0810-dashboard-alert-filters';
|
||||
const CACHE = 'gree-controller-v0813-settings-debug-docs';
|
||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json'), path('/lang/pl.json')];
|
||||
self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)).then(() => self.skipWaiting())));
|
||||
self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim())));
|
||||
self.addEventListener('fetch', event => {
|
||||
|
||||
Reference in New Issue
Block a user