-
-
Notifications
You must be signed in to change notification settings - Fork 2
uk Climate And Gree
Локальне керування Wi-Fi кондиціонерами на протоколі Gree (Pular GWH12AGB-I-R32, Gree, Cooper&Hunter, EWT, родина Ewpe Smart) плюс окремий високорівневий модуль Climate з картками кліматичних пристроїв, згрупованими по кімнатах.
Без хмарного акаунта, без залежностей від Home Assistant, без httpx між системними модулями — лише прямі Python-виклики в межах одного процесу та EventBus.
➡ Також див.: provider-system-and-modules.md — пост-Gree рефакторинг, який перетворив device-control на runtime-плаговану систему провайдерів, додав SYSTEM-модуль
lights-switches, об'єднав налаштування energy-monitor в одну фільтровану таблицю та переробив віджет climate під компактні плитки дашборду. Драйвер Gree і модуль Climate, описані нижче, залишаються канонічною реалізацією; цей документ описує їхній початковий дизайн.
Фіча додає два шари:
| Шар | Призначення | Модуль |
|---|---|---|
| Драйвер | Спілкування з кондиціонером по протоколу Gree (UDP/7000, AES-ECB) |
device-control (новий драйвер gree) |
| UI-модуль | Відображення всіх кліматичних пристроїв по кімнатах та керування ними | Новий SYSTEM-модуль climate
|
Шари свідомо роз'єднані:
-
device-control володіє голосовими інтентами та реєстром пристроїв. Нові інтенти для кондиціонера (
device.set_temperature,device.set_mode,device.set_fan_speed) живуть поряд з існуючимиdevice.on/device.off. Усі вони використовують один резолвер, який звужує вибір через фільтрentity_type, тож команда «встанови температуру» не може випадково потрапити на лампу. -
Climate-модуль — лише презентація. Він не володіє жодним голосовим інтентом, не опитує пристрій, не говорить по HTTP. Він підписується на
device.state_changedдля свіжості кешу та передає дії користувача вDeviceControlModule.execute_command()прямим викликом Python (між-модульний виклик у межах одного процесуselena-coreдозволений). -
Енергоспоживання — це задача
energy_monitor, не клімату. Драйвер Gree свідомо не реалізуєconsume_metering().
┌─────────────────────────┐
│ selena-core (один │
│ процес Python) │
│ │
Голосовий інтент ─► │ device-control │
device.set_ │ ├─ _on_voice_intent │
temperature/mode │ ├─ _resolve_device │ ─┐
│ │ (entity_filter) │ │
│ ├─ execute_command │ │
│ └─ _watch_device │ │
│ │ │ │
│ ▼ │ │
│ GreeDriver (gree.py) │ │
│ │ greeclimate │ │
│ ▼ UDP/7000 AES │ │
│ ┌─────────────┐ │ │
│ │ Pular AC │ │ │
│ └─────────────┘ │ │
│ │ │
│ climate module │ │
│ ├─ widget /rooms │ │
│ ├─ apply_command() ───┘
│ │ (in-process call) │
│ └─ _on_state_event │ ◄─ device.state_changed
│ (кеш) │ на EventBus
└─────────────────────────┘
Ключові інваріанти:
-
Один екземпляр драйвера на пристрій, одна корутина-вотчер на пристрій. Реконнект з експоненційним бек-офом при
DriverError. -
Драйвер мутує
self.metaна місці (Gree вивчає AES-ключ під часbind());_persist_driver_meta()записує дифф у БД після кожногоconnect(), тож після рестарту повторний бінд не потрібен. -
Між-модульний виклик через
get_sandbox().get_in_process_module("device-control")— без HTTP, без httpx, без портів. Climate-модуль кешує посилання наdevice-controlліниво. -
Climate-модуль ніколи не володіє голосовими інтентами. Уся голосова маршрутизація централізована в
device-control._on_voice_intent.
| Шлях | Призначення |
|---|---|
| system_modules/device_control/drivers/gree.py |
GreeDriver(DeviceDriver) — async обгортка над greeclimate
|
| system_modules/climate/init.py | Експортує module_class = ClimateModule
|
| system_modules/climate/manifest.json | Маніфест SYSTEM-модуля, без порту |
| system_modules/climate/module.py | ClimateModule(SystemModule) |
| system_modules/climate/routes.py |
/devices, /rooms, /device/{id}/command
|
| system_modules/climate/widget.html | 2x2 сітка карток A/C по кімнатах |
| system_modules/climate/settings.html | Read-only діагностична таблиця |
| system_modules/climate/icon.svg | Іконка модуля |
| tests/test_gree_driver.py | 15 unit-тестів для маперів |
| Шлях | Зміна |
|---|---|
| requirements.txt | greeclimate>=2.1 |
| system_modules/device_control/drivers/registry.py | Реєстрація "gree": GreeDriver; запис у list_driver_types()
|
| system_modules/device_control/routes.py |
POST /gree/discover, POST /gree/import; дозвіл gree у add_device
|
| system_modules/device_control/settings.html | Нова вкладка «Gree / Pular» зі Scan + Import; air_conditioner як entity_type; повний EN/UK i18n |
| system_modules/device_control/module.py | Climate-інтенти декларовані у _OWNED_INTENT_META, _intent_to_state(), _resolve_device(entity_filter=) з composite tier-0 дисамбігуацією, _persist_driver_meta(), _claim_intent_ownership() (вставляє/claims рядки на кожному старті) |
| system_modules/llm_engine/pattern_generator.py |
rebuild_composite_device_patterns() створює composite device.set_temperature regex з (?P<name>...) alternation усіх кліматичних пристроїв |
Драйвер перекладає між логічним dict-ом SelenaCore (зберігається в Device.state як JSON) і об'єктом greeclimate.device.Device.
| Ключ | Тип | Діапазон | Опис |
|---|---|---|---|
on |
bool | — | Живлення |
mode |
str |
auto / cool / dry / fan / heat
|
Режим роботи |
target_temp |
int | 16–30 | Цільова температура °C (з обмеженням) |
current_temp |
int | — | Поточна температура (read-only) |
fan_speed |
str |
auto / low / medium_low / medium / medium_high / high
|
Швидкість вентилятора |
swing_v |
str |
off / full / fixed_top / fixed_middle_top / fixed_middle / fixed_middle_bottom / fixed_bottom / swing_bottom / swing_middle / swing_top
|
Вертикальні жалюзі |
swing_h |
str |
off / full / left / left_center / center / right_center / right
|
Горизонтальні жалюзі |
sleep |
bool | — | Нічний режим |
turbo |
bool | — | Турбо-режим |
light |
bool | — | Підсвітка дисплея |
eco |
bool | — | Steady-heat / еко |
health |
bool | — | Анти-іон / здоров'я |
quiet |
bool | — | Тихий режим |
{
"gree": {
"ip": "192.168.1.50",
"mac": "aa:bb:cc:dd:ee:ff",
"name": "Кондиціонер у спальні",
"port": 7000,
"key": null,
"brand": "gree",
"model": "GWH12AGB"
}
}key — null до першого успішного bind(). Драйвер записує отриманий AES-ключ у це поле, а DeviceControlModule._persist_driver_meta() зливає його в БД, щоб після перезавантаження не довелося повторювати рукостискання.
| Метод | Поведінка |
|---|---|
connect() |
Створює Device(DeviceInfo(ip, port, mac, name)), await bind(key=...), зберігає новий device_key, update_state(), повертає логічний стан. Будь-який виняток обгортається у DriverError. |
set_state(state) |
Під asyncio.Lock перекладає логічні ключі на атрибути greeclimate, викликає push_state_update(). Обмежує target_temp діапазоном 16–30 °C. Кидає DriverError на невідомий mode/fan/swing. |
get_state() |
Lock + update_state() + _to_logical(). |
stream_events() |
Gree-пристрої не пушать події. Цикл з POLL_INTERVAL_SECONDS = 5, віддає лише коли стан реально змінився (дифф з _last_state). Збої мережі піднімають DriverError, що тригерить реконнект вотчера. |
disconnect() |
Ідемпотентний — занулює _device (greeclimate не тримає постійних сокетів). |
consume_metering() |
Не перевизначений — енергія належить energy_monitor. |
# system_modules/device_control/drivers/registry.py
DRIVERS = {
"tuya_local": TuyaLocalDriver,
"tuya_cloud": TuyaCloudDriver,
"mqtt": MqttBridgeDriver,
"gree": GreeDriver, # ← новий
}# list_driver_types() — для випадаючого списку «Add device»
{
"id": "gree",
"name": "Gree / Pular WiFi A/C",
"needs_cloud": False,
"fields": ["gree.ip", "gree.mac", "gree.name"],
}| Метод | Шлях | Тіло | Повертає |
|---|---|---|---|
POST |
/api/ui/modules/device-control/gree/discover |
{"timeout": 10} (необов'язково) |
{"devices": [{ip, mac, name, brand, model, version}, ...]} |
POST |
/api/ui/modules/device-control/gree/import |
{"devices": [{ip, mac, name, location}, ...]} |
{"created": [...], "skipped": [...]} |
/gree/discover запускає greeclimate.Discovery().scan(timeout=10) (з резервним викликом для 1.x API). Best-effort: повертає порожній список, якщо greeclimate не встановлено або скан кинув виняток.
/gree/import створює рядки Device з:
protocol = "gree"entity_type = "air_conditioner"-
capabilities = AC_CAPABILITIES(["on","off","set_temperature","set_mode","set_fan_speed","set_swing"]) enabled = Truemeta.gree = {ip, mac, name, port:7000, key:null, brand:"gree"}
Після вставки рядка викликається add_device_watcher(), який виконує перший connect() (і узгоджує ключ). Новий ключ зливається в БД через _persist_driver_meta() на тому самому шляху.
device-control/settings.html має третю вкладку «Gree / Pular» поряд з Devices та Tuya Cloud Wizard:
- Натиснути Сканувати →
POST /gree/discover→ індикатор протягом 10 секунд. - Таблиця результатів показує IP, MAC, виробник/модель. У кожному рядку — чекбокс Імпорт (за замовчуванням увімкнений), редаговане поле Назва та Кімната.
- Натиснути Імпортувати вибрані →
POST /gree/import→ toast підтвердження → перехід на вкладку Devices з новими записами.
Усі рядки UI мають повний переклад EN/UK у словнику var L = {en:{}, uk:{}}.
Існуючий POST /devices теж працює — protocol="gree", entity_type="air_conditioner", meta={"gree": {"ip": ..., "mac": ..., "name": ...}}. Вотчер виконає bind при першому підключенні.
{
"name": "climate",
"type": "SYSTEM",
"runtime_mode": "always_on",
"permissions": ["device.read", "device.write", "events.subscribe", "events.publish"],
"ui": {
"icon": "icon.svg",
"widget": {"file": "widget.html", "size": "2x2"},
"settings": "settings.html"
}
}Без port. SYSTEM-модулі живуть у процесі selena-core.
# system_modules/climate/module.py
from core.module_loader.sandbox import get_sandbox
self._dc = get_sandbox().get_in_process_module("device-control")
await self._dc.execute_command(device_id, state)Посилання кешується ліниво після першого пошуку. Якщо device-control ще не завантажено (race на старті), apply_command() кидає RuntimeError, а маршрут повертає 503.
Підключений на /api/ui/modules/climate/:
| Метод | Шлях | Повертає |
|---|---|---|
GET |
/health |
{status, module, cached_devices} |
GET |
/devices |
Плоский список усіх пристроїв з entity_type ∈ {air_conditioner, thermostat}
|
GET |
/rooms |
Ті самі дані, згруповані за location (порожня кімната → бакет unassigned) |
GET |
/device/{id} |
Деталі одного пристрою |
POST |
/device/{id}/command |
Тіло {state: {...}}. Перевіряє дозволені ключі (ALLOWED_STATE_KEYS) і викликає DeviceControlModule.execute_command(). |
Дозволені ключі state: on, mode, target_temp, fan_speed, swing_v, swing_h, sleep, turbo, light, eco, health, quiet.
Модуль підписаний на device.state_changed і кешує payload["new_state"] у self._latest[device_id]. list_climate_devices() зливає стан з БД та кешований дельта-апдейт, тому віджет читає за O(1) після першого завантаження.
widget.html — це 2x2 плитка дашборду:
- Пристрої згруповані по кімнатах (location).
- Кожна картка показує: назву пристрою, кнопку живлення, поточну температуру, цільову температуру з кнопками
+/−(обмеження 16–30), чипи режиму (auto/cool/dry/fan/heat), чипи швидкості вентилятора (auto/low/medium/high). - Опитує
GET /roomsкожні 10 с + приwindow.focus. - Реагує на глобальний postMessage
lang_changedповним перерендером. - Повна локалізація EN/UK через
var L = {en:{}, uk:{}}.
settings.html навмисно мінімалістичний — лише read-only діагностична таблиця з кімнатою, назвою, типом, протоколом, бейджем on/off і сирим JSON стану. Без випадаючого списку «джерело клімату»: усі кліматичні пристрої автоматично з'являються за entity_type.
Голосові інтенти живуть у device-control, не в climate. Це усуває будь-яке перетинання патернів зі світлом/розетками.
| Інтент | Параметри | Приклад (EN) | Приклад (UK) |
|---|---|---|---|
device.set_temperature |
level: int, location?: str
|
"set temperature to 22 in bedroom" | "встанови температуру на 22 в спальні" |
device.set_mode |
mode: enum(auto,cool,dry,fan,heat), location?: str
|
"switch bedroom to cool mode" | "перемкни спальню в режим охолодження" |
device.set_fan_speed |
level: enum(auto,low,medium,high,min,max,...), location?: str
|
"set fan to high in bedroom" | "встанови вентилятор на високу в спальні" |
Аліаси, які обробляє парсер: min/minimum → low, max/maximum → high, mid/middle → medium, cooling → cool, heating → heat.
Патерни більше не сидяться зовнішнім скриптом. device-control декларує ці інтенти у _OWNED_INTENT_META і вставляє/claims рядки intent_definitions при кожному start() через _claim_intent_ownership(). Composite FastMatcher-патерни (один regex на дієслово пристрою з (?P<name>...) alternation усіх meta.name_en кліматичних пристроїв) перебудовуються PatternGenerator.rebuild_composite_device_patterns() при кожному device CRUD. Повний дизайн — у intent-routing.md §2.
DeviceControlModule._resolve_device(params, entity_filter=...) обирає рівно один цільовий пристрій. Резолвер використовує кілька рівнів по порядку:
-
Composite fast path — якщо FastMatcher захопив унікальний
name_enдля однозначного пристрою, резолвер завантажує його напряму заdevice_id. -
Tier 0 дисамбігуація — якщо FastMatcher захопив
name_en, який ділять 2+ пристрої (одне ім'я в різних кімнатах), резолвер матчитьmeta.name_en AND locationодночасно. - Strict (entity_type AND location)
-
Тільки location (
locationзбігається зdevice.location,device.name,meta.name_en, абоmeta.location_en) - Тільки entity
- Single-device fallback (коли під управлінням рівно один пристрій)
Climate-інтенти передають entity_filter=("air_conditioner","thermostat") (або ("air_conditioner","fan") для device.set_fan_speed), щоб резолвер звузив набір кандидатів ще до tier-матчингу. Саме це гарантує, що «встанови температуру на 22» не може випадково потрапити на лампу або розетку.
DeviceControlModule._claim_intent_ownership() оновлює всі рядки в intent_definitions, що перелічені в OWNED_INTENTS (device.on, device.off, device.set_temperature, device.set_mode, device.set_fan_speed, device.query_temperature, device.lock, device.unlock), виставляючи module="device-control". Потім вставляє відсутні рядки, використовуючи defaults з _OWNED_INTENT_META. Ідемпотентний — виконується при кожному старті модуля, зовнішній seed-скрипт не потрібен.
Драйвери, які отримують облікові дані під час connect() (AES-ключ Gree), мутують self.meta на місці. Цикл вотчера викликає _persist_driver_meta(device_id, drv) одразу після кожного успішного connect():
async def _persist_driver_meta(self, device_id: str, drv: Any) -> None:
new_json = json.dumps(drv.meta, sort_keys=True)
async with self._db_session() as session:
async with session.begin():
d = await session.get(Device, device_id)
current_json = json.dumps(json.loads(d.meta) if d.meta else {}, sort_keys=True)
if current_json == new_json:
return # no-op якщо нічого не змінилося
d.set_meta(drv.meta)Перевірка дифу запобігає зайвим записам у БД на кожному циклі реконнекту.
pytest tests/test_gree_driver.py -v15 тестів покривають: список можливостей, обмеження температури (_clamp_temp), двосторонній round-trip enum-мап, _to_logical() на MagicMock greeclimate.Device, відмову на невідомий mode, переклад eco/health/quiet/light, ініціалізацію meta.
Тести підставляють greeclimate у sys.modules ще до імпорту драйвера, тому вони проходять навіть без встановленого пакета (наприклад, у CI).
pytest tests/test_device_watchdog.py tests/test_energy_monitor.py -q
# 47 passed-
docker compose up -d --build— обов'язковий rebuild, бо змінивсяrequirements.txt. - Рядки інтентів автоматично claim'ляться модулем при першому старті — seed-скрипт не потрібен.
- Виявлення: Device Control → Gree / Pular → Сканувати → побачити Pular → Імпортувати.
-
Вотчер:
docker compose logs -f core→ очікуватиdevice.online, потімdevice.state_changedприблизно кожні 5 с. - Прямий контроль: тоглнути on/off у віджеті Device Control → AC реагує.
- Climate UI: відкрити віджет Climate → картка з'являється у потрібній кімнаті → +/− температури, режим, вентилятор, живлення — все відображається на AC.
-
Голос (UK): «встанови температуру на 24» →
voice.intent→ device-control резолвить AC → фізична зміна. - Голос (EN): «switch bedroom to cool mode» → резолвиться лише на AC у спальні → режим змінюється.
-
Persistence після рестарту:
docker compose restart core→ AC переконнектиться без нового binding (meta.gree.keyзберігся).
- Одне джерело клімату на команду — multi-room голосові команди («встанови всі кондиціонери на 24») потребують broadcast-шляху; поза скоупом v1.
-
Без розкладів / комфорт-профілів — Climate-модуль лише презентаційний. Розклади належать
automation-engine. -
Без перегляду історії у v1 —
GET /historyне реалізовано; сирі дані лежать уstate_history, можна додати пізніше. -
Варіанти прошивок Pular — якщо discovery повертає порожній список, зніміть LAN-трафік
tcpdump -i any udp port 7000під час handshake додатка Gree+/Ewpe Smart, щоб виявити відмінності діалекту. Ручне додавання черезPOST /devicesзавжди працює як резерв. -
Дрейф API
greeclimate— драйвер націлений на v2.x. OEM-специфічні атрибути (steady_heat,anion,quiet) можуть відрізнятися на ребрендованих юнітах; перевіряти на залізі і підправляти мапінг_to_logical/_apply_logicalза потреби.
🤖 This wiki is auto-synced from docs/ in the main repo. Hand-edits on the wiki UI get overwritten on the next push. Open a PR against the main repo instead.
MIT License · Sponsor · Ko-fi
SelenaCore
🇬🇧 English
Getting started
Architecture
Voice & translation
Hardware integration
Development
- Modules overview
- Module development
- System module development
- Module API guide
- Module bus protocol
- Widget development
- User manager / auth
Reference
🇺🇦 Українська
Початок
Архітектура
Голос і переклад
Інтеграція заліза
Розробка
- Розробка модулів
- Розробка системних модулів
- Module API
- Module bus
- Widget development
- User manager / auth
Довідник