v2.5.2 — фикс порядка блоков (батареи), фикс форматирования, список 'Неизвестно', системный статус, поддержка scene, мониторинг switch
This commit is contained in:
+165
-28
@@ -2,7 +2,7 @@
|
||||
title: Home Assistant Smart Control
|
||||
description: Управление умным домом через Home Assistant REST API с интерпретацией естественного языка.
|
||||
author: ChatGPT (refactored by MiniMax-M3)
|
||||
version: 2.5.1
|
||||
version: 2.5.2
|
||||
license: MIT
|
||||
"""
|
||||
|
||||
@@ -50,7 +50,7 @@ class Tools:
|
||||
"sensor": ["sensor", "показател", "значение", "значения"],
|
||||
"light": ["light", "свет", "лампочк", "лампа", "люстр", "свитч"],
|
||||
"switch": ["switch", "розетк", "выключател", "реле"],
|
||||
"climate": ["climate", "климат", "кондиционер", "термостат"],
|
||||
"climate": ["climate", "климат", "климата", "кондиционер", "термостат", "пол", "полотенц"],
|
||||
"cover": ["cover", "штор", "жалюз", "роллет", "гардин"],
|
||||
"media_player": ["media_player", "медиа", "плеер", "колонк", "телевизор"],
|
||||
"lock": ["lock", "замок"],
|
||||
@@ -60,7 +60,8 @@ class Tools:
|
||||
"device_tracker": ["device_tracker", "трекер", "геолокац"],
|
||||
"button": ["button", "кнопк"],
|
||||
"automation": ["automation", "автоматизац"],
|
||||
"script": ["script", "скрипт", "сцен", "сценарий"],
|
||||
"script": ["script", "скрипт"],
|
||||
"scene": ["scene", "сцен"],
|
||||
"input_boolean": ["input_boolean", "переключател", "тумблер"],
|
||||
}
|
||||
|
||||
@@ -80,8 +81,6 @@ class Tools:
|
||||
"domains": ["binary_sensor"],
|
||||
"open_states": ("open", "on", "unlocked", "detected"),
|
||||
"closed_states": ("closed", "off", "locked", "clear"),
|
||||
# Допустимые device_class для окон/дверей/замков.
|
||||
# Если в HA не задан device_class — сущность игнорируется.
|
||||
"valid_device_classes": ("door", "window", "garage_door", "lock"),
|
||||
},
|
||||
"light_status": {
|
||||
@@ -89,7 +88,7 @@ class Tools:
|
||||
"keywords": ["свет", "лампочк", "люстр"],
|
||||
},
|
||||
"climate_status": {
|
||||
"domains": ["climate"],
|
||||
"domains": ["climate", "sensor"],
|
||||
"keywords": ["температур", "климат", "тепл", "холод", "кондиционер", "батаре"],
|
||||
},
|
||||
"battery": {
|
||||
@@ -115,6 +114,15 @@ class Tools:
|
||||
"device_classes": ["volatile_organic_compounds", "carbon_dioxide",
|
||||
"pm25", "pm10", "humidity", "aqi"],
|
||||
},
|
||||
"switches": {
|
||||
"domains": ["switch"],
|
||||
"keywords": ["розетк", "выключател", "реле"],
|
||||
},
|
||||
"system": {
|
||||
"domains": ["binary_sensor"],
|
||||
"keywords": ["бэкап", "backup", "stale", "updates", "updates_package",
|
||||
"node", "status", "connection", "gateway", "bridge"],
|
||||
},
|
||||
}
|
||||
|
||||
def _headers(self) -> Dict[str, str]:
|
||||
@@ -218,7 +226,15 @@ class Tools:
|
||||
return (s.get("attributes", {}) or {}).get(key, default)
|
||||
|
||||
def _resolve_domain(self, cmd: str) -> Optional[str]:
|
||||
# Приоритет: scene > script (т.к. "сцен" подходит обоим)
|
||||
for domain in ["scene", "script", "automation", "binary_sensor"]:
|
||||
if domain in self._DOMAIN_ALIASES:
|
||||
for a in self._DOMAIN_ALIASES[domain]:
|
||||
if a in cmd:
|
||||
return domain
|
||||
for domain, aliases in self._DOMAIN_ALIASES.items():
|
||||
if domain in ("scene", "script", "automation", "binary_sensor"):
|
||||
continue
|
||||
for a in aliases:
|
||||
if a in cmd:
|
||||
return domain
|
||||
@@ -249,25 +265,24 @@ class Tools:
|
||||
return default
|
||||
|
||||
def _format_entity(self, s: Dict[str, Any]) -> str:
|
||||
"""Форматирует сущность для вывода. Пробел между state и unit."""
|
||||
attrs = s.get("attributes", {}) or {}
|
||||
fn = attrs.get("friendly_name", s["entity_id"])
|
||||
state = s.get("state", "?")
|
||||
unit = attrs.get("unit_of_measurement", "")
|
||||
area = attrs.get("area", "") or attrs.get("area_id", "")
|
||||
room = " (" + area + ")" if area else ""
|
||||
return " * " + fn + room + ": " + str(state) + str(unit)
|
||||
# Разделяем state и unit пробелом только если оба непустые
|
||||
if state and unit and unit not in str(state):
|
||||
state_str = str(state) + " " + str(unit)
|
||||
else:
|
||||
state_str = str(state)
|
||||
return " * " + fn + room + ": " + state_str
|
||||
|
||||
# ========================================================================
|
||||
# Мониторинг
|
||||
# ========================================================================
|
||||
def _monitor_windows_doors(self, tokens: List[str]) -> List[str]:
|
||||
"""
|
||||
Какие окна/двери открыты.
|
||||
|
||||
Строгий фильтр: только binary_sensor с device_class в
|
||||
(door, window, garage_door, lock). Это исключает ложные
|
||||
срабатывания на датчики движения, батареи штор, шторы Aqara и т.п.
|
||||
"""
|
||||
cfg = self._MONITOR_DOMAINS["window_door"]
|
||||
valid_dc = cfg["valid_device_classes"]
|
||||
states = self._get_states()
|
||||
@@ -298,9 +313,13 @@ class Tools:
|
||||
for s in open_items:
|
||||
lines.append(self._format_entity(s))
|
||||
if closed_items:
|
||||
lines.append("Закрыто: " + str(len(closed_items)) + " шт.")
|
||||
lines.append("**Закрыто:**")
|
||||
for s in closed_items:
|
||||
lines.append(self._format_entity(s))
|
||||
if unknown:
|
||||
lines.append("Неизвестно: " + str(len(unknown)) + " шт.")
|
||||
lines.append("**Неизвестно** (state не в open/closed):")
|
||||
for s in unknown:
|
||||
lines.append(self._format_entity(s))
|
||||
|
||||
if tokens:
|
||||
filtered = []
|
||||
@@ -327,14 +346,14 @@ class Tools:
|
||||
off_items.append(eid)
|
||||
lines: List[str] = []
|
||||
if on_items:
|
||||
lines.append("Горит: " + str(len(on_items)) + " шт.")
|
||||
lines.append("**Горит:** " + str(len(on_items)) + " шт.")
|
||||
for eid in on_items[:15]:
|
||||
for s in self._get_states():
|
||||
if s["entity_id"] == eid:
|
||||
lines.append(self._format_entity(s))
|
||||
break
|
||||
if off_items:
|
||||
lines.append("Выключено: " + str(len(off_items)) + " шт.")
|
||||
lines.append("**Выключено:** " + str(len(off_items)) + " шт.")
|
||||
return lines
|
||||
|
||||
def _monitor_climate(self, tokens: List[str]) -> List[str]:
|
||||
@@ -491,6 +510,91 @@ class Tools:
|
||||
lines.append(" (информация отсутствует)")
|
||||
return lines
|
||||
|
||||
def _monitor_switches(self, tokens: List[str]) -> List[str]:
|
||||
"""Какие розетки/реле включены."""
|
||||
cfg = self._MONITOR_DOMAINS["switches"]
|
||||
entities = self._find_entities(tokens if tokens else cfg["keywords"], "switch")
|
||||
if not entities:
|
||||
return ["розетки не найдены"]
|
||||
on_items: List[str] = []
|
||||
off_items: List[str] = []
|
||||
for eid in entities:
|
||||
state = self._get_state(eid)
|
||||
if state == "on":
|
||||
on_items.append(eid)
|
||||
else:
|
||||
off_items.append(eid)
|
||||
lines: List[str] = []
|
||||
if on_items:
|
||||
lines.append("**Включено:** " + str(len(on_items)) + " шт.")
|
||||
for eid in on_items[:15]:
|
||||
for s in self._get_states():
|
||||
if s["entity_id"] == eid:
|
||||
lines.append(self._format_entity(s))
|
||||
break
|
||||
if off_items:
|
||||
lines.append("**Выключено:** " + str(len(off_items)) + " шт.")
|
||||
return lines
|
||||
|
||||
def _monitor_system(self) -> List[str]:
|
||||
"""Состояние технической инфраструктуры: ноды, бэкапы, обновления, мосты."""
|
||||
states = self._get_states()
|
||||
items: List[str] = []
|
||||
warnings: List[str] = []
|
||||
|
||||
keys_node = ("node_", "node ")
|
||||
keys_update = ("updates", "package")
|
||||
keys_backup = ("backup", "stale")
|
||||
keys_conn = ("connection", "gateway", "bridge")
|
||||
|
||||
for s in states:
|
||||
eid = s["entity_id"]
|
||||
if not eid.startswith("binary_sensor."):
|
||||
continue
|
||||
name = (self._attr(s, "friendly_name", "") or "").lower()
|
||||
cur = s.get("state", "")
|
||||
text = (eid + " " + name).lower()
|
||||
|
||||
if any(k in text for k in keys_backup):
|
||||
# Backups Stale: off = всё ОК
|
||||
if cur == "on":
|
||||
warnings.append("Бэкапы устарели: " + self._attr(s, "friendly_name", eid))
|
||||
continue
|
||||
|
||||
if any(k in text for k in keys_update):
|
||||
if cur == "on":
|
||||
warnings.append("Доступны обновления: " + self._attr(s, "friendly_name", eid))
|
||||
continue
|
||||
|
||||
if any(k in text for k in keys_node):
|
||||
# Node kybinka Status: on = OK
|
||||
if cur == "off":
|
||||
warnings.append("Нода offline: " + self._attr(s, "friendly_name", eid))
|
||||
else:
|
||||
items.append(self._format_entity(s))
|
||||
continue
|
||||
|
||||
if any(k in text for k in keys_conn):
|
||||
if cur == "off":
|
||||
warnings.append("Соединение потеряно: " + self._attr(s, "friendly_name", eid))
|
||||
else:
|
||||
items.append(self._format_entity(s))
|
||||
continue
|
||||
|
||||
lines = ["**Системный статус:**"]
|
||||
if items:
|
||||
lines.append("OK:")
|
||||
for s in items:
|
||||
lines.append(s)
|
||||
if warnings:
|
||||
lines.append("")
|
||||
lines.append("⚠️ **Требует внимания:**")
|
||||
for w in warnings:
|
||||
lines.append(" - " + w)
|
||||
if not items and not warnings:
|
||||
lines.append("Нет данных о системе")
|
||||
return lines
|
||||
|
||||
def _monitor_full_report(self) -> List[str]:
|
||||
lines = ["**Отчёт по дому:**", ""]
|
||||
lines.extend(self._monitor_presence([]))
|
||||
@@ -499,6 +603,8 @@ class Tools:
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_lights([]))
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_switches([]))
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_climate([]))
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_air_quality([]))
|
||||
@@ -506,6 +612,8 @@ class Tools:
|
||||
lines.extend(self._monitor_motion([]))
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_battery([]))
|
||||
lines.append("")
|
||||
lines.extend(self._monitor_system())
|
||||
return lines
|
||||
|
||||
# ========================================================================
|
||||
@@ -575,9 +683,15 @@ class Tools:
|
||||
if is_question:
|
||||
matched = False
|
||||
|
||||
# ВАЖНО: порядок блоков влияет на маршрутизацию.
|
||||
# Более специфичные ключевые слова — первыми.
|
||||
|
||||
if any(k in cmd for k in ["окн", "двер", "форточ", "калитк", "ворота", "гараж"]):
|
||||
actions.extend(self._monitor_windows_doors(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["розетк", "выключател", "реле"]):
|
||||
actions.extend(self._monitor_switches(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["свет", "лампочк", "люстр"]) and (
|
||||
"горит" in cmd or "включ" in cmd or "лишн" in cmd
|
||||
):
|
||||
@@ -589,18 +703,22 @@ class Tools:
|
||||
elif any(k in cmd for k in ["движен", "двигает", "хот", "кто-то ход"]):
|
||||
actions.extend(self._monitor_motion(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["температур", "климат", "тепл", "холод", "кондиционер", "батаре"]):
|
||||
actions.extend(self._monitor_climate(tokens))
|
||||
matched = True
|
||||
elif "батарейк" in cmd or "заряд" in cmd:
|
||||
# БАТАРЕЙКИ — раньше климата! иначе "батарейки" → "батаре" → климат
|
||||
elif "батарейк" in cmd or "аккумулятор" in cmd or "заряд" in cmd:
|
||||
actions.extend(self._monitor_battery(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["температур", "климат", "тепл", "холод", "кондиционер"]):
|
||||
actions.extend(self._monitor_climate(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["музык", "медиа", "плеер", "колонк", "телевизор"]):
|
||||
actions.extend(self._monitor_media(tokens))
|
||||
matched = True
|
||||
elif "дома" in cmd or "присутств" in cmd:
|
||||
elif "дома" in cmd or "присутств" in cmd or "кто" in cmd:
|
||||
actions.extend(self._monitor_presence(tokens))
|
||||
matched = True
|
||||
elif any(k in cmd for k in ["систем", "нод", "бэкап", "backup", "обновлен", "мост"]):
|
||||
actions.extend(self._monitor_system())
|
||||
matched = True
|
||||
|
||||
if not matched:
|
||||
actions.append("**Общий статус дома:**")
|
||||
@@ -623,6 +741,8 @@ class Tools:
|
||||
" - где есть движение?\n"
|
||||
" - кто дома?\n"
|
||||
" - батарейки садятся?\n"
|
||||
" - что в розетках включено?\n"
|
||||
" - как система? есть ли обновления?\n"
|
||||
" - покажи все binary_sensor\n"
|
||||
" - переименуй automation.сертификат в Обновление SSL\n"
|
||||
" - выключи автоматизацию Гирлянда крыльцо\n"
|
||||
@@ -661,7 +781,7 @@ class Tools:
|
||||
t in cmd for t in [
|
||||
"спальн", "гостин", "кухн", "детск", "ванн",
|
||||
"прихож", "коридор", "кабинет", "зал", "веранд",
|
||||
"дима", "диан", "елен", "анатоли",
|
||||
"дима", "диан", "елен", "анатоли", "милен",
|
||||
]
|
||||
) and len(entities) > 1
|
||||
|
||||
@@ -869,9 +989,27 @@ class Tools:
|
||||
else:
|
||||
warnings.append("кнопки не найдены")
|
||||
|
||||
if any(k in cmd for k in ["сцен", "режим", "запуст"]):
|
||||
all_scripts = self._find_entities([], "script")
|
||||
# Сцены (домен scene — мгновенное применение)
|
||||
if any(k in cmd for k in ["сцен"]) and "запуст" in cmd:
|
||||
all_scenes = self._find_entities([], "scene")
|
||||
matched: List[str] = []
|
||||
for s in all_scenes:
|
||||
sl = s.lower()
|
||||
for t in tokens:
|
||||
if len(t) >= 3 and t in sl:
|
||||
matched.append(s)
|
||||
break
|
||||
if matched:
|
||||
for s in matched:
|
||||
self._call_service("scene", "turn_on", {"entity_id": s})
|
||||
actions.append("SCENE " + s)
|
||||
else:
|
||||
warnings.append("подходящая сцена не найдена среди " + str(len(all_scenes)))
|
||||
|
||||
# Скрипты (run-once)
|
||||
if "скрипт" in cmd or "запуст" in cmd:
|
||||
all_scripts = self._find_entities([], "script")
|
||||
matched = []
|
||||
for s in all_scripts:
|
||||
sl = s.lower()
|
||||
for t in tokens:
|
||||
@@ -883,7 +1021,7 @@ class Tools:
|
||||
self._call_service("script", "turn_on", {"entity_id": s})
|
||||
actions.append("SCRIPT " + s)
|
||||
else:
|
||||
warnings.append("подходящая сцена не найдена среди " + str(len(all_scripts)))
|
||||
warnings.append("подходящий скрипт не найден среди " + str(len(all_scripts)))
|
||||
|
||||
return actions
|
||||
|
||||
@@ -918,7 +1056,6 @@ class Tools:
|
||||
|
||||
if domain:
|
||||
if domain == "window_door":
|
||||
# Строгий фильтр: только binary_sensor с правильным device_class
|
||||
if not eid.startswith("binary_sensor."):
|
||||
continue
|
||||
if dc not in ("door", "window", "garage_door", "lock"):
|
||||
|
||||
Reference in New Issue
Block a user