init
This commit is contained in:
BIN
PLStatus.exe
Normal file
BIN
PLStatus.exe
Normal file
Binary file not shown.
39
PLStatus.spec
Normal file
39
PLStatus.spec
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['status_automation.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='PLStatus',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icon.ico'],
|
||||
)
|
||||
208
README.md
Normal file
208
README.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Автоматическая смена статуса в мессенджере
|
||||
|
||||
Скрипт запускается вместе с ОС, проверяет IP по маске из конфига, ждёт запуска процесса мессенджера, через заданное время выполняет смену статуса через имитацию кликов и ввода с клавиатуры.
|
||||
|
||||
Поддерживаются **Windows** и **Linux**.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Клонируйте или скопируйте папку проекта.
|
||||
2. Создайте виртуальное окружение (рекомендуется):
|
||||
```bash
|
||||
python -m venv .venv
|
||||
# Windows:
|
||||
.venv\Scripts\activate
|
||||
# Linux:
|
||||
source .venv/bin/activate
|
||||
```
|
||||
3. Установите зависимости:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
4. **Linux:** для управления окнами установите `wmctrl`:
|
||||
```bash
|
||||
sudo apt install wmctrl # Debian/Ubuntu
|
||||
sudo dnf install wmctrl # Fedora
|
||||
```
|
||||
|
||||
## Настройка
|
||||
|
||||
1. Скопируйте пример конфига (или отредактируйте существующий):
|
||||
```bash
|
||||
copy config.json.example config.json # Windows
|
||||
cp config.json.example config.json # Linux
|
||||
```
|
||||
2. Конфиг **config.json** — объект JSON:
|
||||
- Ключи — **маски IP** (первые три октета, например `"192.168.1"`). Скрипт выбирает блок по текущему IP.
|
||||
- Значение для каждой маски — объект: **message** (текст статуса), **pause** (задержка в секундах после запуска процесса). Имя процесса и заголовок окна задаются в **"_app"** по ОС.
|
||||
- Ключ **"_app"** — общие настройки для всех масок. Обязательно укажите в зависимости от ОС:
|
||||
- **proc_name_windows** и **proc_name_linux** — имя процесса мессенджера (например `postlink-client.exe` / `postlink-client`).
|
||||
- **window_title_windows** и **window_title_linux** — заголовок окна для фокуса и поиска по PID. Скрипт сам выбирает значение по текущей ОС (Windows/Linux).
|
||||
- **Способ клика** — для каждой цели (иконка, поле статуса, кнопка «Сохранить») задаётся **один** из вариантов:
|
||||
- **Поиск по картинке** (рекомендуется для разных разрешений): **icon_image**, **status_image**, **save_button_image** — пути к файлам изображений (относительно папки скрипта, например `images/icon.png`). Скрипт ищет картинку на экране и кликает в её центр. Требуется **opencv-python**.
|
||||
- **Координаты в пикселях**: **icon_position**, **status_position**, **save_button_position** — от левого верхнего угла окна.
|
||||
- **Координаты в долях**: **icon_position_rel**, **status_position_rel**, **save_button_position_rel** — доли 0.0–1.0 от размера окна.
|
||||
- **image_confidence** (опционально) — порог совпадения при поиске по картинке (0.0–1.0, по умолчанию 0.8). Чем выше, тем строже совпадение.
|
||||
|
||||
### Поиск по картинкам (разные разрешения)
|
||||
|
||||
Сделайте скриншоты элементов интерфейса (иконка/аватар для открытия меню, поле статуса, кнопка «Сохранить»), сохраните в папку `images/` и укажите в `_app`:
|
||||
|
||||
- **icon_image**: `"images/icon.png"`
|
||||
- **status_image**: `"images/status.png"`
|
||||
- **save_button_image**: `"images/save.png"`
|
||||
|
||||
Картинки должны быть небольшими фрагментами (например 40×40–80×80 пикселей), без лишнего фона. Скрипт ищет их на экране и кликает в центр — один конфиг работает на любом разрешении и масштабе. При необходимости понизьте **image_confidence** (например 0.7), если совпадение не находится.
|
||||
|
||||
### Координаты в долях (альтернатива картинкам)
|
||||
|
||||
Чтобы один конфиг работал на разных разрешениях без картинок, задайте координаты **относительно окна** в долях 0–1 в полях `*_position_rel`:
|
||||
|
||||
- `icon_position_rel`: [доля по горизонтали, доля по вертикали] — клик по иконке/аватару.
|
||||
- `status_position_rel`: [доля X, доля Y] — поле статуса.
|
||||
- `save_button_position_rel`: [доля X, доля Y] — кнопка «Сохранить».
|
||||
|
||||
Пример: при размере окна 400×300 значение `[0.5, 0.5]` — центр окна. На другом разрешении окно может быть 600×450 — центр останется по центру.
|
||||
|
||||
### Подбор координат
|
||||
|
||||
Запустите вспомогательный скрипт, активируйте окно мессенджера и наведите мышь на нужные элементы — в консоли выводятся координаты относительно окна:
|
||||
|
||||
```bash
|
||||
python get_coords.py
|
||||
```
|
||||
|
||||
Остановка: **Ctrl+C**. Подставьте полученные значения в **config.json** в секцию `_app`: `icon_position`, `status_position`, `save_button_position`.
|
||||
|
||||
## Запуск
|
||||
|
||||
Обычный запуск (для проверки):
|
||||
|
||||
```bash
|
||||
python status_automation.py
|
||||
```
|
||||
|
||||
Логи выводятся в консоль. Скрипт выходит сразу, если текущий IP не совпадает ни с одной маской из конфига; иначе ждёт процесс, затем через `pause` выполняет клики и устанавливает статус.
|
||||
|
||||
## Автозапуск при старте ОС
|
||||
|
||||
### Windows
|
||||
|
||||
1. Откройте **Планировщик заданий** (Task Scheduler).
|
||||
2. Создайте задачу:
|
||||
- Триггер: **При входе в систему** (или **При запуске компьютера**).
|
||||
- Действие: **Запуск программы**.
|
||||
- Программа: полный путь к `python.exe` (из вашего venv или системы), например:
|
||||
```
|
||||
D:\work\PLStatus\venv\Scripts\python.exe
|
||||
```
|
||||
- Аргументы:
|
||||
```
|
||||
D:\work\PLStatus\status_automation.py
|
||||
```
|
||||
- Рабочая папка: `D:\work\PLStatus`.
|
||||
3. В свойствах задачи можно включить «Выполнять с наивысшими правами» только если реально нужно.
|
||||
|
||||
Альтернатива — ярлык в папке автозагрузки:
|
||||
|
||||
- `Win+R` → `shell:startup` → Enter.
|
||||
- Создайте ярлык с целью, например:
|
||||
`D:\work\PLStatus\venv\Scripts\pythonw.exe D:\work\PLStatus\status_automation.py`
|
||||
(использование `pythonw.exe` скрывает консоль.)
|
||||
|
||||
### Linux
|
||||
|
||||
Через **systemd** (пользовательский сервис):
|
||||
|
||||
1. Создайте файл `~/.config/systemd/user/plstatus.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=PLStatus messenger status automation
|
||||
After=network-online.target
|
||||
WantedBy=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /home/USER/PLStatus/status_automation.py
|
||||
WorkingDirectory=/home/USER/PLStatus
|
||||
Restart=on-failure
|
||||
Environment=DISPLAY=:0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Замените `USER` и путь `/home/USER/PLStatus` на свои.
|
||||
|
||||
2. Включите и запустите сервис:
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable plstatus.service
|
||||
systemctl --user start plstatus.service
|
||||
```
|
||||
|
||||
Если скрипт должен выполняться после входа в графическую сессию, убедитесь, что `DISPLAY=:0` соответствует вашей сессии (или задайте корректный `DISPLAY` в `Environment=`).
|
||||
|
||||
## Поведение
|
||||
|
||||
1. При запуске читается `config.yaml`.
|
||||
2. Вычисляются локальные IPv4 и проверяется совпадение с **ip_mask** (первые три октета). Если совпадений нет — скрипт завершается без действий.
|
||||
3. Ожидается появление процесса с именем **process_name** (опрос раз в несколько секунд).
|
||||
4. После появления процесса выполняется пауза **startup_delay** секунд (по умолчанию 60).
|
||||
5. Окно мессенджера активируется по **window_title**.
|
||||
6. Выполняются клики:
|
||||
- по иконке/аватару (**icon_position**),
|
||||
- по полю статуса (**status_position**),
|
||||
- затем Ctrl+A и Delete (очистка текущего статуса),
|
||||
- вставка **status_message** (через буфер обмена, поддерживается кириллица),
|
||||
- клик по кнопке сохранения (**save_button_position**).
|
||||
|
||||
Координаты в секции `_app` задаются **относительно окна** приложения, поэтому не зависят от положения окна на экране.
|
||||
|
||||
## Упаковка в один исполняемый файл
|
||||
|
||||
Чтобы не устанавливать Python и библиотеки на других машинах, можно собрать один исполняемый файл со всеми зависимостями.
|
||||
|
||||
### Варианты
|
||||
|
||||
| Инструмент | Плюсы | Минусы |
|
||||
|-------------|---------------------------------|---------------------------|
|
||||
| **PyInstaller** | Просто, один .exe, одна команда | Размер 50–150 МБ |
|
||||
| **cx_Freeze** | Гибкая настройка, кроссплатформа | Часто папка с файлами, не один exe |
|
||||
| **Nuitka** | Нативная сборка, быстрый старт | Долгая сборка, нужен C-компилятор |
|
||||
|
||||
Рекомендуется **PyInstaller**: один файл, работает на Windows и Linux.
|
||||
|
||||
### Сборка (PyInstaller)
|
||||
|
||||
Скрипты **build.bat** (Windows) и **build.sh** (Linux) собирают exe **без окна консоли** (`--noconsole`) и подставляют иконку, если в корне проекта есть **icon.ico**.
|
||||
|
||||
1. В папке проекта с активированным venv:
|
||||
```bash
|
||||
pip install pyinstaller
|
||||
```
|
||||
2. **Иконка (необязательно):** положите в корень проекта файл **icon.ico** (формат ICO, лучше несколько размеров: 16×16, 32×32, 48×48). Если файла нет, при первой сборке создаётся простая заглушка (или запустите `python make_icon.py`).
|
||||
**Как взять иконку из другого приложения (Windows):**
|
||||
- **Resource Hacker** (бесплатно): скачайте с [angusj.com/resourcehacker](http://www.angusj.com/resourcehacker/). Откройте в нём нужный .exe → в дереве слева откройте **Icon Group** → выберите нужную иконку (часто первая с большим размером) → правый клик → **Save icon as .ico** → сохраните как **icon.ico** в папку проекта.
|
||||
- **IconsExtract (NirSoft):** [nirsoft.net/utils/iconsext.html](https://www.nirsoft.net/utils/iconsext.html) — показывает все иконки из папки с exe, можно сохранить выбранную в .ico.
|
||||
- Рядом с программой иногда уже лежит **.ico** — скопируйте его в проект и при необходимости переименуйте в **icon.ico**.
|
||||
3. **Windows**: запустите `build.bat` или вручную:
|
||||
```cmd
|
||||
pyinstaller --onefile --noconsole --name PLStatus --clean --icon icon.ico status_automation.py
|
||||
```
|
||||
4. **Linux**: `./build.sh` или:
|
||||
```bash
|
||||
pyinstaller --onefile --noconsole --name PLStatus --clean --icon icon.ico status_automation.py
|
||||
```
|
||||
|
||||
Результат: в папке **dist/** появится **PLStatus.exe** (Windows) или **PLStatus** (Linux) — при запуске окно консоли не появляется. Размер порядка 80–150 МБ.
|
||||
|
||||
### Запуск собранного exe
|
||||
|
||||
- Рядом с исполняемым файлом должны лежать:
|
||||
- **config.json** — конфигурация (скопируйте из проекта или создайте по образцу).
|
||||
- Папка **images/** с файлами icon.png, status.png, save.png (если используется поиск по картинкам).
|
||||
- Лог пишется в **status_automation.log** в той же папке.
|
||||
- На целевой машине **Python и библиотеки устанавливать не нужно**; на Linux для управления окнами по‑прежнему нужен **wmctrl** (`sudo apt install wmctrl`).
|
||||
23
build.bat
Normal file
23
build.bat
Normal file
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM Сборка одного exe для Windows (PyInstaller). Запуск из папки проекта.
|
||||
REM Требуется: pip install pyinstaller
|
||||
|
||||
if not exist ".venv\Scripts\activate.bat" (
|
||||
echo Создайте venv и установите зависимости: python -m venv .venv && .venv\Scripts\activate && pip install -r requirements.txt
|
||||
exit /b 1
|
||||
)
|
||||
call .venv\Scripts\activate
|
||||
pip install pyinstaller -q
|
||||
|
||||
REM Иконка: если нет icon.ico — создаём заглушку (make_icon.py)
|
||||
if not exist "icon.ico" python make_icon.py
|
||||
|
||||
set ICON_OPT=
|
||||
if exist "icon.ico" set ICON_OPT=--icon icon.ico
|
||||
|
||||
pyinstaller --onefile --noconsole --name PLStatus --clean %ICON_OPT% status_automation.py
|
||||
|
||||
echo.
|
||||
echo Готово: dist\PLStatus.exe (без консоли, с иконкой если был icon.ico)
|
||||
echo Рядом с exe положите config.json и папку images\ (см. README)
|
||||
exit /b 0
|
||||
26
build.sh
Normal file
26
build.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Сборка одного исполняемого файла для Linux (PyInstaller)
|
||||
# Запуск: chmod +x build.sh && ./build.sh
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f ".venv/bin/activate" ]; then
|
||||
echo "Создайте venv и установите зависимости:"
|
||||
echo " python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
. .venv/bin/activate
|
||||
pip install pyinstaller -q
|
||||
|
||||
# Иконка: если нет icon.ico — создаём заглушку
|
||||
[ ! -f "icon.ico" ] && python make_icon.py || true
|
||||
|
||||
ICON_OPT=""
|
||||
[ -f "icon.ico" ] && ICON_OPT="--icon icon.ico"
|
||||
|
||||
pyinstaller --onefile --noconsole --name PLStatus --clean $ICON_OPT status_automation.py
|
||||
|
||||
echo ""
|
||||
echo "Готово: dist/PLStatus (без консоли, с иконкой если был icon.ico)"
|
||||
echo "Рядом с бинарником положите config.json и папку images/ (см. README)"
|
||||
24
config.json
Normal file
24
config.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"_app": {
|
||||
"proc_name_windows": "postlink-client.exe",
|
||||
"proc_name_linux": "postlink-client",
|
||||
"window_title_windows": "postlink-client.exe",
|
||||
"window_title_linux": "PostLink",
|
||||
"icon_image": "images/icon.png",
|
||||
"status_image": "images/status.png",
|
||||
"save_button_image": "images/save.png",
|
||||
"image_confidence": 0.8
|
||||
},
|
||||
"172.20.36": {
|
||||
"message": "Добро.Душный|Слабоумие и отвага:> На Ручьях: 546(р.т. None)",
|
||||
"pause": 60
|
||||
},
|
||||
"172.26.120": {
|
||||
"message": "Добро.Душный|Слабоумие и отвага:> На К37: 436(р.т. 5183)",
|
||||
"pause": 60
|
||||
},
|
||||
"default": {
|
||||
"message": "Добро.Душный|Слабоумие и отвага",
|
||||
"pause": 5
|
||||
}
|
||||
}
|
||||
24
config.json.example
Normal file
24
config.json.example
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"_app": {
|
||||
"proc_name_windows": "postlink-client.exe",
|
||||
"proc_name_linux": "postlink-client",
|
||||
"window_title_windows": "postlink-client.exe",
|
||||
"window_title_linux": "PostLink",
|
||||
"icon_image": "images/icon.png",
|
||||
"status_image": "images/status.png",
|
||||
"save_button_image": "images/save.png",
|
||||
"image_confidence": 0.8
|
||||
},
|
||||
"192.168.1": {
|
||||
"message": "Home",
|
||||
"pause": 60
|
||||
},
|
||||
"192.20.10": {
|
||||
"message": "Work",
|
||||
"pause": 40
|
||||
},
|
||||
"default": {
|
||||
"message": "Default status",
|
||||
"pause": 60
|
||||
}
|
||||
}
|
||||
69
get_coords.py
Normal file
69
get_coords.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Вспомогательный скрипт для подбора координат для config.json.
|
||||
Запустите его, затем наведите мышь на нужный элемент в окне мессенджера
|
||||
и нажмите клавишу — в консоль выведется позиция мыши.
|
||||
Для координат ОТНОСИТЕЛЬНО окна: сначала активируйте окно мессенджера,
|
||||
запустите скрипт и кликайте по элементам — скрипт покажет и экранные координаты,
|
||||
и координаты относительно активного окна (если доступно).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
def get_cursor_pos():
|
||||
import pyautogui
|
||||
return pyautogui.position()
|
||||
|
||||
def get_active_window_rect():
|
||||
"""Позиция активного окна (Windows: через pygetwindow, Linux: wmctrl)."""
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import pygetwindow as gw
|
||||
w = gw.getActiveWindow()
|
||||
if w:
|
||||
return (w.left, w.top, w.width, w.height)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
try:
|
||||
import subprocess
|
||||
out = subprocess.run(["xdotool", "getactivewindow", "getwindowgeometry"], capture_output=True, text=True, timeout=2)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
# Parse "Position: 100,200 (screen: 0)\nDimensions: 400x300"
|
||||
pos = dim = None
|
||||
for line in out.stdout.strip().splitlines():
|
||||
if line.startswith("Position:"):
|
||||
part = line.split("(", 1)[0].replace("Position:", "").strip()
|
||||
a, b = part.split(",")
|
||||
pos = (int(a.strip()), int(b.strip()))
|
||||
elif line.startswith("Dimensions:"):
|
||||
part = line.split(":", 1)[1].strip()
|
||||
w, h = part.split("x")
|
||||
dim = (int(w), int(h))
|
||||
if pos and dim:
|
||||
return (pos[0], pos[1], dim[0], dim[1])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Координаты обновляются в реальном времени. Остановка: Ctrl+C")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
x, y = get_cursor_pos()
|
||||
rect = get_active_window_rect()
|
||||
if rect:
|
||||
rel_x = x - rect[0]
|
||||
rel_y = y - rect[1]
|
||||
print(f"\rЭкран: ({x}, {y}) | Относительно окна: ({rel_x}, {rel_y}) ", end="", flush=True)
|
||||
else:
|
||||
print(f"\rЭкран: ({x}, {y}) ", end="", flush=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\nГотово.")
|
||||
15
images/README.txt
Normal file
15
images/README.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Папка для изображений при поиске по картинкам.
|
||||
|
||||
Положите сюда скриншоты элементов интерфейса PostLink (или укажите эти имена в config.json):
|
||||
|
||||
icon.png — иконка/аватар, по которой кликаем чтобы открыть меню статуса
|
||||
status.png — поле ввода статуса (небольшой фрагмент, например 50×30 px)
|
||||
save.png — кнопка «Сохранить»
|
||||
|
||||
Как сделать:
|
||||
1. Откройте окно приложения в нужном состоянии.
|
||||
2. Сделайте скриншот экрана (Win+Shift+S или Print Screen).
|
||||
3. В графическом редакторе вырежьте только нужный элемент (без лишнего фона), сохраните как PNG.
|
||||
4. Имена файлов должны совпадать с путями в config.json (например images/icon.png).
|
||||
|
||||
Чем меньше и уникальнее фрагмент — тем надёжнее поиск на любом разрешении.
|
||||
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
images/save.png
Normal file
BIN
images/save.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/status.png
Normal file
BIN
images/status.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
18
make_icon.py
Normal file
18
make_icon.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Создаёт icon.ico в папке проекта (если нет своего). Подставьте свой .ico и пересоберите."""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("Установите Pillow: pip install Pillow")
|
||||
raise
|
||||
|
||||
path = Path(__file__).resolve().parent / "icon.ico"
|
||||
if path.exists():
|
||||
print("icon.ico уже есть, не перезаписываю.")
|
||||
exit(0)
|
||||
# Простая иконка 48x48 (синий квадрат)
|
||||
img = Image.new("RGBA", (48, 48), (70, 130, 180, 255))
|
||||
img.save(path, format="ICO", sizes=[(48, 48), (32, 32), (16, 16)])
|
||||
print("Создан icon.ico. Замените своим файлом при необходимости.")
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
psutil>=5.9.0
|
||||
pyautogui>=0.9.54
|
||||
pyperclip>=1.8.2
|
||||
pygetwindow>=0.0.9; sys_platform == "win32"
|
||||
opencv-python>=4.5.0
|
||||
Pillow>=10.0.0
|
||||
mss>=9.0.0
|
||||
636
status_automation.py
Normal file
636
status_automation.py
Normal file
@@ -0,0 +1,636 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Автоматическая смена статуса в мессенджере по маске IP.
|
||||
Запускается при старте ОС, проверяет IP, ждёт процесс мессенджера,
|
||||
через заданное время выполняет клики: иконка -> статус -> очистка -> ввод текста -> сохранить.
|
||||
Работает на Windows и Linux.
|
||||
pyinstaller --onefile --name PLStatus --clean --noconsole --icon icon.ico status_automation.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import time
|
||||
import platform
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
import psutil
|
||||
import pyautogui
|
||||
import pyperclip
|
||||
|
||||
# Отключаем защиту pyautogui от выхода за край экрана (мессенджер может быть где угодно)
|
||||
pyautogui.FAILSAFE = True
|
||||
pyautogui.PAUSE = 0.15
|
||||
|
||||
CONFIG_NAME = "config.json"
|
||||
LOG_FILE = "status_automation.log"
|
||||
# При запуске из собранного exe (PyInstaller и т.п.) — папка с исполняемым файлом
|
||||
SCRIPT_DIR = Path(sys.executable).resolve().parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def _setup_logging():
|
||||
"""Настраивает логирование в файл (файл перезаписывается при каждом запуске, UTF-8)."""
|
||||
log_path = SCRIPT_DIR / LOG_FILE
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
handler = logging.FileHandler(log_path, mode="w", encoding="utf-8")
|
||||
handler.setFormatter(formatter)
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.INFO)
|
||||
if root.handlers:
|
||||
root.handlers.clear()
|
||||
root.addHandler(handler)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
log = _setup_logging()
|
||||
|
||||
|
||||
def _app_get_os(defaults, base_key):
|
||||
"""Возвращает значение из _app для текущей ОС: base_key + '_windows' или '_linux'."""
|
||||
os_key = "windows" if platform.system() == "Windows" else "linux"
|
||||
key_os = f"{base_key}_{os_key}"
|
||||
return defaults.get(key_os) or defaults.get(base_key)
|
||||
|
||||
|
||||
def load_config():
|
||||
"""
|
||||
Загружает конфиг из config.json.
|
||||
Формат: ключи — маски IP (первые три октета), значение — настройки { proc_name, message, pause };
|
||||
опциональные ключи: "_app" — общие настройки окна; "default" — профиль, если текущий IP не совпал ни с одной маской.
|
||||
Возвращает (profile, ip_mask): профиль и ключ (маска IP или "default"), или (None, None) если конфиг невалиден.
|
||||
"""
|
||||
config_path = SCRIPT_DIR / CONFIG_NAME
|
||||
if not config_path.exists():
|
||||
log.error("Файл конфигурации не найден: %s", config_path)
|
||||
sys.exit(1)
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
if not isinstance(raw, dict):
|
||||
log.error("Конфиг должен быть объектом (ключи — маски IP).")
|
||||
sys.exit(1)
|
||||
prefixes = get_local_ip_prefixes()
|
||||
defaults = raw.pop("_app", None) or {}
|
||||
fallback_block = raw.pop("default", None)
|
||||
for ip_mask, block in raw.items():
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
if ip_mask.strip() not in prefixes:
|
||||
continue
|
||||
profile = _build_profile(block, defaults, ip_mask)
|
||||
if profile is not None:
|
||||
return profile, ip_mask
|
||||
if isinstance(fallback_block, dict):
|
||||
profile = _build_profile(fallback_block, defaults, "default")
|
||||
if profile is not None:
|
||||
log.info("Текущий IP не в конфиге — используем профиль по ключу default.")
|
||||
return profile, "default"
|
||||
return None, None
|
||||
|
||||
|
||||
def _build_profile(block, defaults, label):
|
||||
"""Собирает профиль из блока и defaults. При ошибке возвращает None и логирует."""
|
||||
# Имя процесса и заголовок окна — только из _app, по текущей ОС
|
||||
process_name = _app_get_os(defaults, "proc_name")
|
||||
window_title = _app_get_os(defaults, "window_title")
|
||||
profile = {
|
||||
"window_title": window_title,
|
||||
"icon_position": block.get("icon_position") or defaults.get("icon_position"),
|
||||
"status_position": block.get("status_position") or defaults.get("status_position"),
|
||||
"save_button_position": block.get("save_button_position") or defaults.get("save_button_position"),
|
||||
"icon_position_rel": block.get("icon_position_rel") or defaults.get("icon_position_rel"),
|
||||
"status_position_rel": block.get("status_position_rel") or defaults.get("status_position_rel"),
|
||||
"save_button_position_rel": block.get("save_button_position_rel") or defaults.get("save_button_position_rel"),
|
||||
"icon_image": block.get("icon_image") or defaults.get("icon_image"),
|
||||
"status_image": block.get("status_image") or defaults.get("status_image"),
|
||||
"save_button_image": block.get("save_button_image") or defaults.get("save_button_image"),
|
||||
"image_confidence": block.get("image_confidence") or defaults.get("image_confidence") or 0.8,
|
||||
"status_message": block.get("message", ""),
|
||||
"process_name": process_name,
|
||||
"startup_delay": int(block.get("pause", 60)),
|
||||
}
|
||||
if not profile.get("process_name"):
|
||||
log.error("В _app укажите proc_name_windows и/или proc_name_linux для текущей ОС")
|
||||
return None
|
||||
if not profile.get("window_title"):
|
||||
log.error("В _app укажите window_title_windows и/или window_title_linux для текущей ОС")
|
||||
return None
|
||||
# Для каждой точки клика: либо картинка (*_image), либо координаты (*_position или *_position_rel)
|
||||
for key_img, key_px, key_rel in (
|
||||
("icon_image", "icon_position", "icon_position_rel"),
|
||||
("status_image", "status_position", "status_position_rel"),
|
||||
("save_button_image", "save_button_position", "save_button_position_rel"),
|
||||
):
|
||||
has_img = bool(profile.get(key_img))
|
||||
has_px = profile.get(key_px) is not None
|
||||
has_rel = profile.get(key_rel) is not None
|
||||
if not (has_img or has_px or has_rel):
|
||||
log.error("В конфиге для %s (или в _app) укажите %s или %s или %s", label, key_img, key_px, key_rel)
|
||||
return None
|
||||
return profile
|
||||
|
||||
|
||||
def get_local_ip_prefixes():
|
||||
"""Возвращает множество префиксов IP (первые три октета) для всех IPv4-интерфейсов."""
|
||||
prefixes = set()
|
||||
try:
|
||||
af_inet = getattr(psutil, "AF_INET", None) or socket.AF_INET
|
||||
af_link = getattr(psutil, "AF_LINK", -1)
|
||||
for name, addrs in psutil.net_if_addrs().items():
|
||||
for addr in addrs:
|
||||
if addr.family == af_link:
|
||||
continue
|
||||
if addr.family != af_inet:
|
||||
continue
|
||||
ip = getattr(addr, "address", None) or ""
|
||||
if not ip or ip.startswith("127."):
|
||||
continue
|
||||
parts = ip.split(".")
|
||||
if len(parts) == 4 and all(p.isdigit() for p in parts):
|
||||
prefixes.add(".".join(parts[:3]))
|
||||
except Exception as e:
|
||||
log.warning("Ошибка при получении списка IP: %s", e)
|
||||
print(prefixes)
|
||||
return prefixes
|
||||
|
||||
|
||||
def ip_matches_mask(ip_mask):
|
||||
"""Проверяет, совпадает ли маска (первые три октета) с одним из локальных IP."""
|
||||
ip_mask = (ip_mask or "").strip()
|
||||
if not ip_mask:
|
||||
return False
|
||||
# Нормализуем маску до трёх октетов
|
||||
parts = ip_mask.split(".")
|
||||
if len(parts) >= 3:
|
||||
prefix = ".".join(parts[:3])
|
||||
else:
|
||||
prefix = ip_mask
|
||||
return prefix in get_local_ip_prefixes()
|
||||
|
||||
|
||||
def wait_for_process(process_name, poll_interval=2):
|
||||
"""Ждёт появления процесса с заданным именем. Возвращает PID или None при ошибке."""
|
||||
name_lower = process_name.lower()
|
||||
# На Windows "App.exe", но процесс может быть и без .exe в имени — проверяем оба варианта
|
||||
log.info("Ожидание процесса: %s", process_name)
|
||||
while True:
|
||||
for proc in psutil.process_iter(["name", "pid"]):
|
||||
try:
|
||||
pname = (proc.info.get("name") or "").lower()
|
||||
if pname == name_lower or pname == name_lower.replace(".exe", ""):
|
||||
return proc.info["pid"]
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
def _focus_window_windows_by_pid(pid):
|
||||
"""Активирует главное окно процесса по PID (Windows, ctypes)."""
|
||||
if platform.system() != "Windows":
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
user32 = ctypes.windll.user32
|
||||
found_hwnd = wintypes.HWND()
|
||||
|
||||
def enum_callback(hwnd, _pid):
|
||||
if user32.IsWindowVisible(hwnd):
|
||||
wpid = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
|
||||
if wpid.value == _pid:
|
||||
found_hwnd.value = hwnd
|
||||
return False # прекратить перебор
|
||||
return True
|
||||
|
||||
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
user32.EnumWindows(WNDENUMPROC(enum_callback), pid)
|
||||
if found_hwnd.value:
|
||||
user32.SetForegroundWindow(found_hwnd.value)
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Активация окна по PID не удалась: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def _get_window_rect_windows_by_pid(pid):
|
||||
"""Возвращает (left, top, width, height) главного окна процесса (Windows)."""
|
||||
if platform.system() != "Windows":
|
||||
return None
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
user32 = ctypes.windll.user32
|
||||
rect = wintypes.RECT()
|
||||
found_hwnd = wintypes.HWND()
|
||||
|
||||
def enum_callback(hwnd, _pid):
|
||||
if user32.IsWindowVisible(hwnd):
|
||||
wpid = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
|
||||
if wpid.value == _pid:
|
||||
found_hwnd.value = hwnd
|
||||
return False
|
||||
return True
|
||||
|
||||
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
user32.EnumWindows(WNDENUMPROC(enum_callback), pid)
|
||||
if found_hwnd.value and user32.GetWindowRect(found_hwnd.value, ctypes.byref(rect)):
|
||||
return (rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top)
|
||||
except Exception as e:
|
||||
log.warning("Получение rect по PID не удалось: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def focus_window_windows(title):
|
||||
"""Фокус окна по заголовку (Windows)."""
|
||||
try:
|
||||
import pygetwindow as gw
|
||||
wins = gw.getWindowsWithTitle(title)
|
||||
if not wins:
|
||||
# Частичное совпадение
|
||||
for w in gw.getAllWindows():
|
||||
if title.lower() in (w.title or "").lower():
|
||||
w.activate()
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
return False
|
||||
wins[0].activate()
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Не удалось активировать окно (Windows): %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def _focus_window_linux_by_pid(pid):
|
||||
"""Активирует окно процесса по PID (Linux, wmctrl -lp)."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["wmctrl", "-lp"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return False
|
||||
pid_str = str(pid)
|
||||
for line in out.stdout.strip().splitlines():
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 3 and parts[2] == pid_str:
|
||||
subprocess.run(["wmctrl", "-ia", parts[0]], capture_output=True, timeout=5)
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Активация окна по PID (Linux): %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def _get_window_rect_linux_by_pid(pid):
|
||||
"""Возвращает (left, top, width, height) окна процесса (Linux, wmctrl)."""
|
||||
try:
|
||||
out = subprocess.run(["wmctrl", "-lp"], capture_output=True, text=True, timeout=5)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
pid_str = str(pid)
|
||||
wid = None
|
||||
for line in out.stdout.strip().splitlines():
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 3 and parts[2] == pid_str:
|
||||
wid = parts[0]
|
||||
break
|
||||
if not wid:
|
||||
return None
|
||||
out2 = subprocess.run(["wmctrl", "-l", "-G"], capture_output=True, text=True, timeout=5)
|
||||
if out2.returncode != 0:
|
||||
return None
|
||||
for line in out2.stdout.strip().splitlines():
|
||||
parts = line.split(None, 7)
|
||||
if len(parts) >= 6 and parts[0] == wid:
|
||||
x, y, w, h = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5])
|
||||
return (x, y, w, h)
|
||||
except Exception as e:
|
||||
log.warning("Получение rect по PID (Linux): %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def focus_window_linux(title):
|
||||
"""Фокус окна по заголовку (Linux, wmctrl)."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["wmctrl", "-a", title],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
time.sleep(0.3)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
log.warning("wmctrl не установлен. Установите: sudo apt install wmctrl (или аналог)")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
except Exception as e:
|
||||
log.warning("Ошибка активации окна (Linux): %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def get_window_rect_windows(title):
|
||||
"""Позиция и размер окна (Windows). Возвращает (left, top, width, height) или None."""
|
||||
try:
|
||||
import pygetwindow as gw
|
||||
wins = gw.getWindowsWithTitle(title)
|
||||
if not wins:
|
||||
for w in gw.getAllWindows():
|
||||
if title.lower() in (w.title or "").lower():
|
||||
return (w.left, w.top, w.width, w.height)
|
||||
return None
|
||||
w = wins[0]
|
||||
return (w.left, w.top, w.width, w.height)
|
||||
except Exception as e:
|
||||
log.warning("Ошибка получения позиции окна (Windows): %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_window_rect_linux(title):
|
||||
"""Позиция и размер окна (Linux, wmctrl -l -G). Возвращает (left, top, width, height) или None."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["wmctrl", "-l", "-G"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
for line in out.stdout.strip().splitlines():
|
||||
# wmctrl -l -G: id desktop pid x y w h host title
|
||||
parts = line.split(None, 7)
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
win_title = parts[7]
|
||||
if title.lower() in win_title.lower():
|
||||
x, y, w, h = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5])
|
||||
return (x, y, w, h)
|
||||
return None
|
||||
except Exception as e:
|
||||
log.warning("Ошибка получения позиции окна (Linux): %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def focus_window(title, pid=None):
|
||||
"""Активирует окно по заголовку; если не найдено и задан pid — по PID (Windows) или wmctrl (Linux)."""
|
||||
if platform.system() == "Windows":
|
||||
if focus_window_windows(title):
|
||||
return True
|
||||
if pid is not None and _focus_window_windows_by_pid(pid):
|
||||
log.info("Окно активировано по PID процесса (заголовок не совпал).")
|
||||
return True
|
||||
return False
|
||||
if focus_window_linux(title):
|
||||
return True
|
||||
if pid is not None and _focus_window_linux_by_pid(pid):
|
||||
log.info("Окно активировано по PID процесса (заголовок не совпал).")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_window_rect(title, pid=None):
|
||||
"""Позиция и размер окна по заголовку; если не найдено и задан pid — по PID (Windows/Linux)."""
|
||||
if platform.system() == "Windows":
|
||||
rect = get_window_rect_windows(title)
|
||||
if rect is None and pid is not None:
|
||||
rect = _get_window_rect_windows_by_pid(pid)
|
||||
return rect
|
||||
rect = get_window_rect_linux(title)
|
||||
if rect is None and pid is not None:
|
||||
rect = _get_window_rect_linux_by_pid(pid)
|
||||
return rect
|
||||
|
||||
|
||||
def click_relative(rect, relative_x, relative_y):
|
||||
"""Клик в координатах относительно окна (rect = left, top, w, h)."""
|
||||
abs_x = rect[0] + relative_x
|
||||
abs_y = rect[1] + relative_y
|
||||
pyautogui.click(abs_x, abs_y)
|
||||
|
||||
|
||||
def _resolve_click_position(rect, pixel_pos, rel_pos):
|
||||
"""
|
||||
Возвращает (abs_x, abs_y) для клика.
|
||||
rect = (left, top, width, height). Если заданы rel_pos (доли 0–1 от размера окна) — используем их;
|
||||
иначе pixel_pos (смещение в пикселях от левого верхнего угла окна).
|
||||
"""
|
||||
if rect is None:
|
||||
if pixel_pos is None:
|
||||
return None
|
||||
return (pixel_pos[0], pixel_pos[1])
|
||||
left, top, width, height = rect[0], rect[1], rect[2], rect[3]
|
||||
if rel_pos is not None and len(rel_pos) >= 2:
|
||||
try:
|
||||
rx, ry = float(rel_pos[0]), float(rel_pos[1])
|
||||
return (left + rx * width, top + ry * height)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if pixel_pos is not None and len(pixel_pos) >= 2:
|
||||
return (left + pixel_pos[0], top + pixel_pos[1])
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_image_path(path):
|
||||
"""Путь к файлу: если не абсолютный — относительно папки скрипта."""
|
||||
if not path:
|
||||
return None
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = SCRIPT_DIR / p
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
def _find_and_click_image_opencv(template_path, confidence=0.8):
|
||||
"""
|
||||
Поиск по картинке через OpenCV + mss (без pyscreeze/Pillow).
|
||||
Делает скриншот через mss, ищет шаблон через cv2.matchTemplate, кликает в центр.
|
||||
"""
|
||||
try:
|
||||
import numpy as np
|
||||
import cv2
|
||||
import mss
|
||||
except ImportError as e:
|
||||
log.warning("Поиск по картинке недоступен (нужны opencv-python, mss): %s", e)
|
||||
return False
|
||||
template = cv2.imread(str(template_path))
|
||||
if template is None:
|
||||
log.warning("OpenCV не смог прочитать изображение: %s", template_path)
|
||||
return False
|
||||
with mss.mss() as sct:
|
||||
monitor = sct.monitors[0]
|
||||
screenshot = sct.grab(monitor)
|
||||
img = np.array(screenshot)
|
||||
img_bgr = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
|
||||
result = cv2.matchTemplate(img_bgr, template, cv2.TM_CCOEFF_NORMED)
|
||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
||||
if max_val < confidence:
|
||||
return False
|
||||
h, w = template.shape[:2]
|
||||
center_x = max_loc[0] + w // 2
|
||||
center_y = max_loc[1] + h // 2
|
||||
pyautogui.click(center_x, center_y)
|
||||
return True
|
||||
|
||||
|
||||
def find_and_click_image(image_path, confidence=0.8, timeout=10, retries=3):
|
||||
"""
|
||||
Ищет изображение на экране, кликает в центр.
|
||||
Сначала пробует pyautogui.locateOnScreen (pyscreeze + Pillow);
|
||||
при ошибке импорта — резервно OpenCV + mss.
|
||||
"""
|
||||
resolved = _resolve_image_path(image_path)
|
||||
if not resolved:
|
||||
log.warning("Файл изображения не найден: %s", image_path)
|
||||
return False
|
||||
path_str = str(resolved)
|
||||
use_opencv_fallback = False
|
||||
for attempt in range(retries):
|
||||
if not use_opencv_fallback:
|
||||
try:
|
||||
loc = pyautogui.locateOnScreen(path_str, confidence=confidence)
|
||||
if loc is not None:
|
||||
x = loc.left + loc.width // 2
|
||||
y = loc.top + loc.height // 2
|
||||
pyautogui.click(x, y)
|
||||
return True
|
||||
except TypeError:
|
||||
try:
|
||||
loc = pyautogui.locateOnScreen(path_str)
|
||||
if loc is not None:
|
||||
x = loc.left + loc.width // 2
|
||||
y = loc.top + loc.height // 2
|
||||
pyautogui.click(x, y)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
use_opencv_fallback = True
|
||||
except Exception as e:
|
||||
err_text = str(e).lower()
|
||||
if "pyscreeze" in err_text or "pillow" in err_text or "unable to import" in err_text:
|
||||
use_opencv_fallback = True
|
||||
else:
|
||||
log.warning("Поиск по картинке %s (попытка %s): %s", image_path, attempt + 1, e)
|
||||
if use_opencv_fallback:
|
||||
if _find_and_click_image_opencv(resolved, confidence=confidence):
|
||||
return True
|
||||
if attempt < retries - 1:
|
||||
time.sleep(timeout / retries)
|
||||
continue
|
||||
if attempt < retries - 1:
|
||||
time.sleep(timeout / retries)
|
||||
if use_opencv_fallback:
|
||||
log.warning("Изображение на экране не найдено (OpenCV): %s", image_path)
|
||||
else:
|
||||
log.warning("Изображение на экране не найдено: %s", image_path)
|
||||
return False
|
||||
|
||||
|
||||
def set_status(config, pid=None):
|
||||
"""Выполняет последовательность: фокус окна, клики, очистка, ввод текста, сохранить. pid — для резервного поиска окна по процессу."""
|
||||
title = config["window_title"]
|
||||
message = config["status_message"]
|
||||
confidence = float(config.get("image_confidence", 0.8))
|
||||
|
||||
if not focus_window(title, pid=pid):
|
||||
log.error("Не удалось активировать окно: %s", title)
|
||||
return False
|
||||
|
||||
time.sleep(0.7)
|
||||
rect = get_window_rect(title, pid=pid)
|
||||
use_icon_img = config.get("icon_image")
|
||||
use_status_img = config.get("status_image")
|
||||
use_save_img = config.get("save_button_image")
|
||||
|
||||
# Иконка
|
||||
if use_icon_img:
|
||||
if not find_and_click_image(use_icon_img, confidence=confidence):
|
||||
log.error("Не найдена картинка иконки: %s", use_icon_img)
|
||||
return False
|
||||
else:
|
||||
icon_xy = _resolve_click_position(
|
||||
rect, config.get("icon_position"), config.get("icon_position_rel")
|
||||
)
|
||||
if icon_xy is None:
|
||||
log.error("Не заданы координаты иконки (icon_image или icon_position / icon_position_rel).")
|
||||
return False
|
||||
if not rect:
|
||||
log.warning("Позиция окна не получена; координаты считаются абсолютными.")
|
||||
pyautogui.click(icon_xy[0], icon_xy[1])
|
||||
time.sleep(0.4)
|
||||
|
||||
# Поле статуса
|
||||
if use_status_img:
|
||||
if not find_and_click_image(use_status_img, confidence=confidence):
|
||||
log.error("Не найдена картинка поля статуса: %s", use_status_img)
|
||||
return False
|
||||
else:
|
||||
status_xy = _resolve_click_position(
|
||||
rect, config.get("status_position"), config.get("status_position_rel")
|
||||
)
|
||||
if status_xy is None:
|
||||
log.error("Не заданы координаты поля статуса (status_image или status_position / status_position_rel).")
|
||||
return False
|
||||
pyautogui.click(status_xy[0], status_xy[1])
|
||||
|
||||
time.sleep(0.5)
|
||||
# Выделить всё и удалить (очистка текущего статуса)
|
||||
pyautogui.hotkey("ctrl", "a")
|
||||
time.sleep(0.3)
|
||||
pyautogui.press("delete")
|
||||
time.sleep(0.3)
|
||||
# Ввод нового статуса (через буфер обмена)
|
||||
pyperclip.copy(message)
|
||||
pyautogui.hotkey("ctrl", "v")
|
||||
time.sleep(0.3)
|
||||
|
||||
# Кнопка «Сохранить»
|
||||
if use_save_img:
|
||||
if not find_and_click_image(use_save_img, confidence=confidence):
|
||||
log.error("Не найдена картинка кнопки «Сохранить»: %s", use_save_img)
|
||||
return False
|
||||
else:
|
||||
save_xy = _resolve_click_position(
|
||||
rect, config.get("save_button_position"), config.get("save_button_position_rel")
|
||||
)
|
||||
if save_xy is None:
|
||||
log.error("Не заданы координаты кнопки сохранения (save_button_image или save_button_position / save_button_position_rel).")
|
||||
return False
|
||||
pyautogui.click(save_xy[0], save_xy[1])
|
||||
|
||||
log.info("Статус установлен: %s", message)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
config, ip_mask = load_config()
|
||||
if config is None:
|
||||
log.info("Текущий IP не совпадает ни с одной маской из конфига — выход без действий.")
|
||||
return 0
|
||||
|
||||
process_name = config["process_name"]
|
||||
startup_delay = config["startup_delay"]
|
||||
|
||||
log.info("IP совпадает с маской %s, ожидание процесса %s...", ip_mask, process_name)
|
||||
pid = wait_for_process(process_name)
|
||||
log.info("Процесс запущен (PID %s), ожидание %s сек...", pid, startup_delay)
|
||||
time.sleep(startup_delay)
|
||||
|
||||
set_status(config, pid=pid)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user