cleanup cli, configs, systemd and readme
This commit is contained in:
650
README.md
650
README.md
@@ -1,244 +1,530 @@
|
|||||||
# Alice Stations Plugin
|
# Alice Stations Plugin
|
||||||
|
|
||||||
Standalone-плагин для управления Яндекс Станциями, генерации шаблонов Loxone / wb-rules и web UI.
|
Плагин для поиска Яндекс Станций, сохранения токена, генерации конфигов для Loxone и wb-rules и запуска web UI.
|
||||||
|
|
||||||
## Содержимое репозитория
|
## Содержание
|
||||||
|
|
||||||
- `yastation.py` - демон управления колонками (локальный API: `127.0.0.1:9123`, HTTP API: `:9124`).
|
- [Что умеет плагин](#что-умеет-плагин)
|
||||||
- `alice_plugin.py` - CLI и web API для списка колонок, генерации шаблонов и установки wb-rules.
|
- [Что не умеет и слабые места](#что-не-умеет-и-слабые-места)
|
||||||
- `web/index.html` - web-интерфейс.
|
- [Структура проекта](#структура-проекта)
|
||||||
- `loxone/` - выходные файлы шаблонов и ZIP-архивы.
|
- [Куда устанавливать](#куда-устанавливать)
|
||||||
- `data/` - рабочие данные (`stations.json`, `config.json`).
|
- [Быстрый старт](#быстрый-старт)
|
||||||
- `systemd/*.service` - юниты сервисов.
|
- [Полная установка](#полная-установка)
|
||||||
- `cmd_*.sh` - готовые команды для типовых действий.
|
- [Токен](#токен)
|
||||||
|
- [CLI](#cli)
|
||||||
|
- [Прямые Python-команды](#прямые-python-команды)
|
||||||
|
- [Web UI](#web-ui)
|
||||||
|
- [Systemd](#systemd)
|
||||||
|
- [Где лежат файлы](#где-лежат-файлы)
|
||||||
|
- [Диагностика и логи](#диагностика-и-логи)
|
||||||
|
- [Частые ошибки](#частые-ошибки)
|
||||||
|
- [Удаление](#удаление)
|
||||||
|
|
||||||
## Требования
|
## Что умеет плагин
|
||||||
|
|
||||||
Минимум для Debian/Ubuntu, Wiren Board, Raspberry Pi OS:
|
- сохранить OAuth-токен Яндекса;
|
||||||
|
- получить список станций из облака и сопоставить его с локальным mDNS-поиском;
|
||||||
|
- сохранить итоговый список в `data/stations.json`;
|
||||||
|
- сгенерировать конфиги для `configs/loxone`;
|
||||||
|
- сгенерировать конфиги для `configs/wb-rules`;
|
||||||
|
- скачать ZIP-архивы через web UI;
|
||||||
|
- на Wiren Board установить `wb-rules` шаблоны прямо из web UI или CLI;
|
||||||
|
- поднять web UI для первичной настройки.
|
||||||
|
|
||||||
|
## Что не умеет и слабые места
|
||||||
|
|
||||||
|
- без валидного OAuth-токена обновление списка станций не работает;
|
||||||
|
- если станции и контроллер находятся в разных сетях, локальные IP могут не определиться;
|
||||||
|
- web UI не исправляет проблемы сети или токена, он только показывает состояние и запускает команды;
|
||||||
|
- генерация шаблонов зависит от `data/stations.json`, поэтому сначала всегда нужен поиск колонок;
|
||||||
|
- `wb-install` имеет смысл только на Wiren Board или на системе, где есть `/etc/wb-rules`.
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```text
|
||||||
|
alice/
|
||||||
|
├── alice_plugin.py
|
||||||
|
├── cli.sh
|
||||||
|
├── configs/
|
||||||
|
│ ├── loxone/
|
||||||
|
│ └── wb-rules/
|
||||||
|
├── data/
|
||||||
|
├── install.sh
|
||||||
|
├── README.md
|
||||||
|
├── requirements.txt
|
||||||
|
├── start.sh
|
||||||
|
├── start_plugin_web.sh
|
||||||
|
├── systemd/
|
||||||
|
│ ├── shd-alice-plugin.service
|
||||||
|
│ └── shd-alice.service
|
||||||
|
├── web/
|
||||||
|
│ └── index.html
|
||||||
|
└── yastation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Назначение файлов:
|
||||||
|
|
||||||
|
- `alice_plugin.py` — CLI и backend для web UI;
|
||||||
|
- `yastation.py` — демон управления станциями и HTTP API на `:9124`;
|
||||||
|
- `cli.sh` — основной способ работы из терминала;
|
||||||
|
- `start.sh` — запуск демона для systemd;
|
||||||
|
- `start_plugin_web.sh` — запуск web UI для systemd;
|
||||||
|
- `install.sh` — установка в `/opt/shd/plugins/alice`, создание `.venv`, каталогов и systemd-юнитов.
|
||||||
|
|
||||||
|
## Куда устанавливать
|
||||||
|
|
||||||
|
Штатный путь проекта зафиксирован:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/shd/plugins/alice
|
||||||
|
```
|
||||||
|
|
||||||
|
`install.sh` сам синхронизирует текущий каталог в этот путь. После установки основной рабочий каталог должен быть именно таким:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/shd/plugins/alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /путь/к/распакованному/репозиторию
|
||||||
|
chmod +x *.sh
|
||||||
|
./install.sh
|
||||||
|
|
||||||
|
printf %s 'YANDEX_OAUTH_TOKEN' > /opt/shd/plugins/alice/token.txt
|
||||||
|
chmod 600 /opt/shd/plugins/alice/token.txt
|
||||||
|
|
||||||
|
cd /opt/shd/plugins/alice
|
||||||
|
./cli.sh columns
|
||||||
|
./cli.sh loxone
|
||||||
|
./cli.sh wb-rules
|
||||||
|
./cli.sh web
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого web UI обычно доступен по адресу:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://<ip-устройства>:9140/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полная установка
|
||||||
|
|
||||||
|
### 1. Установить системные зависимости
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y python3 python3-venv python3-pip
|
sudo apt install -y python3 python3-venv python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
Важно:
|
Для более удобной синхронизации в `/opt` желателен `rsync`, но без него установка тоже работает.
|
||||||
- команды установки и запуска нужно выполнять из каталога проекта;
|
|
||||||
- если нет `python3-venv`, `install.sh` завершится ошибкой и сервисы не установятся.
|
|
||||||
|
|
||||||
## Рекомендуемая структура на контроллере
|
### 2. Распаковать проект и запустить установку
|
||||||
|
|
||||||
```text
|
|
||||||
/home/shd/scripts/alice
|
|
||||||
```
|
|
||||||
|
|
||||||
Если репозиторий скопирован в другое место, нужно скорректировать пути в `systemd/*.service`.
|
|
||||||
|
|
||||||
## Установка (пошагово)
|
|
||||||
|
|
||||||
1. Скопировать проект:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /home/shd/scripts
|
cd /путь/к/распакованному/репозиторию
|
||||||
cd /home/shd/scripts
|
|
||||||
cp -a alice_station_plugin_repo alice
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Установить зависимости Python и systemd-сервисы:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
chmod +x *.sh
|
chmod +x *.sh
|
||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Сохранить OAuth-токен:
|
Что делает `install.sh`:
|
||||||
|
|
||||||
|
- копирует проект в `/opt/shd/plugins/alice`;
|
||||||
|
- создаёт `.venv`, если его нет;
|
||||||
|
- ставит зависимости из `requirements.txt`;
|
||||||
|
- создаёт каталоги `data/`, `configs/loxone/`, `configs/wb-rules/`;
|
||||||
|
- копирует systemd-юниты в `/etc/systemd/system/`;
|
||||||
|
- выполняет `systemctl daemon-reload` и `systemctl enable` для сервисов.
|
||||||
|
|
||||||
|
### 3. Сохранить токен
|
||||||
|
|
||||||
|
Через файл:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
printf %s 'YANDEX_OAUTH_TOKEN' > /home/shd/scripts/alice/token.txt
|
printf %s 'YANDEX_OAUTH_TOKEN' > /opt/shd/plugins/alice/token.txt
|
||||||
chmod 600 /home/shd/scripts/alice/token.txt
|
chmod 600 /opt/shd/plugins/alice/token.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Включить и запустить сервисы:
|
Или через CLI/web-команду:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/shd/plugins/alice
|
||||||
|
./cli.sh web --token 'YANDEX_OAUTH_TOKEN'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Обновить список станций
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/shd/plugins/alice
|
||||||
|
./cli.sh columns
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Сгенерировать конфиги
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh loxone
|
||||||
|
./cli.sh wb-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Включить сервисы
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now shd-alice.service shd-alice-plugin.service
|
sudo systemctl enable --now shd-alice.service shd-alice-plugin.service
|
||||||
```
|
```
|
||||||
|
|
||||||
## Проверка после установки
|
## Токен
|
||||||
|
|
||||||
Проверка сервисов:
|
Проект ищет токен в таком порядке:
|
||||||
|
|
||||||
|
1. аргумент `--token`;
|
||||||
|
2. переменная окружения `YANDEX_TOKEN`;
|
||||||
|
3. файл `token.txt`;
|
||||||
|
4. `data/config.json`.
|
||||||
|
|
||||||
|
Надёжнее всего использовать `token.txt`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl status shd-alice.service --no-pager
|
/opt/shd/plugins/alice/token.txt
|
||||||
sudo systemctl status shd-alice-plugin.service --no-pager
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Проверка web API:
|
Если токен неверный, при обновлении списка станций будет ошибка:
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s http://127.0.0.1:9140/api/status
|
|
||||||
```
|
|
||||||
|
|
||||||
Web UI:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://<controller-ip>:9140/
|
YANDEX_OAUTH_TOKEN invalid: update token and retry
|
||||||
```
|
```
|
||||||
|
|
||||||
## Обязательные команды (CLI)
|
## CLI
|
||||||
|
|
||||||
| Команда | Что делает | Входы | Выходы |
|
Основной интерфейс из терминала — `cli.sh`.
|
||||||
|---|---|---|---|
|
|
||||||
| `./cmd_1_get_columns.sh` | Обновляет список колонок (`stations-refresh`) | OAuth-токен: `token.txt` или `YANDEX_TOKEN` | `data/stations.json`, JSON в stdout (`ok`, `stations`, `cloud_total`, `local_total`) |
|
|
||||||
| `./cmd_2_get_loxone_templates.sh` | Генерирует шаблоны Loxone (`templates-loxone`) | `data/stations.json` (должен быть заполнен), `ALICE_CONTROLLER_HOST` (опц.) | Файлы в `loxone/`, архив `loxone/loxone_templates.zip`, JSON в stdout |
|
|
||||||
| `./cmd_3_get_wb_rules_templates.sh` | Генерирует шаблоны wb-rules (`templates-wb-rules`) | `data/stations.json` (должен быть заполнен), `ALICE_CONTROLLER_HOST` (опц.) | Файлы в `loxone/`, архив `loxone/wb_rules_templates.zip`, JSON в stdout |
|
|
||||||
|
|
||||||
Примеры запуска:
|
Перед использованием:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/shd/scripts/alice
|
cd /opt/shd/plugins/alice
|
||||||
./cmd_1_get_columns.sh
|
|
||||||
./cmd_2_get_loxone_templates.sh
|
|
||||||
./cmd_3_get_wb_rules_templates.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Команды `yastation.py` (daemon/client)
|
### Поиск колонок
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# локальные станции (mDNS)
|
./cli.sh columns
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py --timeout 3 list
|
|
||||||
|
|
||||||
# запуск daemon
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py --token "$YANDEX_TOKEN" serve --host 127.0.0.1 --port 9123 --http-host 0.0.0.0 --http-port 9124
|
|
||||||
|
|
||||||
# tts
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py client --host 127.0.0.1 --port 9123 --station "M00..." tts --text "Проверка"
|
|
||||||
|
|
||||||
# command
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py client --host 127.0.0.1 --port 9123 --station "M00..." command --text "включи музыку"
|
|
||||||
|
|
||||||
# player pause
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py client --host 127.0.0.1 --port 9123 --station "M00..." player --cmd pause
|
|
||||||
|
|
||||||
# volume
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py client --host 127.0.0.1 --port 9123 --station "M00..." volume --level 20
|
|
||||||
|
|
||||||
# raw
|
|
||||||
/home/shd/scripts/alice/.venv/bin/python3 /home/shd/scripts/alice/yastation.py client --host 127.0.0.1 --port 9123 --station "M00..." raw --payload '{"command":"setVolume","volume":0.2}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Для станции в шаблоне есть:
|
С токеном напрямую:
|
||||||
- `VirtualOut` команды: `TTS`, `Command`, `Play`, `Pause`, `Stop`, `Next`, `Prev`, `Volume`, `Raw`.
|
|
||||||
- `VirtualIn` статусы: `Daemon OK`, `WS Running`, `Playing`, `Volume`.
|
|
||||||
|
|
||||||
## Работа через web UI
|
```bash
|
||||||
|
./cli.sh --token 'YANDEX_OAUTH_TOKEN' columns
|
||||||
|
```
|
||||||
|
|
||||||
На странице можно:
|
С таймаутом локального поиска:
|
||||||
- сохранить OAuth-токен;
|
|
||||||
- найти/обновить список колонок;
|
```bash
|
||||||
|
./cli.sh columns --timeout 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Что делает команда:
|
||||||
|
|
||||||
|
- проверяет токен;
|
||||||
|
- читает облачный список устройств;
|
||||||
|
- делает локальный mDNS-поиск;
|
||||||
|
- объединяет результаты;
|
||||||
|
- пишет `data/stations.json`.
|
||||||
|
|
||||||
|
### Генерация Loxone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh loxone
|
||||||
|
```
|
||||||
|
|
||||||
|
С ручным IP контроллера:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh loxone --controller-host 192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
Что создаётся:
|
||||||
|
|
||||||
|
- каталоги по станциям в `configs/loxone/`;
|
||||||
|
- `VO.xml`;
|
||||||
|
- `VI.xml`;
|
||||||
|
- `README.md` по каждой станции;
|
||||||
|
- `configs/loxone/loxone_templates.zip`.
|
||||||
|
|
||||||
|
### Генерация wb-rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh wb-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
С ручным IP контроллера:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh wb-rules --controller-host 192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
Что создаётся:
|
||||||
|
|
||||||
|
- каталоги по станциям в `configs/wb-rules/`;
|
||||||
|
- `wb-rules-station.js`;
|
||||||
|
- `README.md` по каждой станции;
|
||||||
|
- `configs/wb-rules/wb_rules_templates.zip`.
|
||||||
|
|
||||||
|
### Установка wb-rules в систему
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh wb-install
|
||||||
|
```
|
||||||
|
|
||||||
|
Для одной станции:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh wb-install --station-id <station_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Что делает команда:
|
||||||
|
|
||||||
|
- берёт файлы из `configs/wb-rules/`;
|
||||||
|
- копирует их в `/etc/wb-rules/`;
|
||||||
|
- перезапускает `wb-rules`.
|
||||||
|
|
||||||
|
### Запуск web UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh web
|
||||||
|
```
|
||||||
|
|
||||||
|
С токеном при первом запуске:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh --token 'YANDEX_OAUTH_TOKEN' web
|
||||||
|
```
|
||||||
|
|
||||||
|
С другим адресом и портом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh web --host 0.0.0.0 --port 9140
|
||||||
|
```
|
||||||
|
|
||||||
|
## Прямые Python-команды
|
||||||
|
|
||||||
|
Все публичные Python-команды задокументированы ниже. Их можно использовать напрямую, но штатный способ — `cli.sh`.
|
||||||
|
|
||||||
|
### Обновить станции
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py --token 'YANDEX_OAUTH_TOKEN' stations-refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
Дополнительно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py --token 'YANDEX_OAUTH_TOKEN' stations-refresh --timeout 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сгенерировать Loxone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py templates-loxone
|
||||||
|
```
|
||||||
|
|
||||||
|
С ручным IP контроллера:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py templates-loxone --controller-host 192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сгенерировать wb-rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py templates-wb-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
С ручным IP контроллера:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py templates-wb-rules --controller-host 192.168.1.50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Установить wb-rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py wb-install
|
||||||
|
```
|
||||||
|
|
||||||
|
Для одной станции:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py wb-install --station-id <station_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запустить web UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py --token 'YANDEX_OAUTH_TOKEN' web
|
||||||
|
```
|
||||||
|
|
||||||
|
На другом адресе/порту:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 alice_plugin.py web --host 0.0.0.0 --port 9140
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
Web UI умеет:
|
||||||
|
|
||||||
|
- сохранить токен;
|
||||||
|
- сохранить IP контроллера;
|
||||||
|
- обновить список колонок;
|
||||||
- скачать ZIP для Loxone;
|
- скачать ZIP для Loxone;
|
||||||
- скачать ZIP для wb-rules;
|
- скачать ZIP для wb-rules;
|
||||||
- на Wiren Board доступны кнопки установки wb-rules в `/etc/wb-rules`.
|
- на Wiren Board установить `wb-rules` по кнопке.
|
||||||
|
|
||||||
## Установка wb-rules из CLI
|
Страница лежит по адресу:
|
||||||
|
|
||||||
Установить все станции:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
.venv/bin/python3 alice_plugin.py wb-install
|
|
||||||
```
|
|
||||||
|
|
||||||
Установить только одну станцию:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
.venv/bin/python3 alice_plugin.py wb-install --station-id <station_id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Сервисы и управление
|
|
||||||
|
|
||||||
- Демон Alice: `shd-alice.service`
|
|
||||||
- Web UI: `shd-alice-plugin.service`
|
|
||||||
|
|
||||||
Перезапуск:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart shd-alice.service shd-alice-plugin.service
|
|
||||||
```
|
|
||||||
|
|
||||||
Остановка:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl stop shd-alice.service shd-alice-plugin.service
|
|
||||||
```
|
|
||||||
|
|
||||||
## Типовые ошибки и решения
|
|
||||||
|
|
||||||
### `Unit shd-alice.service not found`
|
|
||||||
|
|
||||||
Причина: `install.sh` не завершился успешно, поэтому юниты не были скопированы в `/etc/systemd/system`.
|
|
||||||
|
|
||||||
Что делать:
|
|
||||||
1. Исправить ошибку установки (часто это отсутствие `python3-venv`).
|
|
||||||
2. Снова запустить:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
./install.sh
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now shd-alice.service shd-alice-plugin.service
|
|
||||||
```
|
|
||||||
|
|
||||||
### `The virtual environment was not created successfully because ensurepip is not available`
|
|
||||||
|
|
||||||
Причина: отсутствует пакет `python3-venv`.
|
|
||||||
|
|
||||||
Решение:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y python3-venv python3-pip
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### `./install.sh: No such file or directory`
|
|
||||||
|
|
||||||
Причина: команда выполняется не из каталога проекта.
|
|
||||||
|
|
||||||
Решение:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/shd/scripts/alice
|
|
||||||
./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Токен не подхватывается
|
|
||||||
|
|
||||||
Проверь, что файл существует именно по пути:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/home/shd/scripts/alice/token.txt
|
http://<ip-устройства>:9140/
|
||||||
```
|
```
|
||||||
|
|
||||||
А не в `/home/shd/scripts/token.txt`.
|
Если станции ещё не найдены, страница покажет пустой список и предложит сначала сохранить токен и нажать кнопку поиска.
|
||||||
|
|
||||||
## Переменные окружения (опционально)
|
## Systemd
|
||||||
|
|
||||||
- `YANDEX_TOKEN` - OAuth токен (если не используется `token.txt`).
|
В проекте есть два сервиса:
|
||||||
- `ALICE_CONTROLLER_HOST` - host/IP, который вставляется в шаблоны.
|
|
||||||
- `ALICE_PLUGIN_WEB_PORT` - порт web UI (по умолчанию `9140`).
|
|
||||||
|
|
||||||
## Удаление с контроллера
|
- `shd-alice.service` — основной демон `yastation.py`;
|
||||||
|
- `shd-alice-plugin.service` — web UI.
|
||||||
|
|
||||||
|
Юниты используют фиксированный путь:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/shd/plugins/alice
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка состояния:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl stop shd-alice.service shd-alice-plugin.service
|
sudo systemctl status shd-alice.service
|
||||||
sudo systemctl disable shd-alice.service shd-alice-plugin.service
|
sudo systemctl status shd-alice-plugin.service
|
||||||
sudo rm -f /etc/systemd/system/shd-alice.service /etc/systemd/system/shd-alice-plugin.service
|
```
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo rm -rf /home/shd/scripts/alice
|
Логи:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u shd-alice.service -f
|
||||||
|
journalctl -u shd-alice-plugin.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Ручной запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/shd/plugins/alice
|
||||||
|
./start.sh
|
||||||
|
./start_plugin_web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Где лежат файлы
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/shd/plugins/alice/
|
||||||
|
├── token.txt
|
||||||
|
├── data/
|
||||||
|
│ ├── config.json
|
||||||
|
│ └── stations.json
|
||||||
|
└── configs/
|
||||||
|
├── loxone/
|
||||||
|
│ └── loxone_templates.zip
|
||||||
|
└── wb-rules/
|
||||||
|
└── wb_rules_templates.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключевые файлы:
|
||||||
|
|
||||||
|
- `token.txt` — OAuth-токен;
|
||||||
|
- `data/config.json` — сохранённый IP контроллера и служебные настройки;
|
||||||
|
- `data/stations.json` — найденные станции;
|
||||||
|
- `configs/loxone/loxone_templates.zip` — ZIP для Loxone;
|
||||||
|
- `configs/wb-rules/wb_rules_templates.zip` — ZIP для wb-rules.
|
||||||
|
|
||||||
|
## Диагностика и логи
|
||||||
|
|
||||||
|
Проверить, что Python и зависимости в порядке:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/shd/plugins/alice
|
||||||
|
.venv/bin/python3 -m py_compile alice_plugin.py yastation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить web API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:9140/api/status
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить список станций руками:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh columns
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить генерацию шаблонов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh loxone
|
||||||
|
./cli.sh wb-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Частые ошибки
|
||||||
|
|
||||||
|
### `YANDEX_OAUTH_TOKEN invalid: update token and retry`
|
||||||
|
|
||||||
|
Токен неверный или протух. Перезапиши `token.txt` и повтори поиск колонок.
|
||||||
|
|
||||||
|
### `stations.json is empty, run station refresh first`
|
||||||
|
|
||||||
|
Сначала выполни:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./cli.sh columns
|
||||||
|
```
|
||||||
|
|
||||||
|
### `No stations found`
|
||||||
|
|
||||||
|
Локальный поиск ничего не нашёл. Проверь:
|
||||||
|
|
||||||
|
- что станции в той же сети;
|
||||||
|
- что mDNS не режется роутером/VLAN;
|
||||||
|
- что токен валиден и облачный список вообще отдаётся.
|
||||||
|
|
||||||
|
### `No such file or directory` в systemd
|
||||||
|
|
||||||
|
Обычно проект лежит не в `/opt/shd/plugins/alice`. Перезапусти установку:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/etc/wb-rules not found`
|
||||||
|
|
||||||
|
Команда `wb-install` запускается не на Wiren Board и не на системе с установленным `wb-rules`.
|
||||||
|
|
||||||
|
## Удаление
|
||||||
|
|
||||||
|
Остановить сервисы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now shd-alice.service shd-alice-plugin.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Удалить юниты:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm -f /etc/systemd/system/shd-alice.service
|
||||||
|
sudo rm -f /etc/systemd/system/shd-alice-plugin.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Удалить каталог проекта:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm -rf /opt/shd/plugins/alice
|
||||||
```
|
```
|
||||||
|
|||||||
101
alice_plugin.py
101
alice_plugin.py
@@ -24,7 +24,9 @@ import yastation
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
DATA_DIR = BASE_DIR / "data"
|
DATA_DIR = BASE_DIR / "data"
|
||||||
LOXONE_DIR = BASE_DIR / "loxone"
|
CONFIGS_DIR = BASE_DIR / "configs"
|
||||||
|
LOXONE_DIR = CONFIGS_DIR / "loxone"
|
||||||
|
WB_RULES_DIR = CONFIGS_DIR / "wb-rules"
|
||||||
STATIONS_PATH = DATA_DIR / "stations.json"
|
STATIONS_PATH = DATA_DIR / "stations.json"
|
||||||
CONFIG_PATH = DATA_DIR / "config.json"
|
CONFIG_PATH = DATA_DIR / "config.json"
|
||||||
TOKEN_PATH = BASE_DIR / "token.txt"
|
TOKEN_PATH = BASE_DIR / "token.txt"
|
||||||
@@ -59,7 +61,9 @@ class StationRecord:
|
|||||||
|
|
||||||
def ensure_dirs() -> None:
|
def ensure_dirs() -> None:
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
LOXONE_DIR.mkdir(parents=True, exist_ok=True)
|
LOXONE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
WB_RULES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def safe_name(value: str, fallback: str) -> str:
|
def safe_name(value: str, fallback: str) -> str:
|
||||||
@@ -533,19 +537,17 @@ defineRule(DEVICE_ID + "_status_poll", {{
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def write_station_templates(station: StationRecord, controller_host: str, target_dir: Path) -> Dict[str, str]:
|
def write_loxone_templates(station: StationRecord, controller_host: str) -> Dict[str, str]:
|
||||||
station_folder = target_dir / safe_name(station.name or station.station_id, station.station_id or "station")
|
station_folder = LOXONE_DIR / safe_name(station.name or station.station_id, station.station_id or "station")
|
||||||
station_folder.mkdir(parents=True, exist_ok=True)
|
station_folder.mkdir(parents=True, exist_ok=True)
|
||||||
selector = station.selector()
|
selector = station.selector()
|
||||||
|
|
||||||
vo_path = station_folder / "VO.xml"
|
vo_path = station_folder / "VO.xml"
|
||||||
vi_path = station_folder / "VI.xml"
|
vi_path = station_folder / "VI.xml"
|
||||||
wb_path = station_folder / "wb-rules-station.js"
|
|
||||||
readme_path = station_folder / "README.md"
|
readme_path = station_folder / "README.md"
|
||||||
|
|
||||||
vo_path.write_text(build_vo_xml(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
vo_path.write_text(build_vo_xml(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
||||||
vi_path.write_text(build_vi_xml(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
vi_path.write_text(build_vi_xml(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
||||||
wb_path.write_text(build_wb_rules_js(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
|
||||||
readme_path.write_text(
|
readme_path.write_text(
|
||||||
"# Alice templates\n\n"
|
"# Alice templates\n\n"
|
||||||
f"- Station: {station.name}\n"
|
f"- Station: {station.name}\n"
|
||||||
@@ -561,6 +563,31 @@ def write_station_templates(station: StationRecord, controller_host: str, target
|
|||||||
"folder": str(station_folder.relative_to(BASE_DIR)),
|
"folder": str(station_folder.relative_to(BASE_DIR)),
|
||||||
"vo": str(vo_path.relative_to(BASE_DIR)),
|
"vo": str(vo_path.relative_to(BASE_DIR)),
|
||||||
"vi": str(vi_path.relative_to(BASE_DIR)),
|
"vi": str(vi_path.relative_to(BASE_DIR)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_wb_rules_templates(station: StationRecord, controller_host: str) -> Dict[str, str]:
|
||||||
|
station_folder = WB_RULES_DIR / safe_name(station.name or station.station_id, station.station_id or "station")
|
||||||
|
station_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
selector = station.selector()
|
||||||
|
|
||||||
|
wb_path = station_folder / "wb-rules-station.js"
|
||||||
|
readme_path = station_folder / "README.md"
|
||||||
|
|
||||||
|
wb_path.write_text(build_wb_rules_js(controller_host, station.name or station.station_id, selector), encoding="utf-8")
|
||||||
|
readme_path.write_text(
|
||||||
|
"# Alice wb-rules templates\n\n"
|
||||||
|
f"- Station: {station.name}\n"
|
||||||
|
f"- Station ID: {station.station_id}\n"
|
||||||
|
f"- Selector: {selector}\n"
|
||||||
|
f"- API: http://{controller_host}:9124\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"station": station.station_id,
|
||||||
|
"name": station.name,
|
||||||
|
"folder": str(station_folder.relative_to(BASE_DIR)),
|
||||||
"wb": str(wb_path.relative_to(BASE_DIR)),
|
"wb": str(wb_path.relative_to(BASE_DIR)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,28 +600,33 @@ def generate_templates(template_kind: str, controller_host: str) -> Dict[str, An
|
|||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
result: List[Dict[str, str]] = []
|
result: List[Dict[str, str]] = []
|
||||||
|
|
||||||
for station in stations:
|
|
||||||
files = write_station_templates(station, controller_host, LOXONE_DIR)
|
|
||||||
if template_kind == "loxone":
|
if template_kind == "loxone":
|
||||||
files = {k: v for k, v in files.items() if k in {"station", "name", "folder", "vo", "vi"}}
|
target_dir = LOXONE_DIR
|
||||||
|
zip_name = "loxone_templates.zip"
|
||||||
|
for station in stations:
|
||||||
|
result.append(write_loxone_templates(station, controller_host))
|
||||||
elif template_kind == "wb-rules":
|
elif template_kind == "wb-rules":
|
||||||
files = {k: v for k, v in files.items() if k in {"station", "name", "folder", "wb"}}
|
target_dir = WB_RULES_DIR
|
||||||
result.append(files)
|
zip_name = "wb_rules_templates.zip"
|
||||||
|
for station in stations:
|
||||||
|
result.append(write_wb_rules_templates(station, controller_host))
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"unsupported template kind: {template_kind}")
|
||||||
|
|
||||||
zip_name = "loxone_templates.zip" if template_kind == "loxone" else "wb_rules_templates.zip"
|
zip_path = target_dir / zip_name
|
||||||
zip_path = LOXONE_DIR / zip_name
|
|
||||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
for station in stations:
|
for station in stations:
|
||||||
folder = LOXONE_DIR / safe_name(station.name or station.station_id, station.station_id or "station")
|
folder = target_dir / safe_name(station.name or station.station_id, station.station_id or "station")
|
||||||
if template_kind in ("loxone", "all"):
|
if template_kind == "loxone":
|
||||||
for fn in ["VO.xml", "VI.xml", "README.md"]:
|
for fn in ["VO.xml", "VI.xml", "README.md"]:
|
||||||
p = folder / fn
|
p = folder / fn
|
||||||
if p.exists():
|
if p.exists():
|
||||||
zf.write(p, arcname=f"{folder.name}/{fn}")
|
zf.write(p, arcname=f"{folder.name}/{fn}")
|
||||||
if template_kind in ("wb-rules", "all"):
|
elif template_kind == "wb-rules":
|
||||||
p = folder / "wb-rules-station.js"
|
for fn in ["wb-rules-station.js", "README.md"]:
|
||||||
|
p = folder / fn
|
||||||
if p.exists():
|
if p.exists():
|
||||||
zf.write(p, arcname=f"{folder.name}/wb-rules-station.js")
|
zf.write(p, arcname=f"{folder.name}/{fn}")
|
||||||
|
|
||||||
return {"ok": True, "templates": result, "zip": str(zip_path.relative_to(BASE_DIR))}
|
return {"ok": True, "templates": result, "zip": str(zip_path.relative_to(BASE_DIR))}
|
||||||
|
|
||||||
@@ -616,10 +648,10 @@ def install_wb_rules(station_id: Optional[str] = None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
installed: List[str] = []
|
installed: List[str] = []
|
||||||
for station in stations:
|
for station in stations:
|
||||||
folder = LOXONE_DIR / safe_name(station.name or station.station_id, station.station_id or "station")
|
folder = WB_RULES_DIR / safe_name(station.name or station.station_id, station.station_id or "station")
|
||||||
src = folder / "wb-rules-station.js"
|
src = folder / "wb-rules-station.js"
|
||||||
if not src.exists():
|
if not src.exists():
|
||||||
write_station_templates(station, detect_controller_host(), LOXONE_DIR)
|
write_wb_rules_templates(station, detect_controller_host())
|
||||||
src = folder / "wb-rules-station.js"
|
src = folder / "wb-rules-station.js"
|
||||||
dst = target_dir / f"alice_station_{safe_name(station.station_id, 'station')}.js"
|
dst = target_dir / f"alice_station_{safe_name(station.station_id, 'station')}.js"
|
||||||
|
|
||||||
@@ -643,7 +675,14 @@ async def refresh_stations(token: str, timeout: float = 3.0) -> Dict[str, Any]:
|
|||||||
if not token:
|
if not token:
|
||||||
raise RuntimeError("token is required")
|
raise RuntimeError("token is required")
|
||||||
|
|
||||||
|
try:
|
||||||
cloud = await fetch_cloud_stations(token)
|
cloud = await fetch_cloud_stations(token)
|
||||||
|
except Exception as exc:
|
||||||
|
msg = str(exc)
|
||||||
|
if "AUTH_TOKEN_INVALID" in msg:
|
||||||
|
raise RuntimeError("YANDEX_OAUTH_TOKEN invalid: update token and retry") from exc
|
||||||
|
raise
|
||||||
|
|
||||||
local = await discover_local(timeout=timeout)
|
local = await discover_local(timeout=timeout)
|
||||||
merged = merge_cloud_with_local(cloud, local)
|
merged = merge_cloud_with_local(cloud, local)
|
||||||
|
|
||||||
@@ -728,7 +767,7 @@ async def api_download(request: web.Request) -> web.StreamResponse:
|
|||||||
if kind not in {"loxone", "wb-rules"}:
|
if kind not in {"loxone", "wb-rules"}:
|
||||||
raise web.HTTPNotFound()
|
raise web.HTTPNotFound()
|
||||||
zip_name = "loxone_templates.zip" if kind == "loxone" else "wb_rules_templates.zip"
|
zip_name = "loxone_templates.zip" if kind == "loxone" else "wb_rules_templates.zip"
|
||||||
zip_path = LOXONE_DIR / zip_name
|
zip_path = (LOXONE_DIR if kind == "loxone" else WB_RULES_DIR) / zip_name
|
||||||
if not zip_path.exists():
|
if not zip_path.exists():
|
||||||
generate_templates(kind, detect_controller_host())
|
generate_templates(kind, detect_controller_host())
|
||||||
return web.FileResponse(zip_path)
|
return web.FileResponse(zip_path)
|
||||||
@@ -779,21 +818,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
sub = p.add_subparsers(dest="cmd", required=True)
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
sp = sub.add_parser("stations-refresh", help="1) get stations list")
|
sp = sub.add_parser("stations-refresh", help="Обновить список станций")
|
||||||
sp.add_argument("--timeout", type=float, default=3.0)
|
sp.add_argument("--timeout", type=float, default=3.0, help="Секунды ожидания локального mDNS-поиска")
|
||||||
|
|
||||||
sp = sub.add_parser("templates-loxone", help="2) generate Loxone templates in ./loxone")
|
sp = sub.add_parser("templates-loxone", help="Сгенерировать шаблоны Loxone в ./configs/loxone")
|
||||||
sp.add_argument("--controller-host", default="")
|
sp.add_argument("--controller-host", default="", help="IP или host контроллера для ссылок на API")
|
||||||
|
|
||||||
sp = sub.add_parser("templates-wb-rules", help="3) generate wb-rules templates in ./loxone")
|
sp = sub.add_parser("templates-wb-rules", help="Сгенерировать шаблоны wb-rules в ./configs/wb-rules")
|
||||||
sp.add_argument("--controller-host", default="")
|
sp.add_argument("--controller-host", default="", help="IP или host контроллера для ссылок на API")
|
||||||
|
|
||||||
sp = sub.add_parser("wb-install", help="install wb-rules templates to /etc/wb-rules")
|
sp = sub.add_parser("wb-install", help="Установить wb-rules шаблоны в /etc/wb-rules")
|
||||||
sp.add_argument("--station-id", default="")
|
sp.add_argument("--station-id", default="", help="Установить только одну станцию по station id")
|
||||||
|
|
||||||
sp = sub.add_parser("web", help="run web UI")
|
sp = sub.add_parser("web", help="Запустить web UI")
|
||||||
sp.add_argument("--host", default="0.0.0.0")
|
sp.add_argument("--host", default="0.0.0.0", help="Хост для web UI")
|
||||||
sp.add_argument("--port", type=int, default=9140)
|
sp.add_argument("--port", type=int, default=9140, help="Порт для web UI")
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|||||||
84
cli.sh
Executable file
84
cli.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PY="$BASE_DIR/.venv/bin/python3"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./cli.sh columns [--token TOKEN] [--timeout SEC]
|
||||||
|
./cli.sh loxone [--controller-host HOST]
|
||||||
|
./cli.sh wb-rules [--controller-host HOST]
|
||||||
|
./cli.sh wb-install [--station-id ID]
|
||||||
|
./cli.sh web [--token TOKEN] [--host HOST] [--port PORT]
|
||||||
|
|
||||||
|
Global options may be placed before or after the command.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || "${1:-}" == "help" || $# -eq 0 ]]; then
|
||||||
|
usage
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$PY" ]]; then
|
||||||
|
echo "Python venv not found: $BASE_DIR/.venv/bin/python3"
|
||||||
|
echo "Run ./install.sh first"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
GLOBAL_ARGS=()
|
||||||
|
POSITIONAL=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--token)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Missing value for --token"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
GLOBAL_ARGS+=("$1" "$2")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
POSITIONAL+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
CMD="${POSITIONAL[0]:-}"
|
||||||
|
if [[ -z "$CMD" ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
REST_ARGS=("${POSITIONAL[@]:1}")
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
columns)
|
||||||
|
exec "$PY" "$BASE_DIR/alice_plugin.py" "${GLOBAL_ARGS[@]}" stations-refresh "${REST_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
loxone)
|
||||||
|
exec "$PY" "$BASE_DIR/alice_plugin.py" "${GLOBAL_ARGS[@]}" templates-loxone "${REST_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
wb-rules)
|
||||||
|
exec "$PY" "$BASE_DIR/alice_plugin.py" "${GLOBAL_ARGS[@]}" templates-wb-rules "${REST_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
wb-install)
|
||||||
|
exec "$PY" "$BASE_DIR/alice_plugin.py" "${GLOBAL_ARGS[@]}" wb-install "${REST_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
web)
|
||||||
|
exec "$PY" "$BASE_DIR/alice_plugin.py" "${GLOBAL_ARGS[@]}" web "${REST_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $CMD"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
exec "$BASE_DIR/.venv/bin/python3" "$BASE_DIR/alice_plugin.py" stations-refresh "$@"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
exec "$BASE_DIR/.venv/bin/python3" "$BASE_DIR/alice_plugin.py" templates-loxone "$@"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
exec "$BASE_DIR/.venv/bin/python3" "$BASE_DIR/alice_plugin.py" templates-wb-rules "$@"
|
|
||||||
0
configs/wb-rules/.gitkeep
Normal file
0
configs/wb-rules/.gitkeep
Normal file
92
install.sh
Normal file → Executable file
92
install.sh
Normal file → Executable file
@@ -1,39 +1,89 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
APP_DIR="/opt/shd/plugins/alice"
|
||||||
cd "$BASE_DIR"
|
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
IN_PLACE="${1:-}"
|
||||||
|
|
||||||
chmod +x *.sh
|
sync_project() {
|
||||||
|
mkdir -p /opt/shd/plugins
|
||||||
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
|
rsync -a --delete --exclude '.venv/' --exclude '__pycache__/' --exclude '*.pyc' --exclude '*.zip' "$SRC_DIR/" "$APP_DIR/"
|
||||||
|
else
|
||||||
|
mkdir -p "$APP_DIR"
|
||||||
|
cp -a "$SRC_DIR/." "$APP_DIR/"
|
||||||
|
rm -rf "$APP_DIR/.venv" "$APP_DIR/__pycache__"
|
||||||
|
find "$APP_DIR" -type d -name '__pycache__' -prune -exec rm -rf {} +
|
||||||
|
find "$APP_DIR" -type f \( -name '*.pyc' -o -name '*.zip' \) -delete
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -d .venv ]]; then
|
if [[ "$SRC_DIR" != "$APP_DIR" && "$IN_PLACE" != "--in-place" ]]; then
|
||||||
rm -rf .venv 2>/dev/null || sudo rm -rf .venv
|
echo "Sync project to $APP_DIR"
|
||||||
|
if [[ -w /opt/shd/plugins || -w /opt/shd || -w /opt ]]; then
|
||||||
|
sync_project
|
||||||
|
else
|
||||||
|
if ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
echo "Need write access to /opt/shd/plugins or sudo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sudo mkdir -p /opt/shd/plugins
|
||||||
|
sudo rm -rf "$APP_DIR"
|
||||||
|
sudo mkdir -p "$APP_DIR"
|
||||||
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
|
sudo rsync -a --delete --exclude '.venv/' --exclude '__pycache__/' --exclude '*.pyc' --exclude '*.zip' "$SRC_DIR/" "$APP_DIR/"
|
||||||
|
else
|
||||||
|
sudo cp -a "$SRC_DIR/." "$APP_DIR/"
|
||||||
|
sudo rm -rf "$APP_DIR/.venv" "$APP_DIR/__pycache__"
|
||||||
|
sudo find "$APP_DIR" -type d -name '__pycache__' -prune -exec rm -rf {} +
|
||||||
|
sudo find "$APP_DIR" -type f \( -name '*.pyc' -o -name '*.zip' \) -delete
|
||||||
|
fi
|
||||||
|
if [[ -n "${SUDO_USER:-}" ]]; then
|
||||||
|
sudo chown -R "$SUDO_USER":"$SUDO_USER" "$APP_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exec "$APP_DIR/install.sh" --in-place
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
chmod +x cli.sh install.sh start.sh start_plugin_web.sh
|
||||||
|
|
||||||
|
if [[ ! -d .venv ]]; then
|
||||||
|
python3 -m venv .venv
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -m venv .venv
|
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
python3 -m pip install -r requirements.txt
|
python3 -m pip install -r requirements.txt
|
||||||
python3 -c "import aiohttp, zeroconf"
|
python3 -c "import aiohttp, zeroconf" >/dev/null
|
||||||
|
|
||||||
mkdir -p "$BASE_DIR/loxone" "$BASE_DIR/data"
|
mkdir -p data configs/loxone configs/wb-rules
|
||||||
|
: > data/.gitkeep
|
||||||
|
|
||||||
|
install_service() {
|
||||||
|
local src="$1"
|
||||||
|
local dst="/etc/systemd/system/$(basename "$src")"
|
||||||
|
if [[ -w /etc/systemd/system ]]; then
|
||||||
|
cp -f "$src" "$dst"
|
||||||
|
else
|
||||||
|
sudo cp -f "$src" "$dst"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
install_service "$APP_DIR/systemd/shd-alice.service"
|
||||||
|
install_service "$APP_DIR/systemd/shd-alice-plugin.service"
|
||||||
if [[ -w /etc/systemd/system ]]; then
|
if [[ -w /etc/systemd/system ]]; then
|
||||||
cp -f "$BASE_DIR/systemd/shd-alice.service" /etc/systemd/system/shd-alice.service
|
|
||||||
cp -f "$BASE_DIR/systemd/shd-alice-plugin.service" /etc/systemd/system/shd-alice-plugin.service
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable shd-alice.service || true
|
systemctl enable shd-alice.service shd-alice-plugin.service || true
|
||||||
systemctl enable shd-alice-plugin.service || true
|
else
|
||||||
systemctl restart shd-alice-plugin.service || true
|
sudo systemctl daemon-reload
|
||||||
elif command -v sudo >/dev/null 2>&1; then
|
sudo systemctl enable shd-alice.service shd-alice-plugin.service || true
|
||||||
sudo cp -f "$BASE_DIR/systemd/shd-alice.service" /etc/systemd/system/shd-alice.service || true
|
|
||||||
sudo cp -f "$BASE_DIR/systemd/shd-alice-plugin.service" /etc/systemd/system/shd-alice-plugin.service || true
|
|
||||||
sudo systemctl daemon-reload || true
|
|
||||||
sudo systemctl enable shd-alice.service || true
|
|
||||||
sudo systemctl enable shd-alice-plugin.service || true
|
|
||||||
sudo systemctl restart shd-alice-plugin.service || true
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Install done."
|
echo "Install done: $APP_DIR"
|
||||||
|
echo "Next:"
|
||||||
|
echo " 1) save token: printf %s 'YANDEX_OAUTH_TOKEN' > $APP_DIR/token.txt && chmod 600 $APP_DIR/token.txt"
|
||||||
|
echo " 2) update stations: $APP_DIR/cli.sh columns"
|
||||||
|
echo " 3) start web UI: $APP_DIR/cli.sh web"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^shd-alice.service'; then
|
|
||||||
systemctl restart shd-alice.service || sudo systemctl restart shd-alice.service || true
|
|
||||||
else
|
|
||||||
"$(cd "$(dirname "$0")" && pwd)/stop.sh"
|
|
||||||
nohup "$(cd "$(dirname "$0")" && pwd)/start.sh" >/tmp/shd-alice.log 2>&1 &
|
|
||||||
fi
|
|
||||||
9
start.sh
Normal file → Executable file
9
start.sh
Normal file → Executable file
@@ -16,7 +16,8 @@ python3 -c "import aiohttp, zeroconf" >/dev/null 2>&1 || {
|
|||||||
|
|
||||||
TOKEN="${YANDEX_TOKEN:-}"
|
TOKEN="${YANDEX_TOKEN:-}"
|
||||||
if [[ -z "$TOKEN" && -f "$BASE_DIR/token.txt" ]]; then
|
if [[ -z "$TOKEN" && -f "$BASE_DIR/token.txt" ]]; then
|
||||||
TOKEN="$(tr -d '\r' < "$BASE_DIR/token.txt")"
|
TOKEN="$(tr -d '
|
||||||
|
' < "$BASE_DIR/token.txt")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$TOKEN" ]]; then
|
if [[ -z "$TOKEN" ]]; then
|
||||||
@@ -29,8 +30,4 @@ BIND_PORT="${YASTATION_BIND_PORT:-9123}"
|
|||||||
HTTP_HOST="${YASTATION_HTTP_HOST:-0.0.0.0}"
|
HTTP_HOST="${YASTATION_HTTP_HOST:-0.0.0.0}"
|
||||||
HTTP_PORT="${YASTATION_HTTP_PORT:-9124}"
|
HTTP_PORT="${YASTATION_HTTP_PORT:-9124}"
|
||||||
|
|
||||||
exec "$BASE_DIR/.venv/bin/python3" "$BASE_DIR/yastation.py" --token "$TOKEN" serve \
|
exec "$BASE_DIR/.venv/bin/python3" "$BASE_DIR/yastation.py" --token "$TOKEN" serve --host "$BIND_HOST" --port "$BIND_PORT" --http-host "$HTTP_HOST" --http-port "$HTTP_PORT"
|
||||||
--host "$BIND_HOST" \
|
|
||||||
--port "$BIND_PORT" \
|
|
||||||
--http-host "$HTTP_HOST" \
|
|
||||||
--http-port "$HTTP_PORT"
|
|
||||||
|
|||||||
0
start_plugin_web.sh
Normal file → Executable file
0
start_plugin_web.sh
Normal file → Executable file
7
stop.sh
7
stop.sh
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^shd-alice.service'; then
|
|
||||||
systemctl stop shd-alice.service || sudo systemctl stop shd-alice.service || true
|
|
||||||
else
|
|
||||||
pkill -f '/home/shd/scripts/alice/yastation.py' || true
|
|
||||||
fi
|
|
||||||
@@ -5,9 +5,9 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/home/shd/scripts/alice
|
WorkingDirectory=/opt/shd/plugins/alice
|
||||||
EnvironmentFile=-/home/shd/scripts/alice/.env
|
EnvironmentFile=-/opt/shd/plugins/alice/.env
|
||||||
ExecStart=/bin/bash -lc '/home/shd/scripts/alice/start_plugin_web.sh'
|
ExecStart=/bin/bash /opt/shd/plugins/alice/start_plugin_web.sh
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/home/shd/scripts/alice
|
WorkingDirectory=/opt/shd/plugins/alice
|
||||||
EnvironmentFile=-/home/shd/scripts/alice/.env
|
EnvironmentFile=-/opt/shd/plugins/alice/.env
|
||||||
ExecStart=/bin/bash -lc '/home/shd/scripts/alice/start.sh'
|
ExecStart=/bin/bash /opt/shd/plugins/alice/start.sh
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|||||||
@@ -264,7 +264,14 @@
|
|||||||
function renderStations(items) {
|
function renderStations(items) {
|
||||||
const tbody = q('#stations');
|
const tbody = q('#stations');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
(items || []).forEach((s) => tbody.appendChild(stationRow(s)));
|
const rows = items || [];
|
||||||
|
if (!rows.length) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `<td colspan="7" class="muted">Станции пока не найдены. Сохрани токен и нажми «Найти колонки».</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.forEach((s) => tbody.appendChild(stationRow(s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStatus() {
|
async function refreshStatus() {
|
||||||
@@ -312,7 +319,7 @@
|
|||||||
setStatus('Генерирую Loxone шаблоны и готовлю архив...');
|
setStatus('Генерирую Loxone шаблоны и готовлю архив...');
|
||||||
await api('/api/templates/loxone', 'POST');
|
await api('/api/templates/loxone', 'POST');
|
||||||
window.location.href = '/api/download/loxone';
|
window.location.href = '/api/download/loxone';
|
||||||
setStatus('Скачивание Loxone ZIP запущено');
|
setStatus('Скачивание Loxone ZIP запущено. Локальная копия лежит в configs/loxone/loxone_templates.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(e.message, false);
|
setStatus(e.message, false);
|
||||||
}
|
}
|
||||||
@@ -323,7 +330,7 @@
|
|||||||
setStatus('Генерирую wb-rules шаблоны и готовлю архив...');
|
setStatus('Генерирую wb-rules шаблоны и готовлю архив...');
|
||||||
await api('/api/templates/wb-rules', 'POST');
|
await api('/api/templates/wb-rules', 'POST');
|
||||||
window.location.href = '/api/download/wb-rules';
|
window.location.href = '/api/download/wb-rules';
|
||||||
setStatus('Скачивание wb-rules ZIP запущено');
|
setStatus('Скачивание wb-rules ZIP запущено. Локальная копия лежит в configs/wb-rules/wb_rules_templates.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(e.message, false);
|
setStatus(e.message, false);
|
||||||
}
|
}
|
||||||
|
|||||||
11
yastation.py
11
yastation.py
@@ -479,9 +479,20 @@ async def serve_daemon(args) -> int:
|
|||||||
raise RuntimeError("No stations found")
|
raise RuntimeError("No stations found")
|
||||||
|
|
||||||
conns: Dict[str, StationConn] = {}
|
conns: Dict[str, StationConn] = {}
|
||||||
|
try:
|
||||||
for st in selected:
|
for st in selected:
|
||||||
conns[st.device_id] = StationConn(st, oauth, args.wait)
|
conns[st.device_id] = StationConn(st, oauth, args.wait)
|
||||||
await conns[st.device_id].start()
|
await conns[st.device_id].start()
|
||||||
|
except Exception as exc:
|
||||||
|
for c in conns.values():
|
||||||
|
try:
|
||||||
|
await c.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
msg = str(exc)
|
||||||
|
if "AUTH_TOKEN_INVALID" in msg:
|
||||||
|
raise RuntimeError("YANDEX_OAUTH_TOKEN invalid: update token and retry") from exc
|
||||||
|
raise
|
||||||
|
|
||||||
async def resolve_conn(station_sel: str) -> Optional[StationConn]:
|
async def resolve_conn(station_sel: str) -> Optional[StationConn]:
|
||||||
station_sel = (station_sel or "").strip()
|
station_sel = (station_sel or "").strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user