- BrainTools - https://www.braintools.ru -
Автор: Алексей Бобрешов, руководитель отдела искусственного интеллекта [1] в федеральном холдинге Категория: Искусственный интеллект, безопасность, умный дом, приватность *Это продолжение серии статей.
Когда я начинал работу над дипломным проектом «Умный дом» в 2020–2021 годах, моя голова была забита другими вопросами:
Как добиться точности распознавания выше 90%?
Как оптимизировать нейросеть для работы на слабом железе?
Как интегрировать распознавание команд с реальными устройствами?
**О безопасности я практически не думал. Ну это логично… Зачем – это же диплом-проект?! **
Сегодня, имея опыт [2] работы над коммерческими ИИ-проектами в крупных компаниях, я понимаю: безопасность — это не базовый набор, это фундамент. И если вы не заложили его с самого начала, перестройка будет болезненной.
В этой статье (Часть 6) я расскажу:
Какие уязвимости я обнаружил в своем дипломном проекте постфактум
Как защитить голосовое управление от утечек и взломов (ИМХО)
Какие архитектурные решения я бы изменил, начни я проект сегодня
Практические рекомендации для тех, кто создает ИИ-системы
Давайте честно: когда ты студент, работающий над дипломом, твои приоритеты выглядят так:
Функциональность — чтобы работало
Точность — чтобы работало хорошо
Производительность — чтобы работало быстро
Безопасность — а что это вообще такое?
В моем дипломе были реализованы:
Распознавание голосовых команд с точностью 94.06%
Интеграция с устройствами умного дома
Обучение [3] до 250 эпох, в борьбе с переобучением
Но не было:
Шифрования голосовых данных
Защиты от replay-атак
Аудита и логирования доступа
Изоляции сетевых сегментов
Когда я начал работать над коммерческими проектами, мой взгляд на безопасность кардинально изменился. Вот что я понял:
Уязвимость №1: Голосовые данные передаются в открытом виде
В моем дипломе аудиопоток передавался от микрофона к нейросети без шифрования. В локальной сети это еще норм, но если представить, что система выходит в интернет…
Риск: Перехват голосовых команд, включая потенциально чувствительную информацию (пароли, адреса, персональные данные).
Уязвимость №2: Нет разграничения прав доступа
Система выполняла команды любого, кто их произнес. Нет понятия «пользователь», «администратор», «гость». (поправочка: предусматривалась разработка, т.е. я думал про этот пункт)
Риск: Любой человек в радиусе слышимости может выключить сигнализацию, открыть дверь или получить доступ к конфиденциальной информации.
Уязвимость №3: Отсутствие защиты от replay-атак
Если злоумышленник запишет вашу голосовую команду «открой дверь», он сможет воспроизвести ее позже.
Риск: Обход системы аутентификации через запись и воспроизведение команд.
Уязвимость №4: Нейросеть как black box
Я не логировал, какие команды были распознаны, кто их отдал, когда и при каких обстоятельствах.
Риск: Невозможность расследования инцидентов, отсутствие аудита.
Давайте систематизируем угрозы:
|
Тип угрозы |
Описание |
Пример |
|---|---|---|
|
Перехват данных |
Перехват голосовых команд при передаче |
Сниффинг трафика в Wi-Fi сети |
|
Replay-атаки |
Запись и воспроизведение команд |
Запись команды «открой дверь» |
|
Spoofing |
Подделка голоса |
Deepfake аудио, синтез голоса |
|
Несанкционированный доступ |
Выполнение команд посторонними |
Гость отдает команды хозяина |
|
Утечка данных |
Компрометация хранимых данных |
Кража базы голосовых профилей |
|
Adversarial attacks |
Специально созданные команды |
Скрытые команды в аудио |
На основе моего опыта, я рекомендую следующую архитектуру:
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 1: ФИЗИЧЕСКАЯ │
├─────────────────────────────────────────────────────────────┤
│ • Изоляция микрофонов (аппаратное отключение) │
│ • Индикаторы активности микрофона (LED) │
│ • Физическая защита устройств │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 2: СЕТЕВАЯ БЕЗОПАСНОСТЬ │
├─────────────────────────────────────────────────────────────┤
│ • Шифрование трафика (TLS 1.3) │
│ • Сегментация сети (VLAN для IoT) │
│ • Firewall и фильтрация трафика │
│ • VPN для удаленного доступа │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 3: АУТЕНТИФИКАЦИЯ │
├─────────────────────────────────────────────────────────────┤
│ • Распознавание голоса (speaker verification) │
│ • Multi-factor authentication (голос + PIN/биометрия) │
│ • Session management (таймауты сессий) │
│ • Защита от replay-атак (nonce, timestamps) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 4: АВТОРИЗАЦИЯ │
├─────────────────────────────────────────────────────────────┤
│ • RBAC (Role-Based Access Control) │
│ • Гранулярные права доступа │
│ • Контекстная авторизация (время, место, устройство) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 5: ЗАЩИТА ДАННЫХ │
├─────────────────────────────────────────────────────────────┤
│ • Шифрование данных at rest (AES-256) │
│ • Анонимизация и псевдонимизация │
│ • Secure storage (HSM, TPM) │
│ • Data retention policies │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 6: МОНИТОРИНГ И АУДИТ │
├─────────────────────────────────────────────────────────────┤
│ • Логирование всех команд и событий │
│ • SIEM системы (Security Information & Event Management) │
│ • Аномалии детекшн (ML-based) │
│ • Регулярные security audits │
└─────────────────────────────────────────────────────────────┘
Если бы я начинал проект сегодня, вот конкретные изменения:
Планировал но не сделал: Система распознавала только команду, но не того, кто ее отдал.
Стало бы: Двухэтапная проверка:
Кто говорит? (Speaker Verification)
Что говорит? (Speech Recognition)
# Пример архитектуры
class SecureVoiceControl:
def __init__(self):
self.speaker_verifier = SpeakerVerificationModel()
self.command_recognizer = CommandRecognitionModel()
self.authorizer = AuthorizationEngine()
def process_command(self, audio):
# Этап 1: Верификация диктора
speaker_id = self.speaker_verifier.identify(audio)
if not speaker_id:
raise UnauthorizedError("Unknown speaker")
# Этап 2: Распознавание команды
command = self.command_recognizer.recognize(audio)
# Этап 3: Авторизация
if not self.authorizer.can_execute(speaker_id, command):
raise ForbiddenError(f"User {speaker_id} cannot execute {command}")
# Этап 4: Выполнение с логированием
self.audit_log(speaker_id, command)
return self.execute(command)
Было: Команды выполнялись без проверки уникальности.
Стало бы: Использование nonce и timestamps:
import time
import hashlib
import secrets
class ReplayProtection:
def __init__(self):
self.used_nonces = set()
self.nonce_ttl = 300 # 5 минут
def generate_challenge(self):
"""Генерация уникального challenge"""
nonce = secrets.token_hex(16)
timestamp = int(time.time())
return f"{nonce}:{timestamp}"
def verify_response(self, challenge, response, audio):
"""Проверка ответа на challenge"""
nonce, timestamp = challenge.split(':')
# Проверка свежести
if time.time() - int(timestamp) > self.nonce_ttl:
raise ReplayAttackError("Challenge expired")
# Проверка уникальности nonce
if nonce in self.used_nonces:
raise ReplayAttackError("Nonce already used")
# Проверка подписи
expected_signature = hashlib.sha256(
f"{nonce}{audio}".encode()
).hexdigest()
if response != expected_signature:
raise ReplayAttackError("Invalid signature")
# Отметка nonce как использованного
self.used_nonces.add(nonce)
# Очистка старых nonce
self.cleanup_old_nonces()
Было: Аудиоданные передавались в открытом виде.
Стало бы: End-to-end шифрование:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
class EncryptedAudioStream:
def __init__(self, key):
self.key = key
self.backend = default_backend()
def encrypt_audio(self, audio_data):
"""Шифрование аудиопотока"""
iv = os.urandom(16)
cipher = Cipher(
algorithms.AES(self.key),
modes.GCM(iv),
backend=self.backend
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(audio_data) + encryptor.finalize()
return {
'ciphertext': ciphertext,
'iv': iv,
'tag': encryptor.tag
}
def decrypt_audio(self, encrypted_data):
"""Расшифровка аудиопотока"""
cipher = Cipher(
algorithms.AES(self.key),
modes.GCM(
encrypted_data['iv'],
encrypted_data['tag']
),
backend=self.backend
)
decryptor = cipher.decryptor()
return decryptor.update(encrypted_data['ciphertext']) + decryptor.finalize()
Было: Все команды выполнялись без проверки прав.
Стало бы: Гранулярная система прав:
from enum import Enum
from dataclasses import dataclass
from typing import Set, Dict
class Permission(Enum):
LIGHT_ON = "light:on"
LIGHT_OFF = "light:off"
DOOR_UNLOCK = "door:unlock"
THERMOSTAT_CHANGE = "thermostat:change"
CAMERA_VIEW = "camera:view"
ADMIN_ACCESS = "admin:all"
class Role(Enum):
GUEST = "guest"
FAMILY = "family"
ADMIN = "admin"
@dataclass
class User:
id: str
name: str
role: Role
class AuthorizationEngine:
def __init__(self):
self.role_permissions: Dict[Role, Set[Permission]] = {
Role.GUEST: {
Permission.LIGHT_ON,
Permission.LIGHT_OFF,
},
Role.FAMILY: {
Permission.LIGHT_ON,
Permission.LIGHT_OFF,
Permission.THERMOSTAT_CHANGE,
Permission.CAMERA_VIEW,
},
Role.ADMIN: {
Permission.LIGHT_ON,
Permission.LIGHT_OFF,
Permission.DOOR_UNLOCK,
Permission.THERMOSTAT_CHANGE,
Permission.CAMERA_VIEW,
Permission.ADMIN_ACCESS,
}
}
def can_execute(self, user: User, command: str) -> bool:
"""Проверка прав выполнения команды"""
command_permission = self.command_to_permission(command)
return command_permission in self.role_permissions[user.role]
def command_to_permission(self, command: str) -> Permission:
"""Конвертация команды в permission"""
mapping = {
"включи свет": Permission.LIGHT_ON,
"выключи свет": Permission.LIGHT_OFF,
"открой дверь": Permission.DOOR_UNLOCK,
"измени температуру": Permission.THERMOSTAT_CHANGE,
"покажи камеру": Permission.CAMERA_VIEW,
}
return mapping.get(command, Permission.LIGHT_ON)
Было: Никакого логирования.
Стало бы: Детальное логирование всех событий:
import logging
import json
from datetime import datetime
from typing import Dict, Any
class SecurityAuditLogger:
def __init__(self, log_file: str = "security_audit.log"):
self.logger = logging.getLogger("security_audit")
self.logger.setLevel(logging.INFO)
handler = logging.FileHandler(log_file)
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
def log_command_execution(self,
user_id: str,
command: str,
success: bool,
context: Dict[str, Any] = None):
"""Логирование выполнения команды"""
event = {
"event_type": "command_execution",
"timestamp": datetime.utcnow().isoformat(),
"user_id": user_id,
"command": command,
"success": success,
"context": context or {},
"ip_address": context.get("ip_address"),
"device_id": context.get("device_id"),
}
self.logger.info(json.dumps(event))
# Alert на подозрительную активность
if not success:
self.log_security_alert("failed_command", event)
def log_security_alert(self, alert_type: str, event: Dict):
"""Логирование событий безопасности"""
alert = {
"alert_type": alert_type,
"severity": self.calculate_severity(alert_type),
"timestamp": datetime.utcnow().isoformat(),
"event": event
}
self.logger.warning(f"SECURITY_ALERT: {json.dumps(alert)}")
def calculate_severity(self, alert_type: str) -> str:
"""Определение уровня критичности"""
severity_map = {
"failed_command": "LOW",
"replay_attack_detected": "HIGH",
"unauthorized_access": "CRITICAL",
"brute_force_detected": "HIGH",
}
return severity_map.get(alert_type, "MEDIUM")
При работе с голосовыми данными вы попадаете под действие:
В России:
152-ФЗ «О персональных данных»
Голосовые данные — это биометрические персональные данные
Требуется письменное согласие на обработку
Обязательная локализация баз данных на территории РФ
Уведомление Роскомнадзора
В Европе:
GDPR (General Data Protection Regulation)
Статья 9: Особые категории данных (биометрия)
Право на забвение (статья 17)
Privacy by Design (статья 25)
Штрафы до 4% от годового оборота или 20 млн евро
При проектировании системы я рекомендую следовать принципам:
Принцип 1: Минимизация данных
Собирайте только то, что действительно нужно:
# НЕЛЬЗЯ: Сохранять все аудиозаписи
def process_voice(audio):
save_to_database(audio) # ❌
command = recognize(audio)
return command
# НУЖНО: Обрабатывать и удалять
def process_voice(audio):
command = recognize(audio)
delete_audio(audio) # ✅
save_command_metadata(command) # Только метаданные
return command
Принцип 2: Локальная обработка
По возможности обрабатывайте данные локально:
# Предпочтительно: Edge computing
class LocalVoiceProcessor:
def __init__(self):
self.model = load_on_device_model()
def process(self, audio):
# Все данные остаются на устройстве
return self.model.predict(audio)
Принцип 3: Прозрачность
Информируйте пользователей (Инструкцией к ПО):
class PrivacyNotice:
def __init__(self):
self.notice = """
Мы собираем следующие данные:
- Голосовые команды (для распознавания)
- Время выполнения команд (для аудита)
Мы НЕ собираем:
- Фоновые разговоры
- Биометрические шаблоны (после верификации)
Ваши права:
- Запросить копию данных
- Удалить данные
- Отозвать согласие
"""
Принцип 4: Контроль пользователя
Предоставьте инструменты управления:
class UserDataController:
def export_user_data(self, user_id: str) -> bytes:
"""Экспорт всех данных пользователя (GDPR Article 20)"""
data = {
"voice_commands": self.get_commands(user_id),
"voice_profile": self.get_profile(user_id),
"audit_logs": self.get_logs(user_id),
}
return json.dumps(data).encode()
def delete_user_data(self, user_id: str):
"""Полное удаление данных (GDPR Article 17)"""
self.delete_commands(user_id)
self.delete_profile(user_id)
self.delete_logs(user_id)
self.revoke_consent(user_id)
Проблема 1: Постоянное прослушивание
Даже если система «слушает» только wake word («Алиса», «Салют»), это создает ощущение постоянного наблюдения.
Решение:
Аппаратное отключение микрофона (физическая кнопка)
Визуальная индикация (LED при активном микрофоне)
Локальная обработка wake word (не отправлять в облако)
Проблема 2: Дети и уязвимые группы
Дети могут не осознавать, что их данные собираются.
Решение:
Детский режим (ограниченные команды, повышенная приватность)
Родительский контроль
Автоматическое удаление детских записей
Проблема 3: Дискриминация алгоритмов
Нейросети могут хуже распознавать:
Акценты
Детские голоса
Голоса пожилых людей
Люди с нарушениями речи
Решение:
Разнообразные тренировочные данные
Тестирование на разных демографических группах
Альтернативные способы ввода (текст, жесты)
Этап 1: Threat Modeling
Используйте методологию STRIDE:
Spoofing (подделка личности)
Tampering (несанкционированное изменение)
Repudiation (отказ от действий)
Information Disclosure (раскрытие информации)
Denial of Service (отказ в обслуживании)
Elevation of Privilege (повышение привилегий)
Этап 2: Penetration Testing
Примеры тестов:
class VoiceSecurityTester:
def test_replay_attack(self):
"""Тест на replay-атаку"""
# Запись команды
original_audio = record_command("открой дверь")
original_response = send_command(original_audio)
# Повторная отправка той же записи
replay_response = send_command(original_audio)
# Ожидаем блокировку
assert replay_response.status == "BLOCKED", "Replay attack succeeded!"
def test_spoofing_attack(self):
"""Тест на подделку голоса"""
# Синтез голоса через TTS
fake_audio = tts_synthesize("открой дверь", target_voice="admin")
response = send_command(fake_audio)
# Ожидаем отказ
assert response.status == "UNAUTHORIZED", "Voice spoofing succeeded!"
def test_adversarial_attack(self):
"""Тест на adversarial примеры"""
# Создание adversarial audio
clean_audio = record_command("включи свет")
adversarial_audio = add_adversarial_noise(
clean_audio,
target_command="открой дверь"
)
response = send_command(adversarial_audio)
# Ожидаем распознавание оригинальной команды
assert response.command == "включи свет", "Adversarial attack succeeded!"
def test_privacy_leak(self):
"""Тест на утечку данных"""
# Отправка команды
send_command("какой у меня пароль?")
# Перехват сетевого трафика
network_traffic = capture_network_traffic()
# Проверка на наличие чувствительных данных
assert "пароль" not in network_traffic, "Privacy leak detected!"
Этап 3: Code Review
Чеклист для code review:
[ ] Все данные шифруются при передаче (TLS)
[ ] Все данные шифруются при хранении (AES-256)
[ ] Пароли и ключи не захардкожены
[ ] Реализована защита от replay-атак
[ ] Есть логирование security events
[ ] Реализован rate limiting
[ ] Есть input validation
[ ] Нет уязвимостей (SQL injection, XSS, etc.)
Этап 4: Compliance Audit
Проверка соответствия:
152-ФЗ (для РФ)
GDPR (для ЕС)
Отраслевым стандартам (если есть)
Для сетевого анализа:
Wireshark
tcpdump
Burp Suite
Для fuzzing:
AFL (American Fuzzy Lop)
libFuzzer
custom fuzzers для аудио
Для статического анализа:
SonarQube
Bandit (для Python)
Semgrep
Для динамического анализа:
OWASP ZAP
Metasploit
Архитектура:
[ ] Используется defense in depth (многоуровневая защита)
[ ] Реализована сегментация сети
[ ] Есть изоляция критических компонентов
[ ] Используется principle of least privilege
Аутентификация:
[ ] Реализована multi-factor authentication
[ ] Есть защита от brute force (rate limiting, account lockout)
[ ] Используются secure сессии (timeout, rotation)
[ ] Реализована защита от replay-атак
Шифрование:
[ ] TLS 1.3 для передачи данных
[ ] AES-256 для хранения данных
[ ] Secure key management (HSM, KMS)
[ ] Regular key rotation
Приватность:
[ ] Data minimization (только необходимые данные)
[ ] Purpose limitation (только заявленные цели)
[ ] Storage limitation (автоматическое удаление)
[ ] Privacy by design и by default
Мониторинг:
[ ] Логирование всех security events
[ ] SIEM система
[ ] Alerting на аномалии
[ ] Regular security audits
Разработка:
[ ] Secure coding practices
[ ] Code review с фокусом на безопасность
[ ] SAST/DAST инструменты
[ ] Dependency scanning (уязвимости в библиотеках)
Если вы используете умный дом:
Настройки:
[ ] Измените дефолтные пароли
[ ] Включите двухфакторную аутентификацию
[ ] Отключите ненужные функции
[ ] Проверьте разрешения приложений
Сеть:
[ ] Используйте отдельную VLAN для IoT
[ ] Включите WPA3 на Wi-Fi
[ ] Отключите WPS
[ ] Обновите прошивку роутера
Приватность:
[ ] Проверьте, какие данные собираются
[ ] Отключите сбор данных, если возможно
[ ] Регулярно очищайте историю команд
[ ] Используйте локальную обработку
Физическая безопасность:
[ ] Используйте физические переключатели для микрофонов
[ ] Размещайте устройства вне прямой видимости с улицы
[ ] Защищайте устройства от физического доступа
Deepfake Voice Attacks
С развитием генеративного ИИ (GPT, Tacotron, WaveNet) создание реалистичных подделок голоса становится тривиальным.
Защита:
Liveness detection (проверка «живости» голоса)
Multi-modal authentication (голос + лицо + поведение [4])
Continuous authentication (постоянная проверка в течение сессии)
Adversarial Machine Learning
Специально созданные аудиокоманды, неслышимые для человека, но распознаваемые ИИ.
Защита:
Adversarial training (обучение на adversarial примерах)
Input sanitization (очистка входных данных)
Ensemble models (ансамбли моделей для детекции аномалий)
Supply Chain Attacks
Компрометация библиотек, фреймворков, обновлений прошивки.
Защита:
Code signing (подпись кода)
Secure boot (безопасная загрузка)
SBOM (Software Bill of Materials)
Dependency verification
Federated Learning
Обучение моделей без передачи сырых данных:
Данные остаются на устройстве
Передаются только градиенты
Differential privacy для защиты градиентов
Homomorphic Encryption
Вычисления на зашифрованных данных:
Данные никогда не расшифровываются
Медленно, но безопасно
Подходит для критических операций
Secure Multi-Party Computation (MPC)
Совместные вычисления без раскрытия данных:
Несколько сторон участвуют в вычислениях
Никто не видит данные других
Cryptographic guarantees
Ожидаемые изменения:
EU AI Act
Классификация ИИ по уровню риска
Голосовые системы — высокий риск
Обязательная сертификация
US Executive Order on AI
Standards for AI safety
Red-teaming requirements
Transparency obligations
Russia
Развитие 152-ФЗ для ИИ
Требования к локализации
Обязательная сертификация
Когда я начинал свой дипломный проект в 2020 году, я думал о безопасности как о «фиче», которую можно добавить потом.
Сегодня я понимаю: это была ошибка [5].
Безопасность — это:
Архитектурное решение, а не патч
Культура разработки, а не чеклист
Непрерывный процесс, а не разовое мероприятие
Баланс между удобством и защитой, а не компромисс
Начни с threat modeling — пойми угрозы до написания кода
Используй security by design — закладывай защиту в архитектуру
Тестируй на безопасность — не только на функциональность
Учись continuously — угрозы эволюционируют
Не доверяй, проверяй — zero trust architecture
Не повторяйте моих ошибок. Безопасность — это не «потом». Это «сейчас».
Ваш умный дом должен быть не только умным, но и безопасным. Мой дом – моя крепость
Если вы создаёте нейросети и ИИ-системы:
Сразу, с первого дня, думайте о безопасности — хотя можно сделать её потом (но до внедрения)
Регулярно проверяйте систему: нет ли дыр и уязвимостей (не просто так есть тестировщики)
Учитесь сами: как защищать свой ИИ от взлома и атак
Если вы руководите продуктом (продукт-менеджер):
Сделайте правило: задача считается готовой, только если она безопасная
Закладывайте деньги в бюджет на тестирование безопасности
Ставьте в приоритет функции, которые защищают личные данные пользователей
Если вы обычный пользователь:
Требуйте от компаний, чтобы они честно рассказывали, как работают их ИИ
Настраивайте приватность в приложениях и соцсетях (не оставляйте как попало)
Регулярно обновляйте свои устройства — телефон, ноутбук, планшет (даже не ИИ)
Читать Часть 1: «От диплома до продакшена: Как я создавал архитектуру ИИ-проекта для… Часть 1: Что я хотел видеть дома в 2021» [6]
Читать Часть 5: «Интеграция с устройствами «Умного дома» — от модели к реальному устройству» [7]
Автор: Алексей Бобрешов, руководитель отдела искусственного интеллекта
Лицензия: CC BY-NC 4.0
Автор: AlekseiVB
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/28875
URLs in this post:
[1] интеллекта: http://www.braintools.ru/article/7605
[2] опыт: http://www.braintools.ru/article/6952
[3] Обучение: http://www.braintools.ru/article/5125
[4] поведение: http://www.braintools.ru/article/9372
[5] ошибка: http://www.braintools.ru/article/4192
[6] «От диплома до продакшена: Как я создавал архитектуру ИИ-проекта для… Часть 1: Что я хотел видеть дома в 2021»: https://habr.com/ru/articles/1001512/
[7] «Интеграция с устройствами «Умного дома» — от модели к реальному устройству»: https://habr.com/ru/articles/1012052/
[8] Источник: https://habr.com/ru/articles/1023434/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1023434
Нажмите здесь для печати.