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